bat.c \
common.c \
signal.c \
+ latencytest.c \
convert.c
noinst_HEADERS = \
common.h \
bat-signal.h \
+ latencytest.h \
convert.h
if HAVE_LIBFFTW3
#include "common.h"
#include "alsa.h"
+#include "latencytest.h"
struct pcm_container {
snd_pcm_t *handle;
}
if (bat->buffer_size > 0 && bat->period_size == 0)
- bat->period_size = bat->buffer_size / DIV_BUFFERTIME;
+ bat->period_size = bat->buffer_size / DIV_BUFFERSIZE;
+
+ if (bat->roundtriplatency && bat->buffer_size == 0) {
+ /* Set to minimum buffer size and period size
+ for latency test */
+ if (snd_pcm_hw_params_get_buffer_size_min(params,
+ &buffer_size) < 0) {
+ fprintf(bat->err,
+ _("Get parameter from device error: "));
+ fprintf(bat->err, _("buffer size min: %d %s: %s(%d)\n"),
+ (int) buffer_size,
+ device_name, snd_strerror(err), err);
+ return -EINVAL;
+ }
+
+ if (snd_pcm_hw_params_get_period_size_min(params,
+ &period_size, 0) < 0) {
+ fprintf(bat->err,
+ _("Get parameter from device error: "));
+ fprintf(bat->err, _("period size min: %d %s: %s(%d)\n"),
+ (int) period_size,
+ device_name, snd_strerror(err), err);
+ return -EINVAL;
+ }
+ bat->buffer_size = (int) buffer_size;
+ bat->period_size = (int) period_size;
+ }
if (bat->buffer_size > 0) {
buffer_size = bat->buffer_size;
} else if (err == -EPIPE) {
fprintf(bat->err, _("Underrun: %s(%d)\n"),
snd_strerror(err), err);
+ if (bat->roundtriplatency)
+ bat->latency.xrun_error = true;
snd_pcm_prepare(sndpcm->handle);
} else if (err < 0) {
fprintf(bat->err, _("Write PCM device error: %s(%d)\n"),
return 0;
}
+/**
+ * Process output data for latency test
+ */
+static int latencytest_process_output(struct pcm_container *sndpcm,
+ struct bat *bat)
+{
+ int err = 0;
+ int bytes = sndpcm->period_bytes; /* playback buffer size */
+ int frames = sndpcm->period_size; /* frame count */
+
+ bat->latency.is_playing = true;
+
+ while (1) {
+ /* generate output data */
+ err = handleoutput(bat, sndpcm->buffer, bytes, frames);
+ if (err != 0)
+ break;
+
+ err = write_to_pcm(sndpcm, frames, bat);
+ if (err != 0)
+ break;
+
+ /* Xrun error, terminate the playback thread*/
+ if (bat->latency.xrun_error == true)
+ break;
+
+ if (bat->latency.state == LATENCY_STATE_COMPLETE_SUCCESS)
+ break;
+
+ bat->periods_played++;
+ }
+
+ bat->latency.is_playing = false;
+
+ return err;
+}
+
static int write_to_pcm_loop(struct pcm_container *sndpcm, struct bat *bat)
{
int err = 0;
}
}
- err = write_to_pcm_loop(&sndpcm, bat);
+ if (bat->roundtriplatency)
+ err = latencytest_process_output(&sndpcm, bat);
+ else
+ err = write_to_pcm_loop(&sndpcm, bat);
if (err < 0) {
retval_play = err;
goto exit4;
snd_pcm_prepare(sndpcm->handle);
fprintf(bat->err, _("Overrun: %s(%d)\n"),
snd_strerror(err), err);
+ if (bat->roundtriplatency)
+ bat->latency.xrun_error = true;
} else if (err < 0) {
fprintf(bat->err, _("Read PCM device error: %s(%d)\n"),
snd_strerror(err), err);
return err;
}
+/**
+ * Process input data for latency test
+ */
+static int latencytest_process_input(struct pcm_container *sndpcm,
+ struct bat *bat)
+{
+ int err = 0;
+ FILE *fp = NULL;
+ int bytes_read = 0;
+ int frames = sndpcm->period_size;
+ int size = sndpcm->period_bytes;
+ int bytes_count = bat->frames * bat->frame_size;
+
+ remove(bat->capture.file);
+ fp = fopen(bat->capture.file, "wb");
+ err = -errno;
+ if (fp == NULL) {
+ fprintf(bat->err, _("Cannot open file: %s %d\n"),
+ bat->capture.file, err);
+ return err;
+ }
+ /* leave space for file header */
+ if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) {
+ fclose(fp);
+ return err;
+ }
+
+ bat->latency.is_capturing = true;
+
+ while (bytes_read < bytes_count) {
+ /* read a chunk from pcm device */
+ err = read_from_pcm(sndpcm, frames, bat);
+ if (err != 0)
+ break;
+
+ /* Xrun error, terminate the capture thread*/
+ if (bat->latency.xrun_error == true)
+ break;
+
+ err = handleinput(bat, sndpcm->buffer, frames);
+ if (err != 0)
+ break;
+
+ if (bat->latency.is_playing == false)
+ break;
+
+ /* write the chunk to file */
+ if (fwrite(sndpcm->buffer, 1, size, fp) != size) {
+ err = -EIO;
+ break;
+ }
+
+ bytes_read += size;
+ }
+
+ bat->latency.is_capturing = false;
+
+ update_wav_header(bat, fp, bytes_read);
+
+ fclose(fp);
+ return err;
+}
+
+
static void pcm_cleanup(void *p)
{
snd_pcm_close(p);
pthread_cleanup_push(free, sndpcm.buffer);
fprintf(bat->log, _("Recording ...\n"));
- err = read_from_pcm_loop(&sndpcm, bat);
+ if (bat->roundtriplatency)
+ err = latencytest_process_input(&sndpcm, bat);
+ else
+ err = read_from_pcm_loop(&sndpcm, bat);
if (err != 0) {
retval_record = err;
goto exit3;
latestfile=`ls -t1 /tmp/bat.wav.* | head -n 1`
feature_test "--local -F $maxfreq --file $latestfile" \
"local mode: analyze local file"
+ feature_test "--roundtriplatency" \
+ "round trip latency test"
print_result
}
The ALSABAT being built without libfftw3 support is always in standalone mode.
The ALSABAT in normal mode can also bypass data analysis using option
"--standalone".
+.TP
+\fI\-\-roundtriplatency\fP
+Round trip latency test.
+Audio latency is the time delay as an audio signal passes through a system.
+There are many kinds of audio latency metrics. One useful metric is the
+round trip latency, which is the sum of output latency and input latency.
.SH EXAMPLES
#ifdef HAVE_LIBFFTW3
#include "analyze.h"
#endif
+#include "latencytest.h"
static int get_duration(struct bat *bat)
{
" --saveplay=# file that storing playback content, for debug\n"
" --local internal loop, set to bypass pcm hardware devices\n"
" --standalone standalone mode, to bypass analysis\n"
+" --roundtriplatency round trip latency mode\n"
));
fprintf(bat->log, _("Recognized sample formats are: "));
fprintf(bat->log, _("U8 S16_LE S24_3LE S32_LE\n"));
bat->local = false;
bat->buffer_size = 0;
bat->period_size = 0;
+ bat->roundtriplatency = false;
#ifdef HAVE_LIBTINYALSA
bat->channels = 2;
bat->playback.fct = &playback_tinyalsa;
{"saveplay", 1, 0, OPT_SAVEPLAY},
{"local", 0, 0, OPT_LOCAL},
{"standalone", 0, 0, OPT_STANDALONE},
+ {"roundtriplatency", 0, 0, OPT_ROUNDTRIPLATENCY},
{0, 0, 0, 0}
};
case OPT_STANDALONE:
bat->standalone = true;
break;
+ case OPT_ROUNDTRIPLATENCY:
+ bat->roundtriplatency = true;
+ break;
case 'D':
if (bat->playback.device == NULL)
bat->playback.device = optarg;
if (err < 0)
goto out;
+ /* round trip latency test thread */
+ if (bat.roundtriplatency) {
+ while (1) {
+ fprintf(bat.log,
+ _("\nStart round trip latency\n"));
+ roundtrip_latency_init(&bat);
+ test_loopback(&bat);
+
+ if (bat.latency.xrun_error == false)
+ break;
+ else {
+ /* Xrun error in playback or capture,
+ increase period size and try again */
+ bat.period_size += bat.rate / 1000;
+ bat.buffer_size =
+ bat.period_size * DIV_BUFFERSIZE;
+
+ /* terminate the test if period_size is
+ large enough */
+ if (bat.period_size > bat.rate * 0.2)
+ break;
+ }
+
+ /* Waiting 500ms and start the next round */
+ usleep(CAPTURE_DELAY * 1000);
+ }
+ goto out;
+ }
+
/* single line playback thread: playback only, no capture */
if (bat.playback.mode == MODE_SINGLE) {
test_playback(&bat);
#define OPT_SAVEPLAY (OPT_BASE + 3)
#define OPT_LOCAL (OPT_BASE + 4)
#define OPT_STANDALONE (OPT_BASE + 5)
+#define OPT_ROUNDTRIPLATENCY (OPT_BASE + 6)
#define COMPOSE(a, b, c, d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24))
#define WAV_RIFF COMPOSE('R', 'I', 'F', 'F')
/* default period size for tinyalsa */
#define TINYALSA_PERIODSIZE 1024
+#define LATENCY_TEST_NUMBER 5
+#define LATENCY_TEST_TIME_LIMIT 25
+#define DIV_BUFFERSIZE 2
+
#define EBATBASE 1000
#define ENOPEAK (EBATBASE + 1)
#define EONLYDC (EBATBASE + 2)
MODE_LAST
};
+enum latency_state {
+ LATENCY_STATE_COMPLETE_FAILURE = -1,
+ LATENCY_STATE_COMPLETE_SUCCESS = 0,
+ LATENCY_STATE_MEASURE_FOR_1_SECOND,
+ LATENCY_STATE_PLAY_AND_LISTEN,
+ LATENCY_STATE_WAITING,
+};
+
struct pcm {
unsigned int card_tiny;
unsigned int device_tiny;
float magnitude;
};
+struct roundtrip_latency {
+ int number;
+ enum latency_state state;
+ float result[LATENCY_TEST_NUMBER];
+ int final_result;
+ int samples;
+ float sum;
+ int threshold;
+ int error;
+ bool is_capturing;
+ bool is_playing;
+ bool xrun_error;
+};
+
struct bat {
unsigned int rate; /* sampling rate */
int channels; /* nb of channels */
char *logarg; /* path name of log file */
char *debugplay; /* path name to store playback signal */
bool standalone; /* enable to bypass analysis */
+ bool roundtriplatency; /* enable round trip latency */
struct pcm playback;
struct pcm capture;
+ struct roundtrip_latency latency;
unsigned int periods_played;
unsigned int periods_total;
--- /dev/null
+/*
+ * Copyright (C) 2013-2015 Intel Corporation
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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.
+ *
+ */
+
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+
+#include "common.h"
+#include "bat-signal.h"
+#include "gettext.h"
+
+/* How one measurement step works:
+ - Listen and measure the average loudness of the environment for 1 second.
+ - Create a threshold value 16 decibels higher than the average loudness.
+ - Begin playing a ~1000 Hz sine wave and start counting the samples elapsed.
+ - Stop counting and playing if the input's loudness is higher than the
+ threshold, as the output wave is probably coming back.
+ - Calculate the round trip audio latency value in milliseconds. */
+
+static float sumaudio(struct bat *bat, short int *buffer, int frames)
+{
+ float sum = 0;
+ int n = 0;
+
+ while (frames) {
+ frames--;
+
+ for (n = 0; n < bat->channels; n++) {
+ sum += abs(buffer[0]);
+ buffer++;
+ }
+ }
+
+ sum = sum / bat->channels;
+
+ return sum;
+}
+
+static void play_and_listen(struct bat *bat, void *buffer, int frames)
+{
+ int averageinput;
+ int n = 0;
+ float sum = 0;
+ float max = 0;
+ float min = 100000.0f;
+ short int *input;
+ int num = bat->latency.number;
+
+ averageinput = (int) (sumaudio(bat, buffer, frames) / frames);
+
+ /* The signal is above threshold
+ So our sine wave comes back on the input */
+ if (averageinput > bat->latency.threshold) {
+ input = buffer;
+
+ /* Check the location when it became loud enough */
+ while (n < frames) {
+ if (*input++ > bat->latency.threshold)
+ break;
+ *input += bat->channels;
+ n++;
+ }
+
+ /* Now we get the total round trip latency*/
+ bat->latency.samples += n;
+
+ /* Expect at least 1 buffer of round trip latency. */
+ if (bat->latency.samples > frames) {
+ bat->latency.result[num - 1] =
+ (float) bat->latency.samples * 1000 / bat->rate;
+ fprintf(bat->log,
+ _("Test%d, round trip latency %dms\n"),
+ num,
+ (int) bat->latency.result[num - 1]);
+
+ for (n = 0; n < num; n++) {
+ if (bat->latency.result[n] > max)
+ max = bat->latency.result[n];
+ if (bat->latency.result[n] < min)
+ min = bat->latency.result[n];
+ sum += bat->latency.result[n];
+ }
+
+ /* The maximum is higher than the minimum's double */
+ if (max / min > 2.0f) {
+ bat->latency.state =
+ LATENCY_STATE_COMPLETE_FAILURE;
+ bat->latency.is_capturing = false;
+ return;
+
+ /* Final results */
+ } else if (num == LATENCY_TEST_NUMBER) {
+ bat->latency.final_result =
+ (int) (sum / LATENCY_TEST_NUMBER);
+ fprintf(bat->log,
+ _("Final round trip latency: %dms\n"),
+ bat->latency.final_result);
+
+ bat->latency.state =
+ LATENCY_STATE_COMPLETE_SUCCESS;
+ bat->latency.is_capturing = false;
+ return;
+
+ /* Next step */
+ } else
+ bat->latency.state = LATENCY_STATE_WAITING;
+
+ bat->latency.number++;
+
+ } else
+ /* Happens when an early noise comes in */
+ bat->latency.state = LATENCY_STATE_WAITING;
+
+ } else {
+ /* Still listening */
+ bat->latency.samples += frames;
+
+ /* Do not listen to more than a second
+ Maybe too much background noise */
+ if (bat->latency.samples > bat->rate) {
+ bat->latency.error++;
+
+ if (bat->latency.error > LATENCY_TEST_NUMBER) {
+ fprintf(bat->err,
+ _("Could not detect signal."));
+ fprintf(bat->err,
+ _("Too much background noise?\n"));
+ bat->latency.state =
+ LATENCY_STATE_COMPLETE_FAILURE;
+ bat->latency.is_capturing = false;
+ return;
+ }
+
+ /* let's start over */
+ bat->latency.state = LATENCY_STATE_WAITING;
+ }
+ }
+
+ return;
+}
+
+static void calculate_threshold(struct bat *bat)
+{
+ float average;
+ float reference;
+
+ /* Calculate the average loudness of the environment and create
+ a threshold value 16 decibels higher than the average loudness */
+ average = bat->latency.sum / bat->latency.samples / 32767.0f;
+ reference = 20.0f * log10f(average) + 16.0f;
+ bat->latency.threshold = (int) (powf(10.0f, reference / 20.0f)
+ * 32767.0f);
+}
+
+void roundtrip_latency_init(struct bat *bat)
+{
+ bat->latency.number = 1;
+ bat->latency.state = LATENCY_STATE_MEASURE_FOR_1_SECOND;
+ bat->latency.final_result = 0;
+ bat->latency.samples = 0;
+ bat->latency.sum = 0;
+ bat->latency.threshold = 0;
+ bat->latency.is_capturing = false;
+ bat->latency.is_playing = false;
+ bat->latency.error = 0;
+ bat->latency.xrun_error = false;
+ bat->format = BAT_PCM_FORMAT_S16_LE;
+ bat->frames = LATENCY_TEST_TIME_LIMIT * bat->rate;
+ bat->periods_played = 0;
+}
+
+int handleinput(struct bat *bat, void *buffer, int frames)
+{
+ switch (bat->latency.state) {
+ /* Measuring average loudness for 1 second */
+ case LATENCY_STATE_MEASURE_FOR_1_SECOND:
+ bat->latency.sum += sumaudio(bat, buffer, frames);
+ bat->latency.samples += frames;
+
+ /* 1 second elapsed */
+ if (bat->latency.samples >= bat->rate) {
+ calculate_threshold(bat);
+ bat->latency.state = LATENCY_STATE_PLAY_AND_LISTEN;
+ bat->latency.samples = 0;
+ bat->latency.sum = 0;
+ }
+ break;
+
+ /* Playing sine wave and listening if it comes back */
+ case LATENCY_STATE_PLAY_AND_LISTEN:
+ play_and_listen(bat, buffer, frames);
+ break;
+
+ /* Waiting 1 second */
+ case LATENCY_STATE_WAITING:
+ bat->latency.samples += frames;
+
+ if (bat->latency.samples > bat->rate) {
+ /* 1 second elapsed, start over */
+ bat->latency.samples = 0;
+ bat->latency.state = LATENCY_STATE_MEASURE_FOR_1_SECOND;
+ }
+ break;
+
+ default:
+ return 0;
+ }
+
+ return 0;
+}
+
+int handleoutput(struct bat *bat, void *buffer, int bytes, int frames)
+{
+ int err = 0;
+
+ /* If capture completed, terminate the playback */
+ if (bat->periods_played * frames > 2 * bat->rate
+ && bat->latency.is_capturing == false)
+ return bat->latency.state;
+
+ if (bat->latency.state == LATENCY_STATE_PLAY_AND_LISTEN)
+ err = generate_sine_wave(bat, frames, buffer);
+ else
+ /* Output silence */
+ memset(buffer, 0, bytes);
+
+ return err;
+}
--- /dev/null
+/*
+ * Copyright (C) 2013-2015 Intel Corporation
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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.
+ *
+ */
+void roundtrip_latency_init(struct bat *);
+int handleinput(struct bat *, void *, int);
+int handleoutput(struct bat *, void *, int, int);
#include "common.h"
#include "tinyalsa.h"
+#include "latencytest.h"
struct format_map_table {
enum _bat_pcm_format format_bat;
return err;
}
+/**
+ * Process output data for latency test
+ */
+static int latencytest_process_output(struct bat *bat, struct pcm *pcm,
+ void *buffer, int bytes)
+{
+ int err = 0;
+ int frames = bytes / bat->frame_size;
+
+ fprintf(bat->log, _("Play sample with %d frames buffer\n"), frames);
+
+ bat->latency.is_playing = true;
+
+ while (1) {
+ /* generate output data */
+ err = handleoutput(bat, buffer, bytes, frames);
+ if (err != 0)
+ break;
+
+ err = pcm_write(pcm, buffer, bytes);
+ if (err != 0)
+ break;
+
+ if (bat->latency.state == LATENCY_STATE_COMPLETE_SUCCESS)
+ break;
+
+ bat->periods_played++;
+ }
+
+ bat->latency.is_playing = false;
+
+ return err;
+}
+
/**
* Play sample
*/
}
}
- err = play_sample(bat, pcm, buffer, bufbytes);
+ if (bat->roundtriplatency)
+ err = latencytest_process_output(bat, pcm, buffer, bufbytes);
+ else
+ err = play_sample(bat, pcm, buffer, bufbytes);
if (err < 0) {
retval_play = err;
goto exit4;
return err;
}
+/**
+ * Process input data for latency test
+ */
+static int latencytest_process_input(struct bat *bat, struct pcm *pcm,
+ void *buffer, unsigned int bytes)
+{
+ int err = 0;
+ FILE *fp = NULL;
+ unsigned int bytes_read = 0;
+ unsigned int bytes_count = bat->frames * bat->frame_size;
+
+ remove(bat->capture.file);
+ fp = fopen(bat->capture.file, "wb");
+ err = -errno;
+ if (fp == NULL) {
+ fprintf(bat->err, _("Cannot open file: %s %d\n"),
+ bat->capture.file, err);
+ return err;
+ }
+ /* leave space for file header */
+ if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) {
+ err = -errno;
+ fclose(fp);
+ return err;
+ }
+
+ bat->latency.is_capturing = true;
+
+ while (bytes_read < bytes_count && !pcm_read(pcm, buffer, bytes)) {
+ if (fwrite(buffer, 1, bytes, fp) != bytes)
+ break;
+
+ err = handleinput(bat, buffer, bytes / bat->frame_size);
+ if (err != 0)
+ break;
+
+ if (bat->latency.is_playing == false)
+ break;
+
+ bytes_read += bytes;
+ }
+
+ bat->latency.is_capturing = false;
+
+ err = update_wav_header(bat, fp, bytes_read);
+
+ fclose(fp);
+ return err;
+}
+
/**
* Record
*/
pthread_cleanup_push(free, buffer);
fprintf(bat->log, _("Recording ...\n"));
- err = capture_sample(bat, pcm, buffer, bufbytes);
+ if (bat->roundtriplatency)
+ err = latencytest_process_input(bat, pcm, buffer, bufbytes);
+ else
+ err = capture_sample(bat, pcm, buffer, bufbytes);
if (err != 0) {
retval_record = err;
goto exit3;