diff --git a/src/Makefile.am b/src/Makefile.am index a621a3008..163976e5d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -261,7 +261,8 @@ TESTS_norun = \ rtstutter \ sig2str-test \ stripnul \ - echo-cancel-test + echo-cancel-test \ + lo-latency-test # These tests need a running pulseaudio daemon TESTS_daemon = \ @@ -574,6 +575,11 @@ echo_cancel_test_CXXFLAGS = $(module_echo_cancel_la_CXXFLAGS) -DECHO_CANCEL_TEST endif echo_cancel_test_LDFLAGS = $(AM_LDFLAGS) $(BINLDFLAGS) +lo_latency_test_SOURCES = tests/lo-latency-test.c +lo_latency_test_LDADD = $(AM_LDADD) libpulse.la +lo_latency_test_CFLAGS = $(AM_CFLAGS) $(LIBCHECK_CFLAGS) +lo_latency_test_LDFLAGS = $(AM_LDFLAGS) $(BINLDFLAGS) $(LIBCHECK_LIBS) + ################################### # Common library # ################################### diff --git a/src/tests/lo-latency-test.c b/src/tests/lo-latency-test.c new file mode 100644 index 000000000..2bb6678d4 --- /dev/null +++ b/src/tests/lo-latency-test.c @@ -0,0 +1,446 @@ +/*** + This file is part of PulseAudio. + + Copyright 2013 Collabora Ltd. + Author: Arun Raghavan + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, + or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + USA. +***/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#define SAMPLE_HZ 44100 +#define CHANNELS 2 +#define N_OUT (SAMPLE_HZ * 1) + +#define TONE_HZ (SAMPLE_HZ / 100) +#define PLAYBACK_LATENCY 25 /* ms */ +#define CAPTURE_LATENCY 5 /* ms */ + +static pa_context *context = NULL; +static pa_stream *pstream, *rstream; +static pa_mainloop_api *mainloop_api = NULL; +static const char *context_name = NULL; + +static float out[N_OUT][CHANNELS]; +static int ppos = 0; + +static int n_underflow = 0; +static int n_overflow = 0; + +static struct timeval tv_out, tv_in; + +static const pa_sample_spec sample_spec = { + .format = PA_SAMPLE_FLOAT32, + .rate = SAMPLE_HZ, + .channels = CHANNELS, +}; +static int ss, fs; + +static void nop_free_cb(void *p) {} + +static void underflow_cb(struct pa_stream *s, void *userdata) { + fprintf(stderr, "Underflow\n"); + n_underflow++; +} + +static void overflow_cb(struct pa_stream *s, void *userdata) { + fprintf(stderr, "Overlow\n"); + n_overflow++; +} + +static void write_cb(pa_stream *s, size_t nbytes, void *userdata) { + int r, nsamp = nbytes / fs; + + if (ppos + nsamp > N_OUT) { + r = pa_stream_write(s, &out[ppos][0], (N_OUT - ppos) * fs, nop_free_cb, 0, PA_SEEK_RELATIVE); + nbytes -= (N_OUT - ppos) * fs; + ppos = 0; + } + + if (ppos == 0) + pa_gettimeofday(&tv_out); + + r = pa_stream_write(s, &out[ppos][0], nbytes, nop_free_cb, 0, PA_SEEK_RELATIVE); + fail_unless(r == 0); + + ppos = (ppos + nbytes / fs) % N_OUT; +} + +static inline float rms(const float *s, int n) { + float sq = 0; + int i; + + for (i = 0; i < n; i++) + sq += s[i] * s[i]; + + return sqrtf(sq / n); +} + +#define WINDOW (2 * CHANNELS) + +static void read_cb(pa_stream *s, size_t nbytes, void *userdata) { + static float last = 0.0f; + const float *in; + float cur; + int r; + unsigned int i = 0; + size_t l; + + r = pa_stream_peek(s, (const void **)&in, &l); + fail_unless(r == 0); + + if (l == 0) + return; + +#if 0 + { + static int fd = -1; + + if (fd == -1) { + fd = open("loopback.raw", O_CREAT | O_TRUNC | O_RDWR, S_IRUSR | S_IWUSR); + fail_if(fd < 0); + } + + r = write(fd, in, l); + } +#endif + + do { +#if 0 + { + int j; + fprintf(stderr, "%g (", rms(in, WINDOW)); + for (j = 0; j < WINDOW; j++) + fprintf(stderr, "%g ", in[j]); + fprintf(stderr, ")\n"); + } +#endif + if (i + (ss * WINDOW) < l) + cur = rms(in, WINDOW); + else + cur = rms(in, (l - i)/ss); + + /* We leave the definition of 0 generous since the window might + * straddle the 0->1 transition, raising the average power. We keep the + * definition of 1 tight in this case and detect the transition in the + * next round. */ + if (last < 0.5f && cur > 0.8f) { + pa_gettimeofday(&tv_in); + fprintf(stderr, "Latency %llu\n", (unsigned long long) pa_timeval_diff(&tv_in, &tv_out)); + } + + last = cur; + in += WINDOW; + i += ss * WINDOW; + } while (i + (ss * WINDOW) <= l); + + pa_stream_drop(s); +} + +/* + * We run a simple volume calibration so that we know we can detect the signal + * being played back. We start with the playback stream at 100% volume, and + * capture at 0. + * + * First, we then play a sine wave and increase the capture volume till the + * signal is clearly received. + * + * Next, we play back silence and make sure that the level is low enough to + * distinguish from when playback is happening. + * + * Finally, we hand off to the real read/write callbacks to run the actual + * test. + */ + +enum { + CALIBRATION_ONE, + CALIBRATION_ZERO, + CALIBRATION_DONE, +}; + +static int cal_state = CALIBRATION_ONE; + +static void calibrate_write_cb(pa_stream *s, size_t nbytes, void *userdata) { + int i, r, nsamp = nbytes / fs; + float tmp[nsamp][2]; + static int count = 0; + + /* Write out a sine tone */ + for (i = 0; i < nsamp; i++) + tmp[i][0] = tmp[i][1] = cal_state == CALIBRATION_ONE ? sinf(count++ * TONE_HZ * 2 * M_PI / SAMPLE_HZ) : 0.0f; + + r = pa_stream_write(s, &tmp, nbytes, nop_free_cb, 0, PA_SEEK_RELATIVE); + fail_unless(r == 0); + + if (cal_state == CALIBRATION_DONE) + pa_stream_set_write_callback(s, write_cb, NULL); +} + +static void calibrate_read_cb(pa_stream *s, size_t nbytes, void *userdata) { + static double v = 0; + static int skip = 0, confirm; + + pa_cvolume vol; + pa_operation *o; + int r, nsamp; + float *in; + size_t l; + + r = pa_stream_peek(s, (const void **)&in, &l); + fail_unless(r == 0); + + nsamp = l / fs; + + /* For each state or volume step change, throw out a few samples so we know + * we're seeing the changed samples. */ + if (skip++ < 100) + goto out; + else + skip = 0; + + switch (cal_state) { + case CALIBRATION_ONE: + /* Try to detect the sine wave */ + if (rms(in, nsamp) < 0.8f) { + confirm = 0; + v += 0.02f; + + if (v > 1.0) { + fprintf(stderr, "Capture signal too weak at 100%% volume (%g). Giving up.\n", rms(in, nsamp)); + fail(); + } + + pa_cvolume_set(&vol, CHANNELS, v * PA_VOLUME_NORM); + o = pa_context_set_source_output_volume(context, pa_stream_get_index(s), &vol, NULL, NULL); + fail_if(o == NULL); + pa_operation_unref(o); + } else { + /* Make sure the signal strength is steadily above our threshold */ + if (++confirm > 5) { +#if 0 + fprintf(stderr, "Capture volume = %g (%g)\n", v, rms(in, nsamp)); +#endif + cal_state = CALIBRATION_ZERO; + } + } + + break; + + case CALIBRATION_ZERO: + /* Now make sure silence doesn't trigger a false positive because + * of noise. */ + if (rms(in, nsamp) > 0.1f) { + fprintf(stderr, "Too much noise on capture (%g). Giving up.\n", rms(in, nsamp)); + fail(); + } + + cal_state = CALIBRATION_DONE; + pa_stream_set_read_callback(s, read_cb, NULL); + + break; + + default: + break; + } + +out: + pa_stream_drop(s); +} + +/* This routine is called whenever the stream state changes */ +static void stream_state_callback(pa_stream *s, void *userdata) { + switch (pa_stream_get_state(s)) { + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + case PA_STREAM_TERMINATED: + break; + + case PA_STREAM_READY: { + pa_cvolume vol; + pa_operation *o; + + /* Set volumes for calibration */ + if (!userdata) { + pa_cvolume_set(&vol, CHANNELS, PA_VOLUME_NORM); + o = pa_context_set_sink_input_volume(context, pa_stream_get_index(s), &vol, NULL, NULL); + } else { + pa_cvolume_set(&vol, CHANNELS, pa_sw_volume_from_linear(0.0)); + o = pa_context_set_source_output_volume(context, pa_stream_get_index(s), &vol, NULL, NULL); + } + + if (!o) { + fprintf(stderr, "Could not set stream volume: %s\n", pa_strerror(pa_context_errno(context))); + fail(); + } else + pa_operation_unref(o); + + break; + } + + case PA_STREAM_FAILED: + default: + fprintf(stderr, "Stream error: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + fail(); + } +} + +/* This is called whenever the context status changes */ +static void context_state_callback(pa_context *c, void *userdata) { + fail_unless(c != NULL); + + switch (pa_context_get_state(c)) { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: { + pa_buffer_attr buffer_attr; + + /* Create playback stream */ + buffer_attr.maxlength = -1; + buffer_attr.tlength = SAMPLE_HZ * fs * PLAYBACK_LATENCY / 1000; + buffer_attr.prebuf = 0; /* Setting prebuf to 0 guarantees us the stream will run synchronously, no matter what */ + buffer_attr.minreq = -1; + buffer_attr.fragsize = -1; + + pstream = pa_stream_new(c, "loopback: play", &sample_spec, NULL); + fail_unless(pstream != NULL); + pa_stream_set_state_callback(pstream, stream_state_callback, (void *) 0); + pa_stream_set_write_callback(pstream, calibrate_write_cb, NULL); + pa_stream_set_underflow_callback(pstream, underflow_cb, userdata); + + pa_stream_connect_playback(pstream, getenv("TEST_SINK"), &buffer_attr, + PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL); + + /* Create capture stream */ + buffer_attr.maxlength = -1; + buffer_attr.tlength = (uint32_t) -1; + buffer_attr.prebuf = 0; + buffer_attr.minreq = (uint32_t) -1; + buffer_attr.fragsize = SAMPLE_HZ * fs * CAPTURE_LATENCY / 1000; + + rstream = pa_stream_new(c, "loopback: rec", &sample_spec, NULL); + fail_unless(rstream != NULL); + pa_stream_set_state_callback(rstream, stream_state_callback, (void *) 1); + pa_stream_set_read_callback(rstream, calibrate_read_cb, NULL); + pa_stream_set_overflow_callback(rstream, overflow_cb, userdata); + + pa_stream_connect_record(rstream, getenv("TEST_SOURCE"), &buffer_attr, + PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE); + + break; + } + + case PA_CONTEXT_TERMINATED: + mainloop_api->quit(mainloop_api, 0); + break; + + case PA_CONTEXT_FAILED: + default: + fprintf(stderr, "Context error: %s\n", pa_strerror(pa_context_errno(c))); + fail(); + } +} + +START_TEST (loopback_test) { + pa_mainloop* m = NULL; + int i, ret = 0, pulse_hz = SAMPLE_HZ / 1000; + + /* Generate a square pulse */ + for (i = 0; i < N_OUT; i++) + if (i < pulse_hz) + out[i][0] = out[i][1] = 1.0f; + else + out[i][0] = out[i][1] = 0.0f; + + ss = pa_sample_size(&sample_spec); + fs = pa_frame_size(&sample_spec); + + pstream = NULL; + + /* Set up a new main loop */ + m = pa_mainloop_new(); + fail_unless(m != NULL); + + mainloop_api = pa_mainloop_get_api(m); + + context = pa_context_new(mainloop_api, context_name); + fail_unless(context != NULL); + + pa_context_set_state_callback(context, context_state_callback, NULL); + + /* Connect the context */ + if (pa_context_connect(context, NULL, 0, NULL) < 0) { + fprintf(stderr, "pa_context_connect() failed.\n"); + goto quit; + } + + if (pa_mainloop_run(m, &ret) < 0) + fprintf(stderr, "pa_mainloop_run() failed.\n"); + +quit: + pa_context_unref(context); + + if (pstream) + pa_stream_unref(pstream); + + pa_mainloop_free(m); + + fail_unless(ret == 0); +} +END_TEST + +int main(int argc, char *argv[]) { + int failed = 0; + Suite *s; + TCase *tc; + SRunner *sr; + + context_name = argv[0]; + + s = suite_create("Loopback"); + tc = tcase_create("loopback"); + tcase_add_test(tc, loopback_test); + tcase_set_timeout(tc, 5 * 60); + suite_add_tcase(s, tc); + + sr = srunner_create(s); + srunner_set_fork_status(sr, CK_NOFORK); + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +}