diff --git a/.github/iwyu.imp b/.github/iwyu.imp index 05542f396..444edbe91 100644 --- a/.github/iwyu.imp +++ b/.github/iwyu.imp @@ -47,4 +47,6 @@ { include: [ '', private, '', public ] }, + { include: [ '', private, '', public ] }, + ] diff --git a/.github/spellcheck-wordlist.txt b/.github/spellcheck-wordlist.txt index 0344768d0..f4af92b93 100644 --- a/.github/spellcheck-wordlist.txt +++ b/.github/spellcheck-wordlist.txt @@ -163,6 +163,8 @@ BTRH BTT bttransport btusb +# aspell considers PIPE_BUF to be 2 words :( +BUF CCE CIEV CIND @@ -191,6 +193,7 @@ eoc EP EPIPE EPMR +epoll EQMID errno FB diff --git a/doc/bluealsad.8.rst b/doc/bluealsad.8.rst index 7532d0db9..ef0c0ff70 100644 --- a/doc/bluealsad.8.rst +++ b/doc/bluealsad.8.rst @@ -58,6 +58,12 @@ OPTIONS Without this option, **bluealsad** registers itself as an "org.bluealsa" D-Bus service. For more information see the EXAMPLES_ below. +-M, --multi-client + Permit multiple clients to connect to the same PCM stream. + Without this option, only one client can connect to a PCM. + With this option, for playback clients, the streams are mixed together; + for capture each client receives a copy of the stream. + -i hciX, --device=hciX HCI device to use. Can be specified multiple times to select more than one HCI. Because HCI numbering can change after a system reboot, this option diff --git a/src/Makefile.am b/src/Makefile.am index 09d56a68b..56d3b2bc8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -35,6 +35,9 @@ bluealsad_SOURCES = \ ba-transport-pcm.c \ bluealsa-dbus.c \ bluealsa-iface.xml \ + bluealsa-mix-buffer.c \ + bluealsa-pcm-client.c \ + bluealsa-pcm-multi.c \ bluez.c \ bluez-iface.xml \ codec-sbc.c \ diff --git a/src/a2dp-aac.c b/src/a2dp-aac.c index 2e8a680d0..c9979da99 100644 --- a/src/a2dp-aac.c +++ b/src/a2dp-aac.c @@ -30,6 +30,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "io.h" #include "rtp.h" #include "utils.h" @@ -291,6 +292,11 @@ void *a2dp_aac_enc_thread(struct ba_transport_pcm *t_pcm) { /* Get the delay introduced by the encoder. */ t_pcm->codec_delay_dms = info.nDelay * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; /* initialize RTP header and get anchor for payload */ uint8_t *rtp_payload = rtp_a2dp_init(bt.data, &rtp_header, NULL, 0); @@ -471,6 +477,11 @@ void *a2dp_aac_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to 90kHz */ rtp_state_init(&rtp, rate, 90000); diff --git a/src/a2dp-aptx-hd.c b/src/a2dp-aptx-hd.c index 16664f21d..f0d7ef53c 100644 --- a/src/a2dp-aptx-hd.c +++ b/src/a2dp-aptx-hd.c @@ -26,6 +26,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "codec-aptx.h" #include "io.h" #include "rtp.h" @@ -138,6 +139,11 @@ void *a2dp_aptx_hd_enc_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; /* initialize RTP header and get anchor for payload */ uint8_t *rtp_payload = rtp_a2dp_init(bt.data, &rtp_header, NULL, 0); @@ -271,6 +277,11 @@ void *a2dp_aptx_hd_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to PCM sample rate */ rtp_state_init(&rtp, rate, rate); diff --git a/src/a2dp-aptx.c b/src/a2dp-aptx.c index 171bb3445..e5d9dc624 100644 --- a/src/a2dp-aptx.c +++ b/src/a2dp-aptx.c @@ -25,6 +25,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "codec-aptx.h" #include "io.h" #include "shared/a2dp-codecs.h" @@ -135,6 +136,11 @@ void *a2dp_aptx_enc_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { @@ -249,6 +255,11 @@ void *a2dp_aptx_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { diff --git a/src/a2dp-faststream.c b/src/a2dp-faststream.c index 08b704928..9e3e40239 100644 --- a/src/a2dp-faststream.c +++ b/src/a2dp-faststream.c @@ -24,6 +24,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "codec-sbc.h" #include "io.h" #include "shared/a2dp-codecs.h" @@ -157,6 +158,11 @@ void *a2dp_fs_enc_thread(struct ba_transport_pcm *t_pcm) { /* Get the total delay introduced by the codec. */ t_pcm->codec_delay_dms = sbc_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { @@ -277,6 +283,11 @@ void *a2dp_fs_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { diff --git a/src/a2dp-lc3plus.c b/src/a2dp-lc3plus.c index 66f782cfd..fb3f6c357 100644 --- a/src/a2dp-lc3plus.c +++ b/src/a2dp-lc3plus.c @@ -30,6 +30,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "io.h" #include "rtp.h" #include "utils.h" @@ -254,6 +255,11 @@ void *a2dp_lc3plus_enc_thread(struct ba_transport_pcm *t_pcm) { const int lc3plus_delay_frames = lc3plus_enc_get_delay(handle); t_pcm->codec_delay_dms = lc3plus_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; rtp_media_header_t *rtp_media_header; /* initialize RTP headers and get anchor for payload */ @@ -462,6 +468,11 @@ void *a2dp_lc3plus_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to the RTP clock rate */ rtp_state_init(&rtp, rate, rtp_ts_clockrate); diff --git a/src/a2dp-ldac.c b/src/a2dp-ldac.c index 53004d615..a83d06479 100644 --- a/src/a2dp-ldac.c +++ b/src/a2dp-ldac.c @@ -27,6 +27,7 @@ #include "ba-transport.h" #include "ba-transport-pcm.h" #include "ba-config.h" +#include "bluealsa-pcm-multi.h" #include "io.h" #include "rtp.h" #include "utils.h" @@ -170,6 +171,11 @@ void *a2dp_ldac_enc_thread(struct ba_transport_pcm *t_pcm) { /* Get the total delay introduced by the codec. */ t_pcm->codec_delay_dms = ldac_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; rtp_media_header_t *rtp_media_header; /* initialize RTP headers and get anchor for payload */ @@ -327,6 +333,11 @@ void *a2dp_ldac_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to PCM sample rate */ rtp_state_init(&rtp, rate, rate); diff --git a/src/a2dp-mpeg.c b/src/a2dp-mpeg.c index cf9cefc87..7c12fa4cf 100644 --- a/src/a2dp-mpeg.c +++ b/src/a2dp-mpeg.c @@ -35,6 +35,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "io.h" #include "rtp.h" #include "utils.h" @@ -230,6 +231,11 @@ void *a2dp_mp3_enc_thread(struct ba_transport_pcm *t_pcm) { const int mpeg_delay_frames = lame_get_encoder_delay(handle); t_pcm->codec_delay_dms = mpeg_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; rtp_mpeg_audio_header_t *rtp_mpeg_audio_header; /* initialize RTP headers and get anchor for payload */ @@ -415,6 +421,11 @@ void *a2dp_mpeg_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to 90kHz */ rtp_state_init(&rtp, rate, 90000); diff --git a/src/a2dp-opus.c b/src/a2dp-opus.c index 2e82f86fb..81b63929e 100644 --- a/src/a2dp-opus.c +++ b/src/a2dp-opus.c @@ -28,6 +28,7 @@ #include "ba-config.h" #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "io.h" #include "rtp.h" #include "shared/a2dp-codecs.h" @@ -173,6 +174,11 @@ void *a2dp_opus_enc_thread(struct ba_transport_pcm *t_pcm) { opus_encoder_ctl(opus, OPUS_GET_LOOKAHEAD(&opus_delay_frames)); t_pcm->codec_delay_dms = opus_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; rtp_media_header_t *rtp_media_header; /* initialize RTP headers and get anchor for payload */ @@ -304,6 +310,11 @@ void *a2dp_opus_dec_thread(struct ba_transport_pcm *t_pcm) { opus_decoder_ctl(opus, OPUS_GET_LOOKAHEAD(&opus_delay_frames)); t_pcm->codec_delay_dms = opus_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to PCM sample rate */ rtp_state_init(&rtp, rate, rate); diff --git a/src/a2dp-sbc.c b/src/a2dp-sbc.c index b62a83d4f..8063ba4ae 100644 --- a/src/a2dp-sbc.c +++ b/src/a2dp-sbc.c @@ -29,6 +29,7 @@ #include "ba-transport.h" #include "ba-transport-pcm.h" #include "ba-config.h" +#include "bluealsa-pcm-multi.h" #include "codec-sbc.h" #include "io.h" #include "rtp.h" @@ -181,6 +182,11 @@ void *a2dp_sbc_enc_thread(struct ba_transport_pcm *t_pcm) { /* Get the total delay introduced by the codec. */ t_pcm->codec_delay_dms = sbc_delay_frames * 10000 / rate; + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + rtp_header_t *rtp_header; rtp_media_header_t *rtp_media_header; @@ -324,6 +330,11 @@ void *a2dp_sbc_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && + !bluealsa_pcm_multi_init(t_pcm->multi, pcm.nmemb)) + goto fail_ffb; + struct rtp_state rtp = { .synced = false }; /* RTP clock frequency equal to PCM sample rate */ rtp_state_init(&rtp, rate, rate); diff --git a/src/asound/bluealsa-pcm.c b/src/asound/bluealsa-pcm.c index 124cb4a2f..532d3461e 100644 --- a/src/asound/bluealsa-pcm.c +++ b/src/asound/bluealsa-pcm.c @@ -577,6 +577,10 @@ static int bluealsa_hw_params(snd_pcm_ioplug_t *io, snd_pcm_hw_params_t *params) return ret; if (pcm->ba_pcm.channels != channels || pcm->ba_pcm.rate != rate) { + if (pcm->ba_pcm.running) { + SNDERR("Couldn't change BlueALSA PCM configuration"); + return -EINVAL; + } debug2("Changing BlueALSA PCM configuration: %u ch, %u Hz -> %u ch, %u Hz", pcm->ba_pcm.channels, pcm->ba_pcm.rate, channels, rate); @@ -1423,6 +1427,21 @@ static int bluealsa_set_hw_constraint(struct bluealsa_pcm *pcm) { unsigned int list[ARRAYSIZE(codec->rates)]; unsigned int n; + /* If the PCM is already running, we must not change the codec config as + * that would terminate the stream for the running client */ + if (pcm->ba_pcm.running) { + if ((err = snd_pcm_ioplug_set_param_minmax(io, + SND_PCM_IOPLUG_HW_CHANNELS, pcm->ba_pcm.channels, + pcm->ba_pcm.channels)) < 0) + return err; + + if ((err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_RATE, + pcm->ba_pcm.rate, pcm->ba_pcm.rate)) < 0) + return err; + + return 0; + } + /* Populate the list of supported channels and sample rates. For codecs * with fixed configuration, the list will contain only one element. For * other codecs, the list might contain all supported configurations. */ @@ -1655,6 +1674,7 @@ SND_PCM_PLUGIN_DEFINE_FUNC(bluealsa) { * sample rate and channels for HW constraints. */ const char *canonical = ba_dbus_pcm_codec_get_canonical_name(codec_name); const bool name_changed = strcmp(canonical, pcm->ba_pcm.codec.name) != 0; + if (name_changed && !ba_dbus_pcm_select_codec(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path, canonical, NULL, 0, 0, 0, BA_PCM_SELECT_CODEC_FLAG_NONE, &err)) { SNDERR("Couldn't select BlueALSA PCM codec: %s", err.message); @@ -1677,9 +1697,50 @@ SND_PCM_PLUGIN_DEFINE_FUNC(bluealsa) { } } + + if (codec_name[0] != '\0') { + const char *canonical = ba_dbus_pcm_codec_get_canonical_name(codec_name); + const bool name_changed = strcmp(canonical, pcm->ba_pcm.codec.name) != 0; + + if (pcm->ba_pcm.running) { + /* If the PCM is already running we must not change the codec config + * as that would terminate the stream for the running client */ + if (name_changed) + SNDERR("Couldn't change BlueALSA PCM codec"); + else if (codec_config_len > 0 && ( + pcm->ba_pcm.codec.data_len != codec_config_len || + memcmp(pcm->ba_pcm.codec.data, codec_config, codec_config_len) != 0)) + SNDERR("Couldn't change BlueALSA PCM codec configuration"); + } + else { + /* If the codec was given, change it now, so we can get the correct + * sample rate and channels for HW constraints. */ + if (name_changed && !ba_dbus_pcm_select_codec(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path, + canonical, NULL, 0, 0, 0, BA_PCM_SELECT_CODEC_FLAG_NONE, &err)) { + SNDERR("Couldn't select BlueALSA PCM codec: %s", err.message); + dbus_error_free(&err); + } + else { + + memcpy(pcm->ba_pcm_codec_config, codec_config, codec_config_len); + pcm->ba_pcm_codec_config_len = codec_config_len; + + /* Changing the codec may change the audio format, sample rate and/or + * channels. We need to refresh our cache of PCM properties. */ + if (name_changed && !ba_dbus_pcm_get(&pcm->dbus_ctx, &ba_addr, ba_profile, + stream == SND_PCM_STREAM_PLAYBACK ? BA_PCM_MODE_SINK : BA_PCM_MODE_SOURCE, + &pcm->ba_pcm, &err)) { + SNDERR("Couldn't get BlueALSA PCM: %s", err.message); + ret = -dbus_error_to_errno(&err); + goto fail; + } + + } + } + } /* If the BT transport codec is not known (which means that PCM sample - * rate is also not know), we cannot construct useful constraints. */ + * rate is also not known), we cannot construct useful constraints. */ if (pcm->ba_pcm.rate == 0) { ret = -EAGAIN; goto fail; diff --git a/src/ba-config.c b/src/ba-config.c index d5c413083..36eee2151 100644 --- a/src/ba-config.c +++ b/src/ba-config.c @@ -46,6 +46,8 @@ struct ba_config config = { .disable_realtek_usb_fix = false, + .multi_enabled = false, + /* CVSD is a mandatory codec */ .hfp.codecs.cvsd = true, #if ENABLE_MSBC diff --git a/src/ba-config.h b/src/ba-config.h index 8d8ee407d..217afc024 100644 --- a/src/ba-config.h +++ b/src/ba-config.h @@ -75,6 +75,9 @@ struct ba_config { /* disable alt-3 MTU for mSBC with Realtek USB adapters */ bool disable_realtek_usb_fix; + /* Is multi client support enabled? */ + bool multi_enabled; + struct { /* available HFP codecs */ diff --git a/src/ba-transport-pcm.c b/src/ba-transport-pcm.c index d9306aa14..8f21b7533 100644 --- a/src/ba-transport-pcm.c +++ b/src/ba-transport-pcm.c @@ -33,6 +33,7 @@ #include "ba-rfcomm.h" #include "ba-transport.h" #include "bluealsa-dbus.h" +#include "bluealsa-pcm-multi.h" #include "bluez-iface.h" #include "bluez.h" #include "dbus.h" @@ -79,6 +80,9 @@ int transport_pcm_init( pcm->pipe[0] = -1; pcm->pipe[1] = -1; + pcm->multi = NULL; + pcm->paused = false; + for (size_t i = 0; i < ARRAYSIZE(pcm->volume); i++) { pcm->volume[i].level = config.volume_init_level; ba_transport_pcm_volume_set(&pcm->volume[i], NULL, NULL, NULL); @@ -96,6 +100,9 @@ int transport_pcm_init( t->d->ba_dbus_path, transport_get_dbus_path_type(t->profile), mode == BA_TRANSPORT_PCM_MODE_SOURCE ? "source" : "sink"); + if (bluealsa_pcm_multi_enabled(t)) + pcm->multi = bluealsa_pcm_multi_create(pcm); + return 0; } @@ -118,6 +125,11 @@ void transport_pcm_free( g_free(pcm->ba_dbus_path); + if (pcm->multi != NULL) { + bluealsa_pcm_multi_free(pcm->multi); + pcm->multi = NULL; + } + } /** @@ -238,6 +250,10 @@ void ba_transport_pcm_thread_cleanup(struct ba_transport_pcm *pcm) { ba_transport_stop_async(t); pthread_mutex_unlock(&t->bt_fd_mtx); + /* Stop multi client thread if required. */ + if (pcm->multi) + bluealsa_pcm_multi_reset(pcm->multi); + /* Release BT socket file descriptor duplicate created either in the * ba_transport_pcm_start() function or in the IO thread itself. */ ba_transport_pcm_bt_release(pcm); @@ -522,7 +538,7 @@ int ba_transport_pcm_drop(struct ba_transport_pcm *pcm) { pthread_mutex_unlock(&pcm->mutex); #endif - if (io_pcm_flush(pcm) == -1) + if (!pcm->multi && io_pcm_flush(pcm) == -1) return -1; int rv = ba_transport_pcm_signal_send(pcm, BA_TRANSPORT_PCM_SIGNAL_DROP); @@ -740,7 +756,10 @@ int ba_transport_pcm_delay_get(const struct ba_transport_pcm *pcm) { else if (t->profile & BA_TRANSPORT_PROFILE_MASK_AG) delay += 10; - return delay; + if (pcm->multi) + return delay + pcm->multi->delay; + else + return delay; } /** diff --git a/src/ba-transport-pcm.h b/src/ba-transport-pcm.h index acd1e9d85..814dc968f 100644 --- a/src/ba-transport-pcm.h +++ b/src/ba-transport-pcm.h @@ -160,6 +160,9 @@ struct ba_transport_pcm { char *ba_dbus_path; bool ba_dbus_exported; + /* Multi-client stream support */ + struct bluealsa_pcm_multi *multi; + }; int transport_pcm_init( diff --git a/src/bluealsa-dbus.c b/src/bluealsa-dbus.c index 431b1199d..5f5b66c15 100644 --- a/src/bluealsa-dbus.c +++ b/src/bluealsa-dbus.c @@ -39,6 +39,7 @@ #include "ba-transport.h" #include "ba-transport-pcm.h" #include "bluealsa-iface.h" +#include "bluealsa-pcm-multi.h" #include "bluez.h" #include "dbus.h" #include "hfp.h" @@ -496,7 +497,7 @@ static void bluealsa_pcm_open(GDBusMethodInvocation *inv, void *userdata) { const int pcm_fd = pcm->fd; pthread_mutex_unlock(&pcm->mutex); - if (pcm_fd != -1) { + if (pcm->multi == NULL && pcm_fd != -1) { g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR, G_DBUS_ERROR_LIMITS_EXCEEDED, "%s", strerror(EBUSY)); goto fail; @@ -522,8 +523,9 @@ static void bluealsa_pcm_open(GDBusMethodInvocation *inv, void *userdata) { * headset will not run voltage converter (power-on its circuit board) until * the transport is acquired in order to extend battery life. For profiles * like A2DP Sink and HFP headset, we will wait for incoming connection. */ - if (t_profile & BA_TRANSPORT_PROFILE_A2DP_SOURCE || - t_profile & BA_TRANSPORT_PROFILE_MASK_AG) { + if ((t_profile & BA_TRANSPORT_PROFILE_A2DP_SOURCE || + t_profile & BA_TRANSPORT_PROFILE_MASK_AG) + && pcm_fd == -1) { if (ba_transport_acquire(t) == -1) { g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR, @@ -540,24 +542,35 @@ static void bluealsa_pcm_open(GDBusMethodInvocation *inv, void *userdata) { } - pthread_mutex_lock(&pcm->mutex); + /* When multiple client support is enabled, management is delegated to the + * multi client thread. + * Otherwise control channels are managed in this thread. */ + if (pcm->multi) { + /* create new multi client instance */ + if (!bluealsa_pcm_multi_add_client(pcm->multi, pcm_fds[is_sink ? 0 : 1], pcm_fds[2])) + goto fail; + } + else { + pthread_mutex_lock(&pcm->mutex); - /* get correct PIPE endpoint - PIPE is unidirectional */ - pcm->fd = pcm_fds[is_sink ? 0 : 1]; - /* set newly opened PCM as active */ - pcm->paused = false; + /* get correct PIPE endpoint - PIPE is unidirectional */ + pcm->fd = pcm_fds[is_sink ? 0 : 1]; + /* set newly opened PCM as active */ + pcm->paused = false; - GIOChannel *ch = g_io_channel_unix_new(pcm_fds[2]); - g_io_channel_set_close_on_unref(ch, TRUE); - g_io_channel_set_encoding(ch, NULL, NULL); - g_io_channel_set_buffered(ch, FALSE); + GIOChannel *ch = g_io_channel_unix_new(pcm_fds[2]); + g_io_channel_set_close_on_unref(ch, TRUE); + g_io_channel_set_encoding(ch, NULL, NULL); + g_io_channel_set_buffered(ch, FALSE); - pcm->controller = g_io_create_watch_full(ch, G_PRIORITY_DEFAULT, - G_IO_IN, bluealsa_pcm_controller, ba_transport_pcm_ref(pcm), - (GDestroyNotify)ba_transport_pcm_unref); - g_io_channel_unref(ch); + pcm->controller = g_io_create_watch_full(ch, G_PRIORITY_DEFAULT, + G_IO_IN, bluealsa_pcm_controller, ba_transport_pcm_ref(pcm), + (GDestroyNotify)ba_transport_pcm_unref); + g_io_channel_unref(ch); - pthread_mutex_unlock(&pcm->mutex); + pthread_mutex_unlock(&pcm->mutex); + + } /* notify our PCM IO thread that the PCM was opened */ ba_transport_pcm_signal_send(pcm, BA_TRANSPORT_PCM_SIGNAL_OPEN); diff --git a/src/bluealsa-mix-buffer.c b/src/bluealsa-mix-buffer.c new file mode 100644 index 000000000..97ce275aa --- /dev/null +++ b/src/bluealsa-mix-buffer.c @@ -0,0 +1,360 @@ +/* + * BlueALSA - bluealsa-mix-buffer.c + * Copyright (c) 2016-2024 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include + +#include "ba-transport-pcm.h" +#include "shared/log.h" +#include "bluealsa-mix-buffer.h" +#include "bluealsa-pcm-multi.h" + +#define BLUEALSA_24BIT_MIN (int32_t)0xFF800000 +#define BLUEALSA_24BIT_MAX (int32_t)0x007FFFFF + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +#define BA_S16_2LE_TO_INT32(x) (x) +#define BA_S32_4LE_TO_INT64(x) (x) +#define BA_S24_4LE_TO_INT32(x) (int32_t)((x & 0x00800000) ? x | 0xFF000000 : x) +#define BA_INT32_TO_S24_4LE(x) (uint32_t)(((uint32_t)x & 0x80000000) >> 8) | ((uint32_t)x & 0x007FFFFF) + +#elif __BYTE_ORDER == __BIG_ENDIAN + +#include +#define BA_S16_2LE_TO_INT32(x) (int16_t)byteswap_16(x) +#define BA_S32_4LE_TO_INT64(x) (int32_t)byteswap_32(x) + +#define BA_S24_LE_TO_INT32(x) ( \ + ((uint32_t)x & 0xFF000000) >> 24 | \ + ((uint32_t)x & 0x00FF0000) >> 8 | \ + ((uint32_t)x & 0x0000FF00) << 8 | \ + ((x & 0x00008000) ? 0xFF : 0x00)) + +#define BA_INT32_TO_S24_4LE(x) ( \ + ((x & 0x80000000) ? 0xFF : 0x00)) | \ + ((uint32_t)x & 0x00FF0000) >> 8 | \ + ((uint32_t)x & 0x0000FF00) << 8 | \ + ((uint32_t)x & 0x000000FF) << 24 + +#else +# error "Unknown byte order" +#endif + +/** + * Configure the mix buffer for use with given transport stream parameters. + * + * @param buffer Pointer to the buffer that is to be configured. + * @param format The sample format that will be used. + * @param channels The number of channels in each frame. + * @param buffer_frames The requested capacity of the buffer, in frames. + * @param period_frames The number of frames to be transferred at one time.*/ +int bluealsa_mix_buffer_init(struct bluealsa_mix_buffer *buffer, + uint16_t format, uint8_t channels, + size_t buffer_frames, size_t period_frames) { + buffer->format = format; + buffer->channels = channels; + /* We allow for 1 extra empty frame in the buffer. */ + buffer->size = (1 + buffer_frames) * channels; + buffer->period = period_frames * channels; + buffer->mix_offset = 0; + buffer->end = 0; + switch(format) { + case BA_TRANSPORT_PCM_FORMAT_U8: + buffer->frame_size = channels * sizeof(uint8_t); + buffer->data.s16 = calloc(buffer->size, sizeof(int16_t)); + break; + case BA_TRANSPORT_PCM_FORMAT_S16_2LE: + buffer->frame_size = channels * sizeof(int16_t); + buffer->data.s32 = calloc(buffer->size, sizeof(int32_t)); + break; + case BA_TRANSPORT_PCM_FORMAT_S24_4LE: + buffer->frame_size = channels * sizeof(int32_t); + buffer->data.s32 = calloc(buffer->size, sizeof(int32_t)); + break; + case BA_TRANSPORT_PCM_FORMAT_S32_4LE: + buffer->frame_size = channels * sizeof(int32_t); + buffer->data.s64 = calloc(buffer->size, sizeof(int64_t)); + break; + default: + error("Invalid format %u", format); + return -1; + break; + } + if (buffer->data.any == NULL) { + error("Out of memory"); + return -1; + } + return 0; +} + +/** + * Release the resources used by a mix buffer. */ +void bluealsa_mix_buffer_release(struct bluealsa_mix_buffer *buffer) { + buffer->size = 0; + free(buffer->data.any); + buffer->data.any = NULL; +} + +/** + * The number of samples that can be read from start offset to end offset. + * + * @param start offset of first sample to be read. + * @param end offset of last sample to be read. */ +size_t bluealsa_mix_buffer_calc_avail(const struct bluealsa_mix_buffer *buffer, size_t start, size_t end) { + if (end >= start) + return end - start; + else + return buffer->size + end - start; +} + +/** + * Is the buffer empty ? */ +bool bluealsa_mix_buffer_empty(const struct bluealsa_mix_buffer *buffer) { + return buffer->mix_offset == buffer->end; +} + +/** + * The delay, expressed in samples, that would be incurred by adding the next + * frame at the given offset. */ +size_t bluealsa_mix_buffer_delay(const struct bluealsa_mix_buffer *buffer, size_t offset) { + return bluealsa_mix_buffer_calc_avail(buffer, buffer->mix_offset, offset); +} + +bool bluealsa_mix_buffer_at_threshold(struct bluealsa_mix_buffer *buffer) { + size_t avail = bluealsa_mix_buffer_calc_avail(buffer, buffer->mix_offset, buffer->end); + return avail >= BLUEALSA_MULTI_MIX_THRESHOLD * buffer->period / buffer->channels; +} + +/** + * Add a stream of bytes from a client into the mix. + * + * @param offset Current position of this client in the mix buffer. To be + * stored between calls. A negative value is interpreted as relative to + * (ahead of) the current mix offset. + * @param data Pointer to the byte stream. + * @param bytes The number of bytes in the stream. + * @return The number of bytes actually added into the mix. This value is always + * a whole number of frames. */ +ssize_t bluealsa_mix_buffer_add(struct bluealsa_mix_buffer *buffer, ssize_t *offset, const void *data, size_t bytes) { + + size_t mix_offset = buffer->mix_offset; + size_t avail = bluealsa_mix_buffer_calc_avail(buffer, mix_offset, buffer->end); + size_t start; + + if (*offset < 0) + start = mix_offset - *offset; + else + start = *offset; + + /* Only allow complete frames into the mix. */ + size_t frames = bytes / buffer->frame_size; + size_t samples = frames * buffer->channels; + + /* Do not allow any client to advance more than one period ahead of the + * others. */ + if (start < mix_offset) + start += buffer->size; + size_t limit = mix_offset + (BLUEALSA_MULTI_MIX_THRESHOLD + 1) * buffer->period; + if (start >= limit) + return 0; + if (start + samples > limit) + samples = limit - start; + + size_t n; + for (n = 0; n < samples; n++) { + if (start + n >= buffer->size) + start -= buffer->size; + switch (buffer->format) { + case BA_TRANSPORT_PCM_FORMAT_U8: { + uint8_t *ptr = (uint8_t*)data; + buffer->data.s16[start + n] += ptr[n] - 0x80; + break; + } + case BA_TRANSPORT_PCM_FORMAT_S16_2LE: { + const int16_t *ptr = (const int16_t*)data; + buffer->data.s32[start + n] += BA_S16_2LE_TO_INT32(ptr[n]); + break; + } + case BA_TRANSPORT_PCM_FORMAT_S24_4LE: { + const uint32_t *ptr = (const uint32_t*)data; + buffer->data.s32[start + n] += BA_S24_4LE_TO_INT32(ptr[n]); + break; + } + case BA_TRANSPORT_PCM_FORMAT_S32_4LE: { + const int32_t *ptr = (const int32_t*)data; + buffer->data.s64[start + n] += BA_S32_4LE_TO_INT64(ptr[n]); + break; + } + } + } + + *offset = start + n; + + /* If this addition has increased the number of available frames, update + * the end pointer. */ + if (bluealsa_mix_buffer_calc_avail(buffer, mix_offset, *offset) > avail) + buffer->end = *offset; + + /* return number of bytes consumed from client */ + return samples * buffer->frame_size / buffer->channels; +} + +/** + * Read mixed frames from the mix buffer. + * + * Applies volume scaling to the samples returned. + * + * @param data Pointer to a buffer in which to place the frames. + * @param frames Size of the data buffer in samples. + * @param scale An array of scaling factors, one for each channel of the stream. + * @return number of samples fetched from mix. This is always complete frames. + * */ +size_t bluealsa_mix_buffer_read(struct bluealsa_mix_buffer *buffer, void *data, size_t samples, double *scale) { + + size_t start = buffer->mix_offset; + size_t end = buffer->end; + samples -= samples % buffer->channels; + + /* Limit each read to 1 period. */ + if (samples > buffer->period) + samples = buffer->period; + + /* Do not read beyond the last sample written. */ + size_t avail = bluealsa_mix_buffer_calc_avail(buffer, start, end); + if (samples > avail) + samples = avail; + + size_t out_offset = 0; + size_t n; + for (n = 0; n < samples; n += buffer->channels) { + if (start + n >= buffer->size) + start -= buffer->size; + switch (buffer->format) { + case BA_TRANSPORT_PCM_FORMAT_U8: { + uint8_t *dest = (uint8_t*) data; + int16_t *sample = buffer->data.s16 + start + n; + int channel; + for (channel = 0; channel < buffer->channels; channel++) { + if (scale[channel] == 0.0) { + sample[channel] = 0; + } + else { + sample[channel] *= scale[channel]; + if (sample[channel] > INT8_MAX) + sample[channel] = INT8_MAX; + else if (sample[channel] < INT8_MIN) + sample[channel] = INT8_MIN; + } + dest[out_offset++] = + 0x80 + (int8_t)htole16((uint8_t)sample[channel]); + sample[channel] = 0; + } + break; + } + case BA_TRANSPORT_PCM_FORMAT_S16_2LE: { + int16_t *dest = (int16_t*) data; + int32_t *sample = buffer->data.s32 + start + n; + int channel; + for (channel = 0; channel < buffer->channels; channel++) { + if (scale[channel] == 0.0) { + sample[channel] = 0; + } + else { + if (scale[channel] < 0.99) + sample[channel] *= scale[channel]; + if (sample[channel] > INT16_MAX) + sample[channel] = INT16_MAX; + else if (sample[channel] < INT16_MIN) + sample[channel] = INT16_MIN; + } + dest[out_offset++] = + (int16_t)htole16((uint16_t)sample[channel]); + sample[channel] = 0; + } + break; + } + case BA_TRANSPORT_PCM_FORMAT_S24_4LE: { + uint32_t *dest = (uint32_t*) data; + int32_t *sample = buffer->data.s32 + start + n; + int channel; + for (channel = 0; channel < buffer->channels; channel++) { + if (scale[channel] == 0.0) { + sample[channel] = 0; + } + else { + sample[channel] *= scale[channel]; + if (sample[channel] > BLUEALSA_24BIT_MAX) + sample[channel] = BLUEALSA_24BIT_MAX; + else if (sample[channel] < BLUEALSA_24BIT_MIN) + sample[channel] = BLUEALSA_24BIT_MIN; + } + dest[out_offset++] = + (uint32_t)BA_INT32_TO_S24_4LE(sample[channel]); + sample[channel] = 0; + } + break; + } + case BA_TRANSPORT_PCM_FORMAT_S32_4LE: { + int32_t *dest = (int32_t*) data; + int64_t *sample = buffer->data.s64 + start + n; + int channel; + for (channel = 0; channel < buffer->channels; channel++) { + if (scale[channel] == 0.0) { + sample[channel] = 0; + } + else { + sample[channel] *= scale[channel]; + if (sample[channel] > INT32_MAX) + sample[channel] = INT32_MAX; + else if (sample[channel] < INT32_MIN) + sample[channel] = INT32_MIN; + } + dest[out_offset++] = + (int32_t)htole32((uint32_t)sample[channel]); + sample[channel] = 0; + } + break; + } + } + } + + buffer->mix_offset = start + n; + + return samples; +} + +void bluealsa_mix_buffer_clear(struct bluealsa_mix_buffer *buffer) { + buffer->mix_offset = 0; + buffer->end = 0; + size_t buffer_bytes; + switch(buffer->format) { + case BA_TRANSPORT_PCM_FORMAT_U8: + buffer_bytes = buffer->size * sizeof(int16_t); + break; + case BA_TRANSPORT_PCM_FORMAT_S16_2LE: + case BA_TRANSPORT_PCM_FORMAT_S24_4LE: + buffer_bytes = buffer->size * sizeof(int32_t); + break; + case BA_TRANSPORT_PCM_FORMAT_S32_4LE: + buffer_bytes = buffer->size * sizeof(int64_t); + break; + default: + /* not reached */ + buffer_bytes = 0; + } + memset(buffer->data.any, 0, buffer_bytes); +} diff --git a/src/bluealsa-mix-buffer.h b/src/bluealsa-mix-buffer.h new file mode 100644 index 000000000..14ed8de4e --- /dev/null +++ b/src/bluealsa-mix-buffer.h @@ -0,0 +1,59 @@ +/* + * BlueALSA - bluealsa-mix-buffer.h + * Copyright (c) 2016-2024 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ +#ifndef BLUEALSA_MIX_BUFFER_H +#define BLUEALSA_MIX_BUFFER_H + +#include +#include +#include + +struct bluealsa_mix_buffer { + /* sample format */ + uint16_t format; + uint8_t channels; + /* physical bytes per frame */ + uint16_t frame_size; + union { + int16_t *s16; + int32_t *s32; + int64_t *s64; + void *any; + } data; + /* Capacity of the buffer in samples */ + size_t size; + /* The number of samples to be transferred at one time */ + size_t period; + /* Position of next read from the mix */ + _Atomic size_t mix_offset; + /* Postion after last sample written to the mix */ + size_t end; +}; + +int bluealsa_mix_buffer_init(struct bluealsa_mix_buffer *buffer, + uint16_t format, uint8_t channels, + size_t buffer_frames, size_t period_frames); + +void bluealsa_mix_buffer_release(struct bluealsa_mix_buffer *buffer); + +bool bluealsa_mix_buffer_at_threshold(struct bluealsa_mix_buffer *buffer); + +size_t bluealsa_mix_buffer_calc_avail(const struct bluealsa_mix_buffer *buffer, size_t start, size_t end); +bool bluealsa_mix_buffer_empty(const struct bluealsa_mix_buffer *buffer); +size_t bluealsa_mix_buffer_delay(const struct bluealsa_mix_buffer *buffer, size_t offset); + +ssize_t bluealsa_mix_buffer_add(struct bluealsa_mix_buffer *buffer, + ssize_t *offset, const void *data, size_t bytes); + +size_t bluealsa_mix_buffer_read(struct bluealsa_mix_buffer *buffer, + void *data, size_t frames, double *scale); + +void bluealsa_mix_buffer_clear(struct bluealsa_mix_buffer *buffer); + +#endif /* BLUEALSA_MIX_BUFFER_H */ diff --git a/src/bluealsa-pcm-client.c b/src/bluealsa-pcm-client.c new file mode 100644 index 000000000..c9560443e --- /dev/null +++ b/src/bluealsa-pcm-client.c @@ -0,0 +1,543 @@ +/* + * BlueALSA - bluealsa-pcm-client.c + * Copyright (c) 2016-2024 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ba-config.h" +#include "ba-transport-pcm.h" +#include "bluealsa-iface.h" +#include "bluealsa-mix-buffer.h" +#include "bluealsa-pcm-client.h" +#include "bluealsa-pcm-multi.h" +#include "shared/log.h" + +/* How long to wait for drain to complete, in nanoseconds */ +#define BLUEALSA_PCM_CLIENT_DRAIN_NS 300000000 +#define BLUEALSA_CLIENT_BUFFER_PERIODS (BLUEALSA_MULTI_CLIENT_THRESHOLD + 1) + +static bool bluealsa_pcm_client_is_playback(struct bluealsa_pcm_client *client) { + return client->multi->pcm->mode == BA_TRANSPORT_PCM_MODE_SINK; +} + +static bool bluealsa_pcm_client_is_capture(struct bluealsa_pcm_client *client) { + return client->multi->pcm->mode == BA_TRANSPORT_PCM_MODE_SOURCE; +} + +static size_t bluealsa_pcm_client_playback_init_offset(const struct bluealsa_pcm_client *client) { + const struct bluealsa_mix_buffer *buffer = &client->multi->playback_buffer; + return (BLUEALSA_MULTI_MIX_THRESHOLD * buffer->period) - (client->in_offset * buffer->channels / buffer->frame_size); +} + +/** + * Perform side-effects associated with a state change. */ +static void bluealsa_pcm_client_set_state(struct bluealsa_pcm_client *client, enum bluealsa_pcm_client_state new_state) { + if (new_state == client->state) + return; + + switch (new_state) { + case BLUEALSA_PCM_CLIENT_STATE_IDLE: + client->drain_avail = (size_t) -1; + /* fallthrough */ + case BLUEALSA_PCM_CLIENT_STATE_FINISHED: + if (client->state == BLUEALSA_PCM_CLIENT_STATE_RUNNING || client->state == BLUEALSA_PCM_CLIENT_STATE_DRAINING1) + client->multi->active_count--; + break; + case BLUEALSA_PCM_CLIENT_STATE_PAUSED: + if (client->state == BLUEALSA_PCM_CLIENT_STATE_RUNNING && bluealsa_pcm_client_is_capture(client)) + client->multi->active_count--; + break; + case BLUEALSA_PCM_CLIENT_STATE_RUNNING: + if (bluealsa_pcm_client_is_capture(client)) { + if (client->state == BLUEALSA_PCM_CLIENT_STATE_IDLE || client->state == BLUEALSA_PCM_CLIENT_STATE_INIT || client->state == BLUEALSA_PCM_CLIENT_STATE_PAUSED) + client->multi->active_count++; + } + else { + if (client->state == BLUEALSA_PCM_CLIENT_STATE_IDLE) { + client->out_offset = -bluealsa_pcm_client_playback_init_offset(client); + client->multi->active_count++; + } + else if (client->state == BLUEALSA_PCM_CLIENT_STATE_DRAINING1) + return; + } + break; + case BLUEALSA_PCM_CLIENT_STATE_DRAINING1: + break; + case BLUEALSA_PCM_CLIENT_STATE_DRAINING2: + if (client->state == BLUEALSA_PCM_CLIENT_STATE_DRAINING1) + client->multi->active_count--; + break; + default: + /* not reached */ + break; + } + client->state = new_state; +} + +/** + * Clean up resources associated with a client PCM connection. */ +static void bluealsa_pcm_client_close_pcm(struct bluealsa_pcm_client *client) { + if (client->pcm_fd != -1) { + epoll_ctl(client->multi->epoll_fd, EPOLL_CTL_DEL, client->pcm_fd, NULL); + client->watch = false; + close(client->pcm_fd); + client->pcm_fd = -1; + } +} + +/** + * Clean up resources associated with a client control connection. */ +static void bluealsa_pcm_client_close_control( + struct bluealsa_pcm_client *client) { + if (client->control_fd != -1) { + epoll_ctl(client->multi->epoll_fd, EPOLL_CTL_DEL, client->control_fd, NULL); + close(client->control_fd); + client->control_fd = -1; + } +} + +/** + * Start/stop watching for PCM i/o events. */ +static void bluealsa_pcm_client_watch_pcm( + struct bluealsa_pcm_client *client, bool enabled) { + if (client->watch == enabled) + return; + + const uint32_t type = bluealsa_pcm_client_is_playback(client) ? EPOLLIN : EPOLLOUT; + struct epoll_event event = { + .events = enabled ? type : 0, + .data.ptr = &client->pcm_event, + }; + epoll_ctl(client->multi->epoll_fd, EPOLL_CTL_MOD, client->pcm_fd, &event); + client->watch = enabled; +} + +/** + * Start/stop watching for drain timer expiry event. */ +static void bluealsa_pcm_client_watch_drain(struct bluealsa_pcm_client *client, bool enabled) { + struct itimerspec timeout = { + .it_interval = { 0 }, + .it_value = { + .tv_sec = 0, + .tv_nsec = enabled ? BLUEALSA_PCM_CLIENT_DRAIN_NS : 0, + }, + }; + timerfd_settime(client->drain_timer_fd, 0, &timeout, NULL); +} + +/** + * Read bytes from FIFO. + * @return number of bytes read, or -1 if client closed pipe */ +static ssize_t bluealsa_pcm_client_read(struct bluealsa_pcm_client *client) { + + const size_t space = client->buffer_size - client->in_offset; + if (space == 0) + return 0; + + uint8_t *buf = client->buffer + client->in_offset; + + ssize_t bytes; + while ((bytes = read(client->pcm_fd, buf, space)) == -1 && errno == EINTR) + continue; + + /* pipe closed by remote end */ + if (bytes == 0) + return -1; + + /* FIFO may be empty but client still open. */ + if (bytes == -1 && errno == EAGAIN) + bytes = 0; + + if (bytes > 0) + client->in_offset += bytes; + + return bytes; +} + +/** + * Write samples to the client fifo + */ +void bluealsa_pcm_client_write(struct bluealsa_pcm_client *client, const void *buffer, size_t samples) { + const int fd = client->pcm_fd; + const uint8_t *buffer_ = buffer; + size_t len = samples * BA_TRANSPORT_PCM_FORMAT_BYTES(client->multi->pcm->format); + ssize_t ret; + + do { + + if ((ret = write(fd, buffer_, len)) == -1) + switch (errno) { + case EINTR: + continue; + case EAGAIN: + /* If the client is so slow that the FIFO fills up, then it + * is inevitable that audio frames will be eventually be + * dropped in the bluetooth controller if we block here. + * It is better that we discard frames here so that the + * decoder is not interrupted. */ + warn("Dropping PCM frames: %s", "PCM overrun"); + ret = len; + break; + default: + /* The client has closed the pipe, or an unrecoverable error + * has occurred. */ + bluealsa_pcm_client_close_pcm(client); + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_FINISHED); + return; + } + + buffer_ += ret; + len -= ret; + + } while (len != 0); + +} + +/** + * Deliver samples to transport mix. */ +void bluealsa_pcm_client_deliver(struct bluealsa_pcm_client *client) { + struct bluealsa_pcm_multi *multi = client->multi; + + if (client->state != BLUEALSA_PCM_CLIENT_STATE_RUNNING && + client->state != BLUEALSA_PCM_CLIENT_STATE_DRAINING1) + return; + + if (client->state == BLUEALSA_PCM_CLIENT_STATE_DRAINING1) { + ssize_t bytes = bluealsa_pcm_client_read(client); + if (bytes < 0) { + /* client has closed pcm connection */ + bluealsa_pcm_client_close_pcm(client); + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_FINISHED); + return; + } + if (client->in_offset == 0 && bytes == 0 && errno == EAGAIN) { + size_t mix_avail = bluealsa_mix_buffer_calc_avail(&multi->playback_buffer, multi->playback_buffer.mix_offset, client->out_offset); + if (mix_avail == 0 || mix_avail > client->drain_avail) { + /* The mix buffer has completely drained all frames from + * this client. We now wait some time for the bluetooth system + * to play out all sent frames*/ + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_DRAINING2); + bluealsa_pcm_client_watch_drain(client, true); + return; + } + else + client->drain_avail = mix_avail; + } + } + + if (client->in_offset > 0) { + ssize_t delivered = bluealsa_mix_buffer_add(&multi->playback_buffer, &client->out_offset, client->buffer, client->in_offset); + if (delivered > 0) { + memmove(client->buffer, client->buffer + delivered, client->in_offset - delivered); + client->in_offset -= delivered; + + /* If the input buffer was full, we now have room for more. */ + bluealsa_pcm_client_watch_pcm(client, true); + } + } +} + +/** + * Action taken when event occurs on client PCM playback connection. */ +static void bluealsa_pcm_client_handle_playback_pcm(struct bluealsa_pcm_client *client) { + + ssize_t bytes = bluealsa_pcm_client_read(client); + if (bytes < 0) { + /* client has closed pcm connection */ + bluealsa_pcm_client_close_pcm(client); + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_FINISHED); + return; + } + + /* If buffer is full, stop reading from FIFO */ + if (bytes == 0) + bluealsa_pcm_client_watch_pcm(client, false); + + /* Begin adding to mix when sufficient periods are buffered. */ + if (client->state == BLUEALSA_PCM_CLIENT_STATE_IDLE) { + if (client->in_offset > BLUEALSA_MULTI_CLIENT_THRESHOLD * client->multi->period_bytes) + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_RUNNING); + } +} + +/** + * Action client Drain request. + * + * Starts drain timer. */ +static void bluealsa_pcm_client_begin_drain( + struct bluealsa_pcm_client *client) { + debug("DRAIN: client %zu", client->id); + if (bluealsa_pcm_client_is_playback(client) && client->state == BLUEALSA_PCM_CLIENT_STATE_RUNNING) { + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_DRAINING1); + bluealsa_pcm_client_watch_pcm(client, false); + } + else { + if (write(client->control_fd, "OK", 2) != 2) + error("client control response failed"); + } +} + +/** + * Action client Drop request. */ +static void bluealsa_pcm_client_drop(struct bluealsa_pcm_client *client) { + debug("DROP: client %zu", client->id); + if (bluealsa_pcm_client_is_playback(client)) { + bluealsa_pcm_client_watch_drain(client, false); + splice(client->pcm_fd, NULL, config.null_fd, NULL, 1024 * 32, SPLICE_F_NONBLOCK); + client->in_offset = 0; + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_IDLE); + client->drop = true; + } +} + +/** + * Action client Pause request. */ +static void bluealsa_pcm_client_pause(struct bluealsa_pcm_client *client) { + debug("PAUSE: client %zu", client->id); + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_PAUSED); + bluealsa_pcm_client_watch_pcm(client, false); + if (bluealsa_pcm_client_is_playback(client)) { + struct bluealsa_mix_buffer *buffer = &client->multi->playback_buffer; + client->out_offset = -bluealsa_mix_buffer_delay(buffer, client->out_offset); + } +} + +/** + * Action client Resume request. */ +static void bluealsa_pcm_client_resume(struct bluealsa_pcm_client *client) { + debug("RESUME: client %zu", client->id); + if (client->state == BLUEALSA_PCM_CLIENT_STATE_IDLE) { + if (bluealsa_pcm_client_is_playback(client)) { + bluealsa_pcm_client_watch_pcm(client, true); + client->drop = false; + } + else + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_RUNNING); + } + if (client->state == BLUEALSA_PCM_CLIENT_STATE_PAUSED) { + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_RUNNING); + if (bluealsa_pcm_client_is_playback(client)) + bluealsa_pcm_client_watch_pcm(client, true); + } +} + +/** + * Action taken when drain timer expires. */ +static void bluealsa_pcm_client_handle_drain(struct bluealsa_pcm_client *client) { + debug("DRAIN COMPLETE: client %zu", client->id); + if (client->state != BLUEALSA_PCM_CLIENT_STATE_DRAINING2) + return; + + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_IDLE); + bluealsa_pcm_client_watch_drain(client, false); + bluealsa_pcm_client_watch_pcm(client, true); + client->in_offset = 0; + if (write(client->control_fd, "OK", 2) != 2) + error("client control response failed"); +} + +/** + * Action taken when event occurs on client control connection. */ +static void bluealsa_pcm_client_handle_control(struct bluealsa_pcm_client *client) { + char command[6]; + ssize_t len; + do { + len = read(client->control_fd, command, sizeof(command)); + } while (len == -1 && errno == EINTR); + + if (len == -1) { + if (errno == EAGAIN) + return; + } + + if (len <= 0) { + bluealsa_pcm_client_close_control(client); + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_FINISHED); + return; + } + + if (client->state == BLUEALSA_PCM_CLIENT_STATE_DRAINING1 || + client->state == BLUEALSA_PCM_CLIENT_STATE_DRAINING2) { + /* Should not happen - a well-behaved client will block during drain. + * However, not all clients are well behaved. So we invoke the + * drain complete handler before processing this request.*/ + bluealsa_pcm_client_handle_drain(client); + } + + if (strncmp(command, BLUEALSA_PCM_CTRL_DRAIN, len) == 0) { + bluealsa_pcm_client_begin_drain(client); + } + else if (strncmp(command, BLUEALSA_PCM_CTRL_DROP, len) == 0) { + bluealsa_pcm_client_drop(client); + len = write(client->control_fd, "OK", 2); + } + else if (strncmp(command, BLUEALSA_PCM_CTRL_PAUSE, len) == 0) { + bluealsa_pcm_client_pause(client); + len = write(client->control_fd, "OK", 2); + } + else if (strncmp(command, BLUEALSA_PCM_CTRL_RESUME, len) == 0) { + bluealsa_pcm_client_resume(client); + len = write(client->control_fd, "OK", 2); + } + else { + warn("Invalid PCM control command: %*s", (int)len, command); + len = write(client->control_fd, "Invalid", 7); + } +} + +/** + * Marshall client events. + * Invokes appropriate action. */ +void bluealsa_pcm_client_handle_event(struct bluealsa_pcm_client_event *event) { + struct bluealsa_pcm_client *client = event->client; + switch(event->type) { + case BLUEALSA_EVENT_TYPE_PCM: + if (bluealsa_pcm_client_is_playback(client)) + bluealsa_pcm_client_handle_playback_pcm(client); + break; + case BLUEALSA_EVENT_TYPE_CONTROL: + bluealsa_pcm_client_handle_control(client); + break; + case BLUEALSA_EVENT_TYPE_DRAIN: + bluealsa_pcm_client_handle_drain(client); + break; + } +} + +void bluealsa_pcm_client_handle_close_event( + struct bluealsa_pcm_client_event *event) { + struct bluealsa_pcm_client *client = event->client; + switch (event->type) { + case BLUEALSA_EVENT_TYPE_PCM: + bluealsa_pcm_client_close_pcm(client); + break; + case BLUEALSA_EVENT_TYPE_CONTROL: + bluealsa_pcm_client_close_control(client); + break; + default: + g_assert_not_reached(); + } + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_FINISHED); +} + +/** + * Allocate a buffer suitable for transport transfer size, and set initial + * state. */ +bool bluealsa_pcm_client_init(struct bluealsa_pcm_client *client) { + struct bluealsa_pcm_multi *multi = client->multi; + + if (bluealsa_pcm_client_is_playback(client)) { + client->buffer_size = BLUEALSA_CLIENT_BUFFER_PERIODS * multi->period_bytes; + + client->buffer = calloc(client->buffer_size, sizeof(uint8_t)); + if (client->buffer == NULL) { + error("Unable to allocate client buffer: %s", strerror(errno)); + return false; + } + + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_IDLE); + bluealsa_pcm_client_watch_pcm(client, true); + } + else { + /* Capture clients are active immediately. */ + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_RUNNING); + } + + return true; +} + +/** + * Allocate a new client instance. */ +struct bluealsa_pcm_client *bluealsa_pcm_client_new(struct bluealsa_pcm_multi *multi, int pcm_fd, int control_fd) { + struct bluealsa_pcm_client *client = calloc(1, sizeof(struct bluealsa_pcm_client)); + if (!client) { + error("Unable to create new client: %s", strerror(errno)); + return NULL; + } + + client->multi = multi; + client->pcm_fd = pcm_fd; + client->control_fd = control_fd; + client->drain_timer_fd = -1; + client->pcm_event.type = BLUEALSA_EVENT_TYPE_PCM; + client->pcm_event.client = client; + + client->control_event.type = BLUEALSA_EVENT_TYPE_CONTROL; + client->control_event.client = client; + + struct epoll_event ep_event = { + .data.ptr = &client->pcm_event, + }; + + if (epoll_ctl(multi->epoll_fd, EPOLL_CTL_ADD, client->pcm_fd, &ep_event) == -1) { + error("Unable to init client, epoll_ctl: %s\n", strerror(errno)); + bluealsa_pcm_client_free(client); + return NULL; + } + + ep_event.data.ptr = &client->control_event; + ep_event.events = EPOLLIN; + if (epoll_ctl(multi->epoll_fd, EPOLL_CTL_ADD, client->control_fd, &ep_event) == -1) { + error("Unable to init client, epoll_ctl: %s\n", strerror(errno)); + epoll_ctl(multi->epoll_fd, EPOLL_CTL_DEL, client->pcm_fd, NULL); + bluealsa_pcm_client_free(client); + return NULL; + } + + if (bluealsa_pcm_client_is_playback(client)) { + client->drain_timer_fd = timerfd_create(CLOCK_MONOTONIC, 0); + client->drain_event.type = BLUEALSA_EVENT_TYPE_DRAIN; + client->drain_event.client = client; + + ep_event.data.ptr = &client->drain_event; + ep_event.events = EPOLLIN; + if (epoll_ctl(multi->epoll_fd, EPOLL_CTL_ADD, client->drain_timer_fd, &ep_event) == -1) { + error("Unable to init client, epoll_ctl: %s", strerror(errno)); + epoll_ctl(multi->epoll_fd, EPOLL_CTL_DEL, client->pcm_fd, NULL); + epoll_ctl(multi->epoll_fd, EPOLL_CTL_DEL, client->control_fd, NULL); + bluealsa_pcm_client_free(client); + return NULL; + } + + } + + client->watch = false; + client->state = BLUEALSA_PCM_CLIENT_STATE_INIT; + + return client; +} + +/** + * Free the resources used by a client. */ +void bluealsa_pcm_client_free(struct bluealsa_pcm_client *client) { + if (bluealsa_pcm_client_is_playback(client)) { + epoll_ctl(client->multi->epoll_fd, EPOLL_CTL_DEL, client->drain_timer_fd, NULL); + if (client->drain_timer_fd >= 0) + close(client->drain_timer_fd); + free(client->buffer); + } + bluealsa_pcm_client_close_pcm(client); + bluealsa_pcm_client_close_control(client); + bluealsa_pcm_client_set_state(client, BLUEALSA_PCM_CLIENT_STATE_FINISHED); + free(client); +} diff --git a/src/bluealsa-pcm-client.h b/src/bluealsa-pcm-client.h new file mode 100644 index 000000000..77228e294 --- /dev/null +++ b/src/bluealsa-pcm-client.h @@ -0,0 +1,78 @@ +/* + * BlueALSA - bluealsa-pcm-client.h + * Copyright (c) 2016-2024 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#ifndef BLUEALSA_PCM_CLIENT_H +#define BLUEALSA_PCM_CLIENT_H + +#include +#include +#include + +#include "bluealsa-pcm-multi.h" +#include "config.h" + +enum bluealsa_pcm_client_state { + BLUEALSA_PCM_CLIENT_STATE_INIT = 0, + BLUEALSA_PCM_CLIENT_STATE_IDLE, + BLUEALSA_PCM_CLIENT_STATE_RUNNING, + BLUEALSA_PCM_CLIENT_STATE_PAUSED, + BLUEALSA_PCM_CLIENT_STATE_DRAINING1, + BLUEALSA_PCM_CLIENT_STATE_DRAINING2, + BLUEALSA_PCM_CLIENT_STATE_FINISHED, +}; + +enum bluealsa_pcm_client_event_type { + BLUEALSA_EVENT_TYPE_PCM, + BLUEALSA_EVENT_TYPE_CONTROL, + BLUEALSA_EVENT_TYPE_DRAIN, +}; + +struct bluealsa_pcm_client_event { + enum bluealsa_pcm_client_event_type type; + struct bluealsa_pcm_client *client; +}; + +struct bluealsa_pcm_client { + struct bluealsa_pcm_multi *multi; + int pcm_fd; + int control_fd; + int drain_timer_fd; + struct bluealsa_pcm_client_event pcm_event; + struct bluealsa_pcm_client_event control_event; + struct bluealsa_pcm_client_event drain_event; + enum bluealsa_pcm_client_state state; + uint8_t *buffer; + size_t buffer_size; + size_t in_offset; + ssize_t out_offset; + size_t drain_avail; + bool drop; + bool watch; +#if DEBUG + size_t id; +#endif +}; + +struct bluealsa_pcm_client *bluealsa_pcm_client_new( + struct bluealsa_pcm_multi *multi, + int pcm_fd, int control_fd); + +bool bluealsa_pcm_client_init(struct bluealsa_pcm_client *client); + +void bluealsa_pcm_client_free(struct bluealsa_pcm_client *client); + +void bluealsa_pcm_client_handle_event(struct bluealsa_pcm_client_event *event); +void bluealsa_pcm_client_handle_close_event(struct bluealsa_pcm_client_event *event); +void bluealsa_pcm_client_deliver(struct bluealsa_pcm_client *client); +void bluealsa_pcm_client_fetch(struct bluealsa_pcm_client *client); +void bluealsa_pcm_client_write(struct bluealsa_pcm_client *client, const void *buffer, size_t samples); +void bluealsa_pcm_client_drain(struct bluealsa_pcm_client *client); + +#endif /* BLUEALSA_PCM_CLIENT_H */ diff --git a/src/bluealsa-pcm-multi.c b/src/bluealsa-pcm-multi.c new file mode 100644 index 000000000..8fb812945 --- /dev/null +++ b/src/bluealsa-pcm-multi.c @@ -0,0 +1,627 @@ +/* + * BlueALSA - bluealsa-pcm-multi.c + * Copyright (c) 2016-2024 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ba-config.h" +#include "bluealsa-pcm-multi.h" +#include "bluealsa-pcm-client.h" +#include "ba-transport-pcm.h" +#include "ba-transport.h" +#include "shared/log.h" +#include "shared/defs.h" + + +/* Limit number of clients to ensure sufficient resources are available. */ +#define BLUEALSA_MULTI_MAX_CLIENTS 32 + +/* Size of epoll event array. Allow for client control, pcm, and drain timer, + * plus the mix event fd. */ +#define BLUEALSA_MULTI_MAX_EVENTS (1 + BLUEALSA_MULTI_MAX_CLIENTS * 3) + +/* Determines the size of the mix buffer. */ +#define BLUEALSA_MULTI_BUFFER_PERIODS 16 + +static void *bluealsa_pcm_mix_thread_func(struct bluealsa_pcm_multi *multi); +static void *bluealsa_pcm_snoop_thread_func(struct bluealsa_pcm_multi *multi); +static void bluealsa_pcm_multi_remove_client(struct bluealsa_pcm_multi *multi, struct bluealsa_pcm_client *client); + + +static bool bluealsa_pcm_multi_is_capture(const struct bluealsa_pcm_multi *multi) { + return multi->pcm->mode == BA_TRANSPORT_PCM_MODE_SOURCE; +} + +static bool bluealsa_pcm_multi_is_playback(const struct bluealsa_pcm_multi *multi) { + return multi->pcm->mode == BA_TRANSPORT_PCM_MODE_SINK; +} + +static bool bluealsa_pcm_multi_is_target(const struct bluealsa_pcm_multi *multi) { + return multi->pcm->t->profile & (BA_TRANSPORT_PROFILE_A2DP_SINK | BA_TRANSPORT_PROFILE_MASK_HF); +} + +static void bluealsa_pcm_multi_cleanup(struct bluealsa_pcm_multi *multi) { + if (multi->thread != config.main_thread) { + eventfd_write(multi->event_fd, 0xDEAD0000); + pthread_join(multi->thread, NULL); + multi->thread = config.main_thread; + } + if (bluealsa_pcm_multi_is_playback(multi) && multi->playback_buffer.size > 0) + bluealsa_mix_buffer_release(&multi->playback_buffer); + + pthread_mutex_lock(&multi->client_mutex); + while (multi->client_count > 0) + bluealsa_pcm_multi_remove_client(multi, g_list_first(multi->clients)->data); + multi->client_count = 0; + pthread_mutex_unlock(&multi->client_mutex); +} + +/** + * Is multi-client support implemented and configured for the given transport ? */ +bool bluealsa_pcm_multi_enabled(const struct ba_transport *t) { + if (!config.multi_enabled) + return false; + + if (t->profile & BA_TRANSPORT_PROFILE_MASK_A2DP) + return t->a2dp.pcm.format != BA_TRANSPORT_PCM_FORMAT_S24_3LE; + + return true; +} + +/** + * Create multi-client support for the given transport pcm. */ +struct bluealsa_pcm_multi *bluealsa_pcm_multi_create(struct ba_transport_pcm *pcm) { + + struct bluealsa_pcm_multi *multi = calloc(1, sizeof(struct bluealsa_pcm_multi)); + if (multi == NULL) + return multi; + + multi->pcm = pcm; + multi->thread = config.main_thread; + + pthread_mutex_init(&multi->client_mutex, NULL); + pthread_mutex_init(&multi->buffer_mutex, NULL); + pthread_cond_init(&multi->cond, NULL); + + if ((multi->epoll_fd = epoll_create(1)) == -1) + goto fail; + + if ((multi->event_fd = eventfd(0, 0)) == -1) + goto fail; + + pcm->multi = multi; + + return multi; + +fail: + if (multi->epoll_fd != -1) + close(multi->epoll_fd); + if (multi->event_fd != -1) + close(multi->event_fd); + free(multi); + return NULL; +} + +static void bluealsa_pcm_multi_init_clients(struct bluealsa_pcm_multi *multi) { + pthread_mutex_lock(&multi->client_mutex); + GList *el; + for (el = multi->clients; el != NULL; el = el->next) { + struct bluealsa_pcm_client *client = el->data; + if (client->buffer == NULL) { + if (!bluealsa_pcm_client_init(client)) + bluealsa_pcm_multi_remove_client(client->multi, client); + } + } + pthread_mutex_unlock(&multi->client_mutex); +} + +/** + * Start the multi client thread. */ +static bool bluealsa_pcm_multi_start(struct bluealsa_pcm_multi *multi) { + + if (bluealsa_pcm_multi_is_playback(multi)) { + if (pthread_create(&multi->thread, NULL, PTHREAD_FUNC(bluealsa_pcm_mix_thread_func), multi) == -1) { + error("Cannot create pcm multi mix thread: %s", strerror(errno)); + bluealsa_mix_buffer_release(&multi->playback_buffer); + multi->thread = config.main_thread; + return false; + } + pthread_setname_np(multi->thread, "ba-pcm-mix"); + } + else { + if (pthread_create(&multi->thread, NULL, PTHREAD_FUNC(bluealsa_pcm_snoop_thread_func), multi) == -1) { + error("Cannot create pcm multi snoop thread: %s", strerror(errno)); + multi->thread = config.main_thread; + return false; + } + pthread_setname_np(multi->thread, "ba-pcm-snoop"); + } + + return true; +} + +/** + * Initialize multi-client support. + * + * Set up the buffer parameters and enable client audio I/O. + * + * @param multi The multi-client instance to be initialized. + * @param transfer_samples The largest number of samples that will be passed + * between the transport I/O thread and the client + * thread in a single transfer. + * @return true if multi-client successfully initialized. */ +bool bluealsa_pcm_multi_init(struct bluealsa_pcm_multi *multi, size_t transfer_samples) { + + debug("Initializing multi client support"); + + multi->state = BLUEALSA_PCM_MULTI_STATE_INIT; + multi->period_frames = transfer_samples / multi->pcm->channels; + multi->period_bytes = multi->period_frames * multi->pcm->channels * BA_TRANSPORT_PCM_FORMAT_BYTES(multi->pcm->format); + + if (bluealsa_pcm_multi_is_playback(multi)) { + size_t buffer_frames = BLUEALSA_MULTI_BUFFER_PERIODS * multi->period_frames; + if (bluealsa_mix_buffer_init(&multi->playback_buffer, + multi->pcm->format, multi->pcm->channels, + buffer_frames, multi->period_frames) == -1) + return false; + multi->buffer_ready = false; + multi->delay = multi->period_frames * (BLUEALSA_MULTI_MIX_THRESHOLD + BLUEALSA_MULTI_CLIENT_THRESHOLD) * 10000 / multi->pcm->rate; + multi->active_count = 0; + } + + multi->drain = false; + multi->drop = false; + bluealsa_pcm_multi_init_clients(multi); + + if (bluealsa_pcm_multi_is_capture(multi) && multi->client_count > 0) { + if (multi->thread == config.main_thread && !bluealsa_pcm_multi_start(multi)) + return false; + } + return true; +} + +/** + * Stop the multi-client support. */ +void bluealsa_pcm_multi_reset(struct bluealsa_pcm_multi *multi) { + if (!bluealsa_pcm_multi_is_target(multi)) + bluealsa_pcm_multi_cleanup(multi); + multi->state = BLUEALSA_PCM_MULTI_STATE_INIT; +} + +/** + * Release the resources used by a multi. */ +void bluealsa_pcm_multi_free(struct bluealsa_pcm_multi *multi) { + bluealsa_pcm_multi_cleanup(multi); + g_list_free(multi->clients); + + close(multi->epoll_fd); + close(multi->event_fd); + + pthread_mutex_destroy(&multi->client_mutex); + pthread_mutex_destroy(&multi->buffer_mutex); + pthread_cond_destroy(&multi->cond); + + free(multi); +} + +/** + * Include a new client stream. + * + * Starts the multi thread if not already running. + * + * @param multi The multi to which the client is to be added. + * @param pcm_fd File descriptor for client audio i/o. + * @param control_fd File descriptor for client control commands. + * @return true if successful. + */ +bool bluealsa_pcm_multi_add_client(struct bluealsa_pcm_multi *multi, int pcm_fd, int control_fd) { + int rv; + + if (multi->client_count == BLUEALSA_MULTI_MAX_CLIENTS) + return false; + + if (bluealsa_pcm_multi_is_capture(multi) && multi->state == BLUEALSA_PCM_MULTI_STATE_FINISHED) { + /* client thread has failed - clean it up before starting new one. */ + bluealsa_pcm_multi_reset(multi); + } + + pthread_mutex_lock(&multi->pcm->mutex); + if (multi->pcm->fd == -1) + rv = multi->pcm->fd = eventfd(0, EFD_NONBLOCK); + pthread_mutex_unlock(&multi->pcm->mutex); + if (rv == -1) + return false; + + struct bluealsa_pcm_client *client = bluealsa_pcm_client_new(multi, pcm_fd, control_fd); + if (!client) + goto fail; + + + pthread_mutex_lock(&multi->client_mutex); + + /* Postpone initialization of client if multi itself is not yet + * initialized. */ + if (multi->period_bytes > 0) { + if (!bluealsa_pcm_client_init(client)) { + bluealsa_pcm_client_free(client); + pthread_mutex_unlock(&multi->client_mutex); + goto fail; + } + } + +#if DEBUG + client->id = ++multi->client_no; +#endif + + multi->clients = g_list_prepend(multi->clients, client); + multi->client_count++; + + if (bluealsa_pcm_multi_is_playback(multi)) { + if (multi->state == BLUEALSA_PCM_MULTI_STATE_FINISHED) + multi->state = BLUEALSA_PCM_MULTI_STATE_INIT; + } + else { + if (multi->state == BLUEALSA_PCM_MULTI_STATE_INIT) + multi->state = BLUEALSA_PCM_MULTI_STATE_RUNNING; + } + + pthread_mutex_unlock(&multi->client_mutex); + + if (multi->thread == config.main_thread && !bluealsa_pcm_multi_start(multi)) + goto fail; + + debug("new client id %zu, total clients now %zu", client->id, multi->client_count); + return true; + +fail: + pthread_mutex_lock(&multi->pcm->mutex); + if (multi->pcm->fd != -1) { + close(multi->pcm->fd); + multi->pcm->fd = -1; + } + pthread_mutex_unlock(&multi->pcm->mutex); + return false; +} + +/** + * Remove a client stream. + * @return false if no clients remain, true otherwise. */ +static void bluealsa_pcm_multi_remove_client(struct bluealsa_pcm_multi *multi, struct bluealsa_pcm_client *client) { + client->multi->clients = g_list_remove(multi->clients, client); + --client->multi->client_count; + debug("removed client no %zu, total clients now %zu", client->id, multi->client_count); + bluealsa_pcm_client_free(client); +} + +/** + * Write out decoded samples to the clients. + * + * Called by the transport I/O thread. + * @param multi Pointer to the multi. + * @param buffer Pointer to the buffer from which to obtain the samples. + * @param samples the number of samples available in the decoder buffer. + * @return the number of samples written. */ +ssize_t bluealsa_pcm_multi_write(struct bluealsa_pcm_multi *multi, const void *buffer, size_t samples) { + + pthread_mutex_lock(&multi->client_mutex); + + if (multi->state == BLUEALSA_PCM_MULTI_STATE_FINISHED) { + pthread_mutex_lock(&multi->pcm->mutex); + ba_transport_pcm_release(multi->pcm); + pthread_mutex_unlock(&multi->pcm->mutex); + samples = 0; + goto finish; + } + + GList *el; + for (el = multi->clients; el != NULL; el = el->next) { + struct bluealsa_pcm_client *client = el->data; + if (client->state == BLUEALSA_PCM_CLIENT_STATE_RUNNING) + bluealsa_pcm_client_write(client, buffer, samples); + + if (client->state == BLUEALSA_PCM_CLIENT_STATE_FINISHED) { + bluealsa_pcm_multi_remove_client(multi, client); + } + } + +finish: + pthread_mutex_unlock(&multi->client_mutex); + return (ssize_t) samples; +} + +/** + * Read mixed samples. + * + * multi client replacement for io_pcm_read() */ +ssize_t bluealsa_pcm_multi_read(struct bluealsa_pcm_multi *multi, void *buffer, size_t samples) { + eventfd_t value = 0; + ssize_t ret; + enum bluealsa_pcm_multi_state state; + + pthread_mutex_lock(&multi->pcm->mutex); + if (multi->pcm->fd == -1) { + pthread_mutex_unlock(&multi->pcm->mutex); + errno = EBADF; + return -1; + } + + /* Clear pcm available event */ + ret = eventfd_read(multi->pcm->fd, &value); + pthread_mutex_unlock(&multi->pcm->mutex); + if (ret < 0 && errno != EAGAIN) + return ret; + + /* Trigger client thread to re-fill the mix. */ + eventfd_write(multi->event_fd, 1); + + /* Wait for mix update to complete */ + pthread_mutex_lock(&multi->buffer_mutex); + while (((state = multi->state) == BLUEALSA_PCM_MULTI_STATE_RUNNING) && !multi->buffer_ready) + pthread_cond_wait(&multi->cond, &multi->buffer_mutex); + multi->buffer_ready = false; + pthread_mutex_unlock(&multi->buffer_mutex); + + switch (state) { + case BLUEALSA_PCM_MULTI_STATE_RUNNING: + { + double scale_array[multi->pcm->channels]; + if (multi->pcm->soft_volume) { + for (unsigned i = 0; i < multi->pcm->channels; ++i) + scale_array[i] = multi->pcm->volume[i].scale; + } + else { + for (unsigned i = 0; i < multi->pcm->channels; ++i) + if (multi->pcm->volume[i].scale == 0) + scale_array[i] = 0; + } + ret = bluealsa_mix_buffer_read(&multi->playback_buffer, buffer, samples, scale_array); + if (ret == 0) { + errno = EAGAIN; + ret = -1; + } + break; + } + case BLUEALSA_PCM_MULTI_STATE_FINISHED: + pthread_mutex_lock(&multi->pcm->mutex); + ba_transport_pcm_release(multi->pcm); + pthread_mutex_unlock(&multi->pcm->mutex); + ret = 0; + break; + case BLUEALSA_PCM_MULTI_STATE_INIT: + errno = EAGAIN; + ret = -1; + break; + default: + errno = EIO; + ret = -1; + break; + } + + return ret; +} + +/** + * Signal the transport i/o thread that mixed samples are available. */ +static void bluealsa_pcm_multi_wake_transport(struct bluealsa_pcm_multi *multi) { + pthread_mutex_lock(&multi->pcm->mutex); + eventfd_write(multi->pcm->fd, 1); + pthread_mutex_unlock(&multi->pcm->mutex); +} + +/** + * Add more samples from clients into the mix. + * Caller must hold lock on multi client_mutex */ +static void bluealsa_pcm_multi_update_mix(struct bluealsa_pcm_multi *multi) { + GList *el; + for (el = multi->clients; el != NULL; el = el->next) { + struct bluealsa_pcm_client *client = el->data; + bluealsa_pcm_client_deliver(client); + } +} + +static void bluealsa_pcm_multi_stop_if_no_clients(struct bluealsa_pcm_multi *multi) { + pthread_mutex_lock(&multi->pcm->mutex); + ba_transport_pcm_release(multi->pcm); + ba_transport_pcm_signal_send(multi->pcm, BA_TRANSPORT_PCM_SIGNAL_CLOSE); + pthread_mutex_unlock(&multi->pcm->mutex); + ba_transport_stop_if_no_clients(multi->pcm->t); +} + +/** + * The mix thread. */ +static void *bluealsa_pcm_mix_thread_func(struct bluealsa_pcm_multi *multi) { + + struct epoll_event events[BLUEALSA_MULTI_MAX_EVENTS] = { 0 }; + + struct epoll_event event = { + .events = EPOLLIN, + .data.ptr = multi, + }; + + epoll_ctl(multi->epoll_fd, EPOLL_CTL_ADD, multi->event_fd, &event); + + debug("Starting pcm mix loop"); + for (;;) { + + int event_count; + do { + event_count = epoll_wait(multi->epoll_fd, events, BLUEALSA_MULTI_MAX_EVENTS, -1); + } while (event_count == -1 && errno == EINTR); + + if (event_count <= 0) { + error("epoll_wait failed: %d (%s)", errno, strerror(errno)); + goto terminate; + } + + int n; + for (n = 0; n < event_count; n++) { + + if (events[n].data.ptr == multi) { + /* trigger from encoder thread */ + eventfd_t value = 0; + eventfd_read(multi->event_fd, &value); + if (value >= 0xDEAD0000) + goto terminate; + pthread_mutex_lock(&multi->buffer_mutex); + pthread_mutex_lock(&multi->client_mutex); + bluealsa_pcm_multi_update_mix(multi); + pthread_mutex_unlock(&multi->client_mutex); + multi->buffer_ready = true; + pthread_cond_signal(&multi->cond); + pthread_mutex_unlock(&multi->buffer_mutex); + break; + } + + else { /* client event */ + struct bluealsa_pcm_client_event *cevent = events[n].data.ptr; + struct bluealsa_pcm_client *client = cevent->client; + + bluealsa_pcm_client_handle_event(cevent); + + if (client->state == BLUEALSA_PCM_CLIENT_STATE_FINISHED) { + pthread_mutex_lock(&multi->client_mutex); + bluealsa_pcm_multi_remove_client(multi, client); + pthread_mutex_unlock(&multi->client_mutex); + + /* removing a client invalidates the event array, so + * we need to call epoll_wait() again here */ + break; + } + } + } + + if (multi->client_count == 0) { + multi->state = BLUEALSA_PCM_MULTI_STATE_FINISHED; + bluealsa_mix_buffer_clear(&multi->playback_buffer); + bluealsa_pcm_multi_stop_if_no_clients(multi); + continue; + } + + if (multi->client_count == 1) { + struct bluealsa_pcm_client* client = g_list_first(multi->clients)->data; + if (client->drop) { + bluealsa_mix_buffer_clear(&multi->playback_buffer); + ba_transport_pcm_drop(multi->pcm); + client->drop = false; + } + } + + if (multi->state == BLUEALSA_PCM_MULTI_STATE_INIT) { + if (multi->active_count > 0) { + pthread_mutex_lock(&multi->client_mutex); + bluealsa_pcm_multi_update_mix(multi); + pthread_mutex_unlock(&multi->client_mutex); + if (bluealsa_mix_buffer_at_threshold(&multi->playback_buffer)) { + multi->state = BLUEALSA_PCM_MULTI_STATE_RUNNING; + bluealsa_pcm_multi_wake_transport(multi); + } + } + } + else if (multi->state == BLUEALSA_PCM_MULTI_STATE_RUNNING) { + if (bluealsa_mix_buffer_empty(&multi->playback_buffer)) + multi->state = BLUEALSA_PCM_MULTI_STATE_INIT; + else + bluealsa_pcm_multi_wake_transport(multi); + } + } + +terminate: + multi->state = BLUEALSA_PCM_MULTI_STATE_FINISHED; + pthread_cond_signal(&multi->cond); + bluealsa_pcm_multi_wake_transport(multi); + debug("mix thread function terminated"); + return NULL; +} + + +/** + * The snoop thread. */ +static void *bluealsa_pcm_snoop_thread_func(struct bluealsa_pcm_multi *multi) { + + struct epoll_event events[BLUEALSA_MULTI_MAX_EVENTS]; + + struct epoll_event event = { + .events = EPOLLIN, + .data.ptr = multi, + }; + + epoll_ctl(multi->epoll_fd, EPOLL_CTL_ADD, multi->event_fd, &event); + + debug("Starting pcm snoop loop"); + for (;;) { + int ret; + + do { + ret = epoll_wait(multi->epoll_fd, events, BLUEALSA_MULTI_MAX_EVENTS, -1); + } while (ret == -1 && errno == EINTR); + + if (ret <= 0) { + error("epoll_wait failed: %d (%s)", errno, strerror(errno)); + goto terminate; + } + + int n; + for (n = 0; n < ret; n++) { + + if (events[n].data.ptr == multi) { + /* trigger from transport thread */ + + eventfd_t value = 0; + eventfd_read(multi->event_fd, &value); + if (value >= 0xDEAD0000) + goto terminate; + + } + + else { + /* client event */ + struct bluealsa_pcm_client_event *cevent = events[n].data.ptr; + if (events[n].events & (EPOLLHUP|EPOLLERR)) { + bluealsa_pcm_client_handle_close_event(cevent); + pthread_mutex_lock(&multi->client_mutex); + bluealsa_pcm_multi_remove_client(multi, cevent->client); + if (multi->client_count == 0) { + multi->state = BLUEALSA_PCM_MULTI_STATE_FINISHED; + bluealsa_pcm_multi_stop_if_no_clients(multi); + } + pthread_mutex_unlock(&multi->client_mutex); + + /* removing a client invalidates the event array, so + * we need to call epoll_wait() again here */ + break; + } + else { + bluealsa_pcm_client_handle_event(cevent); + if (multi->state == BLUEALSA_PCM_MULTI_STATE_PAUSED && multi->active_count > 0) { + multi->state = BLUEALSA_PCM_MULTI_STATE_RUNNING; + ba_transport_pcm_resume(multi->pcm); +; + } + } + } + } + } + +terminate: + multi->state = BLUEALSA_PCM_MULTI_STATE_FINISHED; + debug("snoop thread function terminated"); + return NULL; +} diff --git a/src/bluealsa-pcm-multi.h b/src/bluealsa-pcm-multi.h new file mode 100644 index 000000000..41c7ca708 --- /dev/null +++ b/src/bluealsa-pcm-multi.h @@ -0,0 +1,90 @@ +/* + * BlueALSA - bluealsa-pcm-multi.h + * Copyright (c) 2016-2022 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#ifndef BLUEALSA_PCM_MULTI_H +#define BLUEALSA_PCM_MULTI_H + +#include +#include +#include +#include +#include + +/* IWYU pragma: no_include "config.h" */ +#include "bluealsa-mix-buffer.h" + +/* Number of periods to hold in mix before starting playback. */ +#define BLUEALSA_MULTI_MIX_THRESHOLD 2 + +/* Number of periods to hold in client before starting mix. */ +#define BLUEALSA_MULTI_CLIENT_THRESHOLD 2 + + +struct ba_transport; +struct ba_transport_pcm; + +enum bluealsa_pcm_multi_state { + BLUEALSA_PCM_MULTI_STATE_INIT = 0, + BLUEALSA_PCM_MULTI_STATE_RUNNING, + BLUEALSA_PCM_MULTI_STATE_PAUSED, + BLUEALSA_PCM_MULTI_STATE_FINISHED, +}; + +struct bluealsa_snoop_buffer { + const uint8_t *data; + size_t len; +}; + +struct bluealsa_pcm_multi { + struct ba_transport_pcm *pcm; + union { + struct bluealsa_mix_buffer playback_buffer; + struct bluealsa_snoop_buffer capture_buffer; + }; + size_t period_bytes; + size_t period_frames; + size_t delay; + GList *clients; + size_t client_count; + size_t active_count; + _Atomic enum bluealsa_pcm_multi_state state; + int epoll_fd; + int event_fd; + pthread_t thread; + pthread_mutex_t client_mutex; + pthread_mutex_t buffer_mutex; + pthread_cond_t cond; + bool buffer_ready; + bool drain; + bool drop; +#if DEBUG + size_t client_no; +#endif +}; + +bool bluealsa_pcm_multi_enabled(const struct ba_transport *t); + +struct bluealsa_pcm_multi *bluealsa_pcm_multi_create(struct ba_transport_pcm *pcm); + +bool bluealsa_pcm_multi_init(struct bluealsa_pcm_multi *multi, size_t transfer_samples); + +void bluealsa_pcm_multi_reset(struct bluealsa_pcm_multi *multi); + +void bluealsa_pcm_multi_free(struct bluealsa_pcm_multi *multi); + +bool bluealsa_pcm_multi_add_client(struct bluealsa_pcm_multi *multi, int pcm_fd, int control_fd); + +ssize_t bluealsa_pcm_multi_read(struct bluealsa_pcm_multi *multi, void *buffer, size_t samples); + +ssize_t bluealsa_pcm_multi_write(struct bluealsa_pcm_multi *multi, const void *buffer, size_t samples); + +ssize_t bluealsa_pcm_multi_fetch(struct bluealsa_pcm_multi *multi, void *buffer, size_t samples, bool *restarted); + +#endif /* BLUEALSA_PCM_MULTI_H */ diff --git a/src/io.c b/src/io.c index 2bbcfc7c8..db0ce8806 100644 --- a/src/io.c +++ b/src/io.c @@ -22,11 +22,31 @@ #include #include "audio.h" +#include "bluealsa-pcm-multi.h" #include "ba-config.h" #include "shared/defs.h" #include "shared/ffb.h" #include "shared/log.h" +/** + * Fill a buffer with silence. */ +static void io_pcm_fill_silence(struct ba_transport_pcm *pcm, void *buffer, size_t samples) { + g_assert_cmpuint((samples % pcm->channels), ==, 0); + + switch (pcm->format) { + case BA_TRANSPORT_PCM_FORMAT_S16_2LE: + memset(buffer, 0, samples * 2); + break; + case BA_TRANSPORT_PCM_FORMAT_S24_4LE: + case BA_TRANSPORT_PCM_FORMAT_S32_4LE: + memset(buffer, 0, samples * 4); + break; + default: + g_assert_not_reached(); + } + +} + /** * Read data from the BT transport (SCO or SEQPACKET) socket. */ ssize_t io_bt_read( @@ -181,7 +201,7 @@ ssize_t io_pcm_flush(struct ba_transport_pcm *pcm) { /** * Read PCM signal from the transport PCM FIFO. */ -ssize_t io_pcm_read( +ssize_t io_pcm_single_read( struct ba_transport_pcm *pcm, void *buffer, size_t samples) { @@ -212,8 +232,23 @@ ssize_t io_pcm_read( } /** - * Write PCM signal to the transport PCM FIFO. */ -ssize_t io_pcm_write( + * Read PCM signal from the transport PCM FIFO or mix as appropriate. */ +ssize_t io_pcm_read( + struct ba_transport_pcm *pcm, + void *buffer, + size_t samples) { + if (pcm->multi) + return bluealsa_pcm_multi_read(pcm->multi, buffer, samples); + else + return io_pcm_single_read(pcm, buffer, samples); +} + +/** + * Write PCM signal to the transport PCM FIFO. + * + * Note: + * This function may temporally re-enable thread cancellation! */ +ssize_t io_pcm_single_write( struct ba_transport_pcm *pcm, const void *buffer, size_t samples) { @@ -264,6 +299,21 @@ ssize_t io_pcm_write( return ret; } +/** + * Write samples to PCM. + * + * Selects either multi or direct client FIFO depending on whether + * multi client support is enabled. */ +ssize_t io_pcm_write( + struct ba_transport_pcm *pcm, + const void *buffer, + size_t samples) { + if (pcm->multi == NULL) + return io_pcm_single_write(pcm, buffer, samples); + else + return bluealsa_pcm_multi_write(pcm->multi, buffer, samples); +} + /** * Poll and read data from the BT transport socket. * @@ -302,6 +352,15 @@ ssize_t io_poll_and_read_bt( return len; } +static void drain_complete(struct io_poll *io, struct ba_transport_pcm *pcm) { + io->drain = false; + io->timeout = -1; + pthread_mutex_lock(&pcm->mutex); + pcm->synced = true; + pthread_mutex_unlock(&pcm->mutex); + pthread_cond_signal(&pcm->cond); +} + /** * Poll and read data from the PCM FIFO. * @@ -330,15 +389,15 @@ ssize_t io_poll_and_read_pcm( /* Poll for reading with optional sync timeout. */ switch (poll_rv) { case 0: - pthread_mutex_lock(&pcm->mutex); - pcm->synced = true; - pthread_mutex_unlock(&pcm->mutex); - pthread_cond_signal(&pcm->cond); - io->timeout = -1; + if (io->drain) + break; + drain_complete(io, pcm); return 0; case -1: if (errno == EINTR) goto repoll; + if (io->drain) + drain_complete(io, pcm); return -1; } @@ -348,14 +407,18 @@ ssize_t io_poll_and_read_pcm( case BA_TRANSPORT_PCM_SIGNAL_RESUME: io->asrs.frames = 0; io->timeout = -1; + io->drain = false; goto repoll; case BA_TRANSPORT_PCM_SIGNAL_CLOSE: /* reuse PCM read disconnection logic */ break; case BA_TRANSPORT_PCM_SIGNAL_SYNC: + io->drain = true; io->timeout = 100; goto repoll; case BA_TRANSPORT_PCM_SIGNAL_DROP: + if (io->drain) + drain_complete(io, pcm); /* Notify caller that the PCM FIFO has been dropped. This will give * the caller a chance to reinitialize its internal state. */ errno = ESTALE; @@ -364,20 +427,38 @@ ssize_t io_poll_and_read_pcm( goto repoll; } - if (fds[1].revents == 0) - return 0; - ssize_t samples; if ((samples = io_pcm_read(pcm, buffer->tail, ffb_len_in(buffer))) == -1) { - if (errno == EAGAIN) - goto repoll; - if (errno != EBADF) - return -1; - samples = 0; + switch (errno) { + case EAGAIN: + if (io->drain) { + /* The FIFO is now empty, but must still ensure that any + * remaining frames in the encoder buffer are flushed + * to bt; so we pad the buffer with silence to ensure the + * encoder codesize minimum is available. */ + samples = ffb_len_in(buffer); + io_pcm_fill_silence(pcm, buffer->tail, samples); + /* Make sure that the next time this function is called we + * signal that the drain is complete */ + io->drain = false; + io->timeout = 0; + break; + } + else + goto repoll; + case EBADF: + samples = 0; + break; + default: + break; + } } - if (samples == 0) - return 0; + if (samples <= 0) { + if (io->drain) + drain_complete(io, pcm); + return samples; + } /* When the thread is created, there might be no data in the FIFO. In fact * there might be no data for a long time - until client starts playback. diff --git a/src/io.h b/src/io.h index d619e3976..10ac4bddc 100644 --- a/src/io.h +++ b/src/io.h @@ -16,6 +16,7 @@ # include #endif +#include #include #include "ba-transport-pcm.h" @@ -32,6 +33,8 @@ struct io_poll { struct asrsync asrs; /* keep-alive and sync timeout */ int timeout; + /* true when PCM FIFO is draining */ + bool drain; }; ssize_t io_bt_read( diff --git a/src/main.c b/src/main.c index 408b4ebf0..c0ee383d6 100644 --- a/src/main.c +++ b/src/main.c @@ -154,7 +154,7 @@ static void g_bus_name_lost(GDBusConnection *conn, const char *name, void *userd int main(int argc, char **argv) { int opt; - static const char *opts = "hVSB:i:p:c:"; + static const char *opts = "hVSB:Mi:p:c:"; static const struct option longopts[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, @@ -162,6 +162,7 @@ int main(int argc, char **argv) { { "loglevel", required_argument, NULL, 23 }, { "dbus", required_argument, NULL, 'B' }, { "device", required_argument, NULL, 'i' }, + { "multi-client", no_argument, NULL, 'M' }, { "profile", required_argument, NULL, 'p' }, { "codec", required_argument, NULL, 'c' }, { "all-codecs", no_argument, NULL, 25 }, @@ -233,6 +234,7 @@ int main(int argc, char **argv) { " --loglevel=LEVEL\t\tminimum message priority\n" " -B, --dbus=NAME\t\tD-Bus service name suffix\n" " -i, --device=hciX\t\tHCI device(s) to use\n" + " -M, --multi-client\t\tpermit multiple clients for each transport PCM\n" " -p, --profile=NAME\t\tset enabled BT profiles\n" " -c, --codec=NAME\t\tset enabled BT audio codecs\n" " --all-codecs\t\t\tenable all available BT audio codecs\n" @@ -352,6 +354,10 @@ int main(int argc, char **argv) { g_array_append_val(config.hci_filter, optarg); break; + case 'M' /* --multi-client */ : + config.multi_enabled = true; + break; + case 'p' /* --profile=NAME */ : { static const struct { diff --git a/src/sco-cvsd.c b/src/sco-cvsd.c index dea164c16..fe7c127ec 100644 --- a/src/sco-cvsd.c +++ b/src/sco-cvsd.c @@ -18,6 +18,7 @@ #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "io.h" #include "shared/defs.h" #include "shared/ffb.h" @@ -44,6 +45,10 @@ void *sco_cvsd_enc_thread(struct ba_transport_pcm *t_pcm) { goto fail_init; } + /* start multi client thread if required. */ + if (t_pcm->multi && !bluealsa_pcm_multi_init(t_pcm->multi, buffer.nmemb)) + goto fail_init; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { @@ -112,6 +117,10 @@ void *sco_cvsd_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_ffb; } + /* start multi client thread if required. */ + if (t_pcm->multi && !bluealsa_pcm_multi_init(t_pcm->multi, buffer.nmemb)) + goto fail_ffb; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { diff --git a/src/sco-lc3-swb.c b/src/sco-lc3-swb.c index b9efb590f..5be7f3a7f 100644 --- a/src/sco-lc3-swb.c +++ b/src/sco-lc3-swb.c @@ -22,6 +22,7 @@ #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "codec-lc3-swb.h" #include "io.h" #include "shared/defs.h" @@ -45,6 +46,10 @@ void *sco_lc3_swb_enc_thread(struct ba_transport_pcm *t_pcm) { const ssize_t lc3_swb_delay_frames = lc3_swb_get_delay(&codec); t_pcm->codec_delay_dms = lc3_swb_delay_frames * 10000 / t_pcm->rate; + /* start multi client thread if required. */ + if (t_pcm->multi && !bluealsa_pcm_multi_init(t_pcm->multi, codec.pcm.nmemb)) + goto exit; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { @@ -113,6 +118,10 @@ void *sco_lc3_swb_dec_thread(struct ba_transport_pcm *t_pcm) { struct esco_lc3_swb codec; lc3_swb_init(&codec); + /* start multi client thread if required. */ + if (t_pcm->multi && !bluealsa_pcm_multi_init(t_pcm->multi, codec.pcm.nmemb)) + goto exit; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { diff --git a/src/sco-msbc.c b/src/sco-msbc.c index 3c9ceaae7..357a43f1f 100644 --- a/src/sco-msbc.c +++ b/src/sco-msbc.c @@ -19,6 +19,7 @@ #include "ba-transport.h" #include "ba-transport-pcm.h" +#include "bluealsa-pcm-multi.h" #include "codec-msbc.h" #include "io.h" #include "shared/defs.h" @@ -47,6 +48,10 @@ void *sco_msbc_enc_thread(struct ba_transport_pcm *t_pcm) { /* Get the total delay introduced by the codec. */ t_pcm->codec_delay_dms = sbc_delay_frames * 10000 / t_pcm->rate; + /* start multi client thread if required. */ + if (t_pcm->multi && !bluealsa_pcm_multi_init(t_pcm->multi, msbc.pcm.nmemb)) + goto fail_msbc; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { @@ -127,6 +132,10 @@ void *sco_msbc_dec_thread(struct ba_transport_pcm *t_pcm) { goto fail_msbc; } + /* start multi client thread if required. */ + if (t_pcm->multi && !bluealsa_pcm_multi_init(t_pcm->multi, msbc.pcm.nmemb)) + goto fail_msbc; + debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { diff --git a/test/Makefile.am b/test/Makefile.am index 519774e31..46baf8cd3 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -125,6 +125,9 @@ test_ba_SOURCES = \ ../src/ba-transport.c \ ../src/ba-transport-pcm.c \ ../src/codec-sbc.c \ + ../src/bluealsa-mix-buffer.c \ + ../src/bluealsa-pcm-client.c \ + ../src/bluealsa-pcm-multi.c \ ../src/dbus.c \ ../src/h2.c \ ../src/hci.c \ @@ -157,6 +160,9 @@ test_io_SOURCES = \ ../src/ba-config.c \ ../src/ba-device.c \ ../src/ba-transport-pcm.c \ + ../src/bluealsa-mix-buffer.c \ + ../src/bluealsa-pcm-client.c \ + ../src/bluealsa-pcm-multi.c \ ../src/codec-sbc.c \ ../src/dbus.c \ ../src/h2.c \ @@ -209,6 +215,9 @@ test_rfcomm_SOURCES = \ ../src/ba-rfcomm.c \ ../src/ba-transport.c \ ../src/ba-transport-pcm.c \ + ../src/bluealsa-mix-buffer.c \ + ../src/bluealsa-pcm-client.c \ + ../src/bluealsa-pcm-multi.c \ ../src/dbus.c \ ../src/h2.c \ ../src/hci.c \ diff --git a/test/mock/Makefile.am b/test/mock/Makefile.am index fdb38075f..8593c6df7 100644 --- a/test/mock/Makefile.am +++ b/test/mock/Makefile.am @@ -28,6 +28,9 @@ bluealsad_mock_SOURCES = \ ../../src/ba-transport-pcm.c \ ../../src/bluealsa-dbus.c \ ../../src/bluealsa-iface.c \ + ../../src/bluealsa-mix-buffer.c \ + ../../src/bluealsa-pcm-client.c \ + ../../src/bluealsa-pcm-multi.c \ ../../src/bluez.c \ ../../src/bluez-iface.c \ ../../src/codec-sbc.c \ diff --git a/test/test-a2dp.c b/test/test-a2dp.c index c69f9828e..aae804908 100644 --- a/test/test-a2dp.c +++ b/test/test-a2dp.c @@ -17,7 +17,7 @@ #include #include #include - +#include #include #include @@ -52,6 +52,10 @@ enum ba_transport_pcm_signal ba_transport_pcm_signal_recv(struct ba_transport_pc (void)pcm; return -1; } void ba_transport_pcm_thread_cleanup(struct ba_transport_pcm *pcm) { (void)pcm; } +bool bluealsa_pcm_multi_init(struct bluealsa_pcm_multi *multi, size_t transfer_samples) { (void) multi; (void) transfer_samples; return true; } +ssize_t bluealsa_pcm_multi_read(struct bluealsa_pcm_multi *multi, void *buffer, size_t samples) { (void) multi; (void) buffer; (void) samples; return -1; } +ssize_t bluealsa_pcm_multi_write(struct bluealsa_pcm_multi *multi, const void *buffer, size_t samples) { (void) multi; (void) buffer; (void) samples; return -1; } + CK_START_TEST(test_a2dp_codecs_codec_id_from_string) { ck_assert_uint_eq(a2dp_codecs_codec_id_from_string("SBC"), A2DP_CODEC_SBC); ck_assert_uint_eq(a2dp_codecs_codec_id_from_string("apt-x"),