Skip to content

Commit 4e7ac92

Browse files
committed
aplay: implement adaptive resampler
1 parent 9a2c8de commit 4e7ac92

13 files changed

+763
-95
lines changed

.github/spellcheck-wordlist.txt

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ SIGIO
256256
SIGPIPE
257257
SIGSEGV
258258
SIGTERM
259+
SINC
259260
SL
260261
SNR
261262
spandsp

configure.ac

+8
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,14 @@ AC_ARG_ENABLE([aplay],
319319
[AS_HELP_STRING([--disable-aplay], [disable building of bluealsa-aplay tool])])
320320
AM_CONDITIONAL([ENABLE_APLAY], [test "x$enable_aplay" != "xno"])
321321

322+
AC_ARG_ENABLE([aplay-resampler],
323+
AS_HELP_STRING([--enable-aplay-resampler], [enable resampler in bluealsa-aplay]))
324+
AM_CONDITIONAL([ENABLE_APLAY_RESAMPLER], [test "x$enable_aplay_resampler" = "xyes"])
325+
AM_COND_IF([ENABLE_APLAY_RESAMPLER], [
326+
PKG_CHECK_MODULES([SAMPLERATE], [samplerate >= 0.2.2])
327+
AC_DEFINE([ENABLE_APLAY_RESAMPLER], [1], [Define to 1 if aplay resampler is enabled.])
328+
])
329+
322330
AC_ARG_ENABLE([rfcomm],
323331
[AS_HELP_STRING([--enable-rfcomm], [enable building of bluealsa-rfcomm tool])])
324332
AM_CONDITIONAL([ENABLE_RFCOMM], [test "x$enable_rfcomm" = "xyes"])

doc/bluealsa-aplay.1.rst

+42-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ bluealsa-aplay
66
a simple BlueALSA player
77
------------------------
88

9-
:Date: August 2024
9+
:Date: January 2025
1010
:Manual section: 1
1111
:Manual group: General Commands Manual
1212
:Version: $VERSION$
@@ -76,10 +76,12 @@ OPTIONS
7676
Select ALSA playback PCM device to use for audio output.
7777
The default is ``default``.
7878

79-
Internally, **bluealsa-aplay** does not perform any audio transformations
79+
Internally, **bluealsa-aplay** is able to perform sample rate conversion
80+
if it was built with libsamplerate support (see the option
81+
**--resampler=**), but does not perform any other audio transformations
8082
nor streams mixing. If multiple Bluetooth devices are connected it simply
8183
opens a new connection to the ALSA PCM device for each stream. Selected
82-
hardware parameters like sample rate and number of channels are
84+
hardware parameters like sample format and number of channels are
8385
taken from the audio profile of a particular Bluetooth connection. Note,
8486
that each connection can have a different setup.
8587

@@ -187,6 +189,38 @@ OPTIONS
187189
Note: Only one of A2DP or SCO can be used. If both are specified, the
188190
last one given will be selected.
189191

192+
--resampler=METHOD
193+
Use libsamplerate to convert the stream from the Bluetooth sample rate to
194+
the ALSA PCM sample rate. This option is only available if
195+
**bluealsa-aplay** was built with libsamplerate support. The resampler uses
196+
adaptive resampling to compensate for timer drift between the Bluetooth
197+
timer and the ALSA device timer. Resampling can be CPU intensive and
198+
therefore by default this option is not enabled. *METHOD* specifies which
199+
libsamplerate converter to use, and may be one of 6 values:
200+
201+
- **best** - use the SRC_SINC_BEST_QUALITY converter; generates the highest
202+
quality output but also has very high CPU usage.
203+
204+
- **medium** - use the SRC_SINC_MEDIUM_QUALITY converter; generates high
205+
quality output and has moderately high CPU usage.
206+
207+
- **fastest** - use the SRC_SINC_FASTEST converter; generates good quality
208+
output with lower CPU usage than the other SINC based converters. Often
209+
this converter is the best compromise for Bluetooth audio.
210+
211+
- **zero-hold** - use the SRC_ZERO_ORDER_HOLD converter; the audio quality
212+
is relatively poor compared to the SINC converters, but CPU usage is very
213+
low so may be better suited to very low power embedded processors.
214+
215+
- **linear** - use the SRC_LINEAR converter; the lowest quality converter
216+
of libsamplerate. Output quality is similar to the ALSA rate plugin's own
217+
internal linear converter, but this option also performs adaptive
218+
resampling which is not possible with the ALSA rate plugin.
219+
220+
- **none** - do not perform any resampling; the ALSA PCM device is then
221+
responsible for rate conversion, and no timer drift adjustment is made.
222+
This is the default when no resampler is specified.
223+
190224
--single-audio
191225
Allow only one Bluetooth device to play audio at a time.
192226
If multiple devices are connected, only the first to start will play, the
@@ -236,10 +270,10 @@ The ALSA **dmix** plugin will ignore the period and buffer times selected by
236270
the application (because it has to allow connections from multiple
237271
applications). Instead it will choose its own values, which can lead to
238272
rounding errors in the period size calculation when used with the ALSA **rate**
239-
plugin. To avoid this, it is recommended to explicitly define the hardware
240-
period size and buffer size for **dmix** in your ALSA configuration. For
241-
example, suppose we want a period time of 50000 µs and a buffer holding 4
242-
periods with an Intel 'PCH' card:
273+
plugin (but not when using the *--resampler=* option). To avoid this, it is
274+
recommended to explicitly define the hardware period size and buffer size for
275+
**dmix** in your ALSA configuration. For example, suppose we want a period time
276+
of 50000 µs and a buffer holding 4 periods with an Intel 'PCH' card:
243277

244278
::
245279

@@ -308,7 +342,7 @@ element will be used as a hardware volume control knob.
308342
COPYRIGHT
309343
=========
310344

311-
Copyright (c) 2016-2024 Arkadiusz Bokowy.
345+
Copyright (c) 2016-2025 Arkadiusz Bokowy.
312346

313347
The bluez-alsa project is licensed under the terms of the MIT license.
314348

misc/bash-completion/bluealsad

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ _bluealsa_aplay() {
265265
__ltrim_colon_completions "$cur"
266266
return
267267
;;
268-
--loglevel|--volume)
268+
--loglevel|--resampler|--volume)
269269
readarray -t list < <(_bluealsa_enum_values "$1" "$prev")
270270
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
271271
return

test/test-utils-aplay.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ CK_START_TEST(test_play_dbus_signals) {
276276
NULL), -1);
277277
spawn_terminate(&sp_ba_aplay, 1500);
278278

279-
char output[8192] = "";
279+
char output[16384] = "";
280280
ck_assert_int_gt(spawn_read(&sp_ba_aplay, NULL, 0, output, sizeof(output)), 0);
281281

282282
#if ENABLE_HFP_CODEC_SELECTION && DEBUG

utils/aplay/Makefile.am

+9-2
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,24 @@ bluealsa_aplay_SOURCES = \
1919
delay-report.c \
2020
aplay.c
2121

22+
if ENABLE_APLAY_RESAMPLER
23+
bluealsa_aplay_SOURCES += \
24+
resampler.c
25+
endif
26+
2227
bluealsa_aplay_CFLAGS = \
2328
-I$(top_srcdir)/src \
2429
@ALSA_CFLAGS@ \
2530
@BLUEZ_CFLAGS@ \
2631
@DBUS1_CFLAGS@ \
27-
@LIBUNWIND_CFLAGS@
32+
@LIBUNWIND_CFLAGS@ \
33+
@SAMPLERATE_CFLAGS@
2834

2935
bluealsa_aplay_LDADD = \
3036
@ALSA_LIBS@ \
3137
@BLUEZ_LIBS@ \
3238
@DBUS1_LIBS@ \
33-
@LIBUNWIND_LIBS@
39+
@LIBUNWIND_LIBS@ \
40+
@SAMPLERATE_LIBS@
3441

3542
endif

utils/aplay/alsa-pcm.c

+69-43
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@
1010

1111
#include "alsa-pcm.h"
1212

13+
#include <stdbool.h>
1314
#include <stdio.h>
1415
#include <stdlib.h>
1516
#include <string.h>
1617

1718
#include "shared/log.h"
1819

19-
static int alsa_pcm_set_hw_params(snd_pcm_t *pcm, snd_pcm_format_t format, int channels,
20-
int rate, unsigned int *buffer_time, unsigned int *period_time, char **msg) {
20+
static int alsa_pcm_set_hw_params(snd_pcm_t *pcm, snd_pcm_format_t format_1,
21+
snd_pcm_format_t format_2, snd_pcm_format_t *selected_format,
22+
int channels, unsigned int *rate, bool exact_rate,
23+
unsigned int *buffer_time, unsigned int *period_time, char **msg) {
2124

2225
const snd_pcm_access_t access = SND_PCM_ACCESS_RW_INTERLEAVED;
26+
snd_pcm_format_t format = SND_PCM_FORMAT_UNKNOWN;
2327
snd_pcm_hw_params_t *params;
2428
char buf[256];
2529
int dir;
@@ -37,18 +41,36 @@ static int alsa_pcm_set_hw_params(snd_pcm_t *pcm, snd_pcm_format_t format, int c
3741
goto fail;
3842
}
3943

40-
if ((err = snd_pcm_hw_params_set_format(pcm, params, format)) != 0) {
41-
snprintf(buf, sizeof(buf), "Set format: %s: %s", snd_strerror(err), snd_pcm_format_name(format));
44+
/* Prefer format_1 if supported by the device. */
45+
if ((err = snd_pcm_hw_params_set_format(pcm, params, format_1)) == 0)
46+
format = format_1;
47+
/* Otherwise try format 2 */
48+
else if (format_2 != SND_PCM_FORMAT_UNKNOWN) {
49+
if ((err = snd_pcm_hw_params_set_format(pcm, params, format_2)) == 0)
50+
format = format_2;
51+
else {
52+
snprintf(buf, sizeof(buf), "Set format: %s: %s and %s", snd_strerror(err), snd_pcm_format_name(format_1), snd_pcm_format_name(format_2));
53+
goto fail;
54+
}
55+
}
56+
else {
57+
snprintf(buf, sizeof(buf), "Set format: %s: %s", snd_strerror(err), snd_pcm_format_name(format_1));
4258
goto fail;
4359
}
60+
*selected_format = format;
4461

4562
if ((err = snd_pcm_hw_params_set_channels(pcm, params, channels)) != 0) {
4663
snprintf(buf, sizeof(buf), "Set channels: %s: %d", snd_strerror(err), channels);
4764
goto fail;
4865
}
4966

50-
if ((err = snd_pcm_hw_params_set_rate(pcm, params, rate, 0)) != 0) {
51-
snprintf(buf, sizeof(buf), "Set sample rate: %s: %d", snd_strerror(err), rate);
67+
dir = 0;
68+
if (exact_rate)
69+
err = snd_pcm_hw_params_set_rate(pcm, params, *rate, dir);
70+
else
71+
err = snd_pcm_hw_params_set_rate_near(pcm, params, rate, &dir);
72+
if (err != 0) {
73+
snprintf(buf, sizeof(buf), "Set sample rate: %s: %u", snd_strerror(err), *rate);
5274
goto fail;
5375
}
5476

@@ -72,43 +94,36 @@ static int alsa_pcm_set_hw_params(snd_pcm_t *pcm, snd_pcm_format_t format, int c
7294
return 0;
7395

7496
fail:
97+
*selected_format = format;
7598
if (msg != NULL)
7699
*msg = strdup(buf);
77100
return err;
78101
}
79102

80-
static int alsa_pcm_set_sw_params(struct alsa_pcm *pcm, snd_pcm_uframes_t buffer_size,
81-
snd_pcm_uframes_t period_size, char **msg) {
103+
static int alsa_pcm_set_sw_params(snd_pcm_t *pcm,
104+
snd_pcm_uframes_t start_threshold, char **msg) {
82105

83-
snd_pcm_t *snd_pcm = pcm->handle;
84106
snd_pcm_sw_params_t *params;
85107
char buf[256];
86108
int err;
87109

88110
snd_pcm_sw_params_alloca(&params);
89111

90-
if ((err = snd_pcm_sw_params_current(snd_pcm, params)) != 0) {
91-
snprintf(buf, sizeof(buf), "Get current params: %s", snd_strerror(err));
112+
if ((err = snd_pcm_sw_params_current(pcm, params)) != 0) {
113+
snprintf(buf, sizeof(buf), "Get current sw params: %s", snd_strerror(err));
92114
goto fail;
93115
}
94116

95-
/* Start the transfer when three periods have been written (or when the
96-
* buffer is full if it holds less than three periods. */
97-
snd_pcm_uframes_t threshold = period_size * 3;
98-
if (threshold > buffer_size)
99-
threshold = buffer_size;
100-
if ((err = snd_pcm_sw_params_set_start_threshold(snd_pcm, params, threshold)) != 0) {
101-
snprintf(buf, sizeof(buf), "Set start threshold: %s: %lu", snd_strerror(err), threshold);
117+
if ((err = snd_pcm_sw_params_set_start_threshold(pcm, params, start_threshold)) != 0) {
118+
snprintf(buf, sizeof(buf), "Set start threshold: %s: %lu", snd_strerror(err), start_threshold);
102119
goto fail;
103120
}
104121

105-
if ((err = snd_pcm_sw_params(snd_pcm, params)) != 0) {
106-
snprintf(buf, sizeof(buf), "%s", snd_strerror(err));
122+
if ((err = snd_pcm_sw_params(pcm, params)) != 0) {
123+
snprintf(buf, sizeof(buf), "Set sw params: %s", snd_strerror(err));
107124
goto fail;
108125
}
109126

110-
pcm->start_threshold = threshold;
111-
112127
return 0;
113128

114129
fail:
@@ -123,18 +138,19 @@ void alsa_pcm_init(struct alsa_pcm *pcm) {
123138
}
124139

125140
int alsa_pcm_open(
126-
struct alsa_pcm *pcm,
127-
const char *name,
128-
snd_pcm_format_t format,
129-
int channels,
130-
unsigned int rate,
131-
unsigned int buffer_time,
132-
unsigned int period_time,
133-
int flags,
134-
char **msg) {
141+
struct alsa_pcm *pcm, const char *name,
142+
snd_pcm_format_t format_1, snd_pcm_format_t format_2,
143+
int channels, unsigned int rate,
144+
unsigned int buffer_time, unsigned int period_time,
145+
int flags, char **msg) {
135146

136147
char *tmp = NULL;
137148
char buf[256];
149+
unsigned int actual_buffer_time = buffer_time;
150+
unsigned int actual_period_time = period_time;
151+
snd_pcm_format_t actual_format = SND_PCM_FORMAT_UNKNOWN;
152+
unsigned int actual_rate = rate;
153+
const bool exact_rate = !(flags & SND_PCM_NO_AUTO_RESAMPLE);
138154
int err;
139155

140156
assert (pcm->handle == NULL);
@@ -144,21 +160,28 @@ int alsa_pcm_open(
144160
goto fail;
145161
}
146162

147-
unsigned int actual_buffer_time = buffer_time;
148-
unsigned int actual_period_time = period_time;
149-
if ((err = alsa_pcm_set_hw_params(pcm->handle, format, channels, rate,
163+
pcm->format = actual_format;
164+
if ((err = alsa_pcm_set_hw_params(pcm->handle, format_1, format_2,
165+
&actual_format, channels, &actual_rate, exact_rate,
150166
&actual_buffer_time, &actual_period_time, &tmp)) != 0) {
151167
snprintf(buf, sizeof(buf), "Set HW params: %s", tmp);
152168
goto fail;
153169
}
170+
pcm->format = actual_format;
154171

155172
snd_pcm_uframes_t buffer_size, period_size;
156173
if ((err = snd_pcm_get_params(pcm->handle, &buffer_size, &period_size)) != 0) {
157174
snprintf(buf, sizeof(buf), "Get params: %s", snd_strerror(err));
158175
goto fail;
159176
}
160177

161-
if ((err = alsa_pcm_set_sw_params(pcm, buffer_size, period_size, &tmp)) != 0) {
178+
/* Start the transfer when three requested periods have been written (or
179+
* when the buffer is full if it holds less than three requested periods. */
180+
snd_pcm_uframes_t start_threshold = (period_time * 3 / 1000) * (rate / 1000);
181+
if (start_threshold > buffer_size)
182+
start_threshold = buffer_size;
183+
184+
if ((err = alsa_pcm_set_sw_params(pcm->handle, start_threshold, &tmp)) != 0) {
162185
snprintf(buf, sizeof(buf), "Set SW params: %s", tmp);
163186
goto fail;
164187
}
@@ -168,15 +191,15 @@ int alsa_pcm_open(
168191
goto fail;
169192
}
170193

171-
pcm->format = format;
172194
pcm->channels = channels;
173-
pcm->sample_size = snd_pcm_format_size(format, 1);
174-
pcm->frame_size = snd_pcm_format_size(format, channels);
175-
pcm->rate = rate;
195+
pcm->sample_size = snd_pcm_format_size(actual_format, 1);
196+
pcm->frame_size = snd_pcm_format_size(actual_format, channels);
197+
pcm->rate = actual_rate;
176198
pcm->buffer_time = actual_buffer_time;
177199
pcm->period_time = actual_period_time;
178200
pcm->buffer_frames = buffer_size;
179201
pcm->period_frames = period_size;
202+
pcm->start_threshold = start_threshold;
180203
pcm->delay = 0;
181204

182205
/* Maintain buffer fill level above 1 period plus 2ms to allow for
@@ -205,7 +228,7 @@ int alsa_pcm_write(struct alsa_pcm *pcm, ffb_t *buffer, bool drain, bool verbose
205228
pcm->underrun = false;
206229
if ((ret = snd_pcm_avail_delay(pcm->handle, &avail, &delay)) < 0) {
207230
if (ret == -EPIPE) {
208-
debug("ALSA playback PCM underrun");
231+
warn("ALSA playback PCM underrun");
209232
pcm->underrun = true;
210233
snd_pcm_prepare(pcm->handle);
211234
avail = pcm->buffer_frames;
@@ -221,7 +244,7 @@ int alsa_pcm_write(struct alsa_pcm *pcm, ffb_t *buffer, bool drain, bool verbose
221244
snd_pcm_sframes_t written_frames = 0;
222245

223246
/* If not draining, write only as many frames as possible without blocking.
224-
* If necessary insert silemce frames to prevent underrun. */
247+
* If necessary insert silence frames to prevent underrun. */
225248
if (!drain) {
226249
if (frames > avail)
227250
frames = avail;
@@ -234,7 +257,10 @@ int alsa_pcm_write(struct alsa_pcm *pcm, ffb_t *buffer, bool drain, bool verbose
234257
info("Underrun imminent: inserting %zu silence frames", padding / pcm->channels);
235258
snd_pcm_format_set_silence(pcm->format, buffer->tail, padding);
236259
ffb_seek(buffer, padding);
237-
frames += padding;
260+
frames = ffb_len_out(buffer) / pcm->channels;
261+
/* Flag an underrun to indicate that we have caused a discontinuity
262+
* in the input stream. */
263+
pcm->underrun = true;
238264
}
239265
}
240266

@@ -245,7 +271,7 @@ int alsa_pcm_write(struct alsa_pcm *pcm, ffb_t *buffer, bool drain, bool verbose
245271
case EINTR:
246272
continue;
247273
case EPIPE:
248-
debug("ALSA playback PCM underrun");
274+
warn("ALSA playback PCM underrun");
249275
pcm->underrun = true;
250276
snd_pcm_prepare(pcm->handle);
251277
continue;

0 commit comments

Comments
 (0)