Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: time synchronised playback #296

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
#include <string.h>
#include "audio.h"
#include "config.h"
#include "common.h"
#include "player.h"
#include "rtp.h"

#ifdef CONFIG_SNDIO
extern audio_output audio_sndio;
Expand Down Expand Up @@ -61,6 +64,7 @@ static audio_output *outputs[] = {
NULL
};

long long audio_delay;

audio_output *audio_get_output(char *name) {
audio_output **out;
Expand Down Expand Up @@ -89,3 +93,35 @@ void audio_ls_outputs(void) {
(*out)->help();
}
}

//gets called for generic outputs
long long audio_get_delay(void) {
return audio_delay;
}

void audio_estimate_delay(audio_output *output) {
signed short *silence;
int frame_size = 200;
long long base_time, cur_time, last_time, frame_time, frame_time_limit;

silence = malloc(4 * frame_size);
memset(silence, 0, 4 * frame_size);
frame_time = (frame_size * 1000000) / 44100;
frame_time_limit = frame_time * 2 / 3;

base_time = tstp_us();
last_time = base_time;
int loop = 0;
while (loop < 1000) {
output->play(silence, frame_size);
cur_time = tstp_us();
if ((cur_time - last_time) > frame_time_limit)
break;
last_time = cur_time;
loop++;
}
debug(3, "totaltime %lld, last loop time %lld, loop %d\n", last_time-base_time,cur_time - last_time, loop);
audio_delay = (loop * frame_time) - (last_time - base_time);
debug(2,"Generic output delay %lld\n", audio_delay);
free(silence);
}
5 changes: 5 additions & 0 deletions audio.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ typedef struct {

// may be NULL, in which case soft volume is applied
void (*volume)(double vol);

//time in us it takes before a new sample is output
long long (*get_delay)(void);
} audio_output;

audio_output *audio_get_output(char *name);
void audio_ls_outputs(void);
long long audio_get_delay(void);
void audio_estimate_delay(audio_output *output);

#endif //_AUDIO_H
75 changes: 69 additions & 6 deletions audio_alsa.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ static void start(int sample_rate);
static void play(short buf[], int samples);
static void stop(void);
static void volume(double vol);
static long long get_delay(void);

audio_output audio_alsa = {
.name = "alsa",
Expand All @@ -49,7 +50,8 @@ audio_output audio_alsa = {
.start = &start,
.stop = &stop,
.play = &play,
.volume = NULL
.volume = NULL,
.get_delay = &get_delay
};

static snd_pcm_t *alsa_handle = NULL;
Expand All @@ -65,6 +67,8 @@ static char *alsa_mix_dev = NULL;
static char *alsa_mix_ctrl = "Master";
static int alsa_mix_index = 0;

static int device_sample_rate;

static void help(void) {
printf(" -d output-device set the output device [default*|...]\n"
" -t mixer-type set the mixer type [software*|hardware]\n"
Expand Down Expand Up @@ -153,9 +157,10 @@ static void deinit(void) {
static void start(int sample_rate) {
if (sample_rate != 44100)
die("Unexpected sample rate!");
device_sample_rate = sample_rate;

int ret, dir = 0;
snd_pcm_uframes_t frames = 64;
snd_pcm_uframes_t period_size, buffer_size;
ret = snd_pcm_open(&alsa_handle, alsa_out_dev, SND_PCM_STREAM_PLAYBACK, 0);
if (ret < 0)
die("Alsa initialization failed: unable to open pcm device: %s\n", snd_strerror(ret));
Expand All @@ -166,10 +171,52 @@ static void start(int sample_rate) {
snd_pcm_hw_params_set_format(alsa_handle, alsa_params, SND_PCM_FORMAT_S16);
snd_pcm_hw_params_set_channels(alsa_handle, alsa_params, 2);
snd_pcm_hw_params_set_rate_near(alsa_handle, alsa_params, (unsigned int *)&sample_rate, &dir);
snd_pcm_hw_params_set_period_size_near(alsa_handle, alsa_params, &frames, &dir);
ret = snd_pcm_hw_params(alsa_handle, alsa_params);
if (ret < 0)
die("unable to set hw parameters: %s\n", snd_strerror(ret));

// setting period and buffer is a simplified version of what XBMC does
snd_pcm_hw_params_get_period_size_max(alsa_params, &period_size, NULL);
snd_pcm_hw_params_get_buffer_size_max(alsa_params, &buffer_size);
debug(1, "Hardware supports period_size_max: %d, buffer_size_max: %d\n", period_size, buffer_size);

// we want about 333 ms of buffer, and 50ms period
// buffer might still need some tweaking to get reliable operation on RPi + USB DAC...
// make sure we do not exceed what HW supports
period_size = (period_size < sample_rate / 20 ? period_size : sample_rate / 20);
buffer_size = (buffer_size < sample_rate / 3 ? buffer_size : sample_rate / 3);

// make sure buffer size is at least 4 times period size
period_size = (period_size < buffer_size / 4 ? period_size : buffer_size / 4);
debug(1, "Trying to set period_size: %d, buffer_size: %d\n", period_size, buffer_size);

// we keep the originals and try setting period and buffer using copies
snd_pcm_uframes_t period_temp, buffer_temp;
period_temp = period_size;
buffer_temp = buffer_size;
snd_pcm_hw_params_t *alsa_params_copy;
snd_pcm_hw_params_alloca(&alsa_params_copy);
snd_pcm_hw_params_copy(alsa_params_copy, alsa_params);

// some HW seems to be picky about the order period and buffer are set, so try both ways
// first try with buffer_size, period_size
if (snd_pcm_hw_params_set_buffer_size_near(alsa_handle, alsa_params_copy, &buffer_temp) != 0
|| snd_pcm_hw_params_set_period_size_near(alsa_handle, alsa_params_copy, &period_temp, NULL) != 0
|| snd_pcm_hw_params(alsa_handle, alsa_params_copy) != 0) {
period_temp = period_size;
buffer_temp = buffer_size;
snd_pcm_hw_params_copy(alsa_params_copy, alsa_params);
// retry with period_size, buffer_size
if (snd_pcm_hw_params_set_period_size_near(alsa_handle, alsa_params_copy, &period_temp, NULL) != 0
|| snd_pcm_hw_params_set_buffer_size_near(alsa_handle, alsa_params_copy, &buffer_temp) != 0
|| snd_pcm_hw_params(alsa_handle, alsa_params_copy) != 0) {
// set what alsa would have
warn("Setting period and buffer failed, going with the defaults\n");
ret = snd_pcm_hw_params(alsa_handle, alsa_params);
if (ret < 0)
die("unable to set hw parameters: %s\n", snd_strerror(ret));
// using alsa defaults, so see what they are
snd_pcm_get_params(alsa_handle, &buffer_size, &period_size);
debug(1, "Defaults are period_size: %d, buffer_size: %d\n", period_size, buffer_size);
}
}
}

static void play(short buf[], int samples) {
Expand All @@ -193,3 +240,19 @@ static void volume(double vol) {
if(snd_mixer_selem_set_playback_volume_all(alsa_mix_elem, alsa_volume) != 0)
die ("Failed to set playback volume");
}

static long long get_delay(void) {
snd_pcm_sframes_t frames = 0;
snd_pcm_delay(alsa_handle, &frames);

if (frames < 0)
{
#if SND_LIB_VERSION >= 0x000901 /* snd_pcm_forward() exists since 0.9.0rc8 */
snd_pcm_forward(alsa_handle, -frames);
#endif
frames = 0;
}

return (long long)frames * 1000000LL / (long long)device_sample_rate;
}

3 changes: 2 additions & 1 deletion audio_ao.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,6 @@ audio_output audio_ao = {
.start = &start,
.stop = &stop,
.play = &play,
.volume = NULL
.volume = NULL,
.get_delay = NULL
};
6 changes: 3 additions & 3 deletions audio_dummy.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ static void play(short buf[], int samples) {

samples_played += samples;

long long finishtime = starttime + samples_played * 1e6 / Fs;

usleep(finishtime - nowtime);
long long sleeptime = starttime + samples_played * 1e6 / Fs - nowtime;
if (sleeptime > 0)
usleep(sleeptime );
}

static void stop(void) {
Expand Down
3 changes: 2 additions & 1 deletion audio_pipe.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,6 @@ audio_output audio_pipe = {
.start = &start,
.stop = &stop,
.play = &play,
.volume = NULL
.volume = NULL,
.get_delay = NULL
};
16 changes: 15 additions & 1 deletion audio_pulse.c
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ static void stop(void) {
fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(pa_error));
}

static long long get_delay() {
pa_usec_t latency;
latency = pa_simple_get_latency(pa_dev, &pa_error);
if (pa_error < 0 )
{
latency = (pa_usec_t) 0;
fprintf(stderr, __FILE__": get_delay() failed: %s\n", pa_strerror(pa_error));
}

return (long long)latency;
}

audio_output audio_pulse = {
.name = "pulse",
.help = &help,
Expand All @@ -124,5 +136,7 @@ audio_output audio_pulse = {
.start = &start,
.stop = &stop,
.play = &play,
.volume = NULL
.volume = NULL,
.get_delay = &get_delay
};

24 changes: 22 additions & 2 deletions audio_sndio.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
static struct sio_hdl *sio;
static struct sio_par par;

long long writepos; /* frames written */


static void position_changed(void *addr, int delta)
{
writepos -= delta;
}

static int init(int argc, char **argv) {
sio = sio_open(SIO_DEVANY, SIO_PLAY, 0);
if (!sio)
Expand All @@ -41,6 +49,8 @@ static int init(int argc, char **argv) {
if (!sio_getpar(sio, &par))
die("sndio: failed to get audio parameters");

sio_onmove(sio, position_changed, NULL);

return 0;
}

Expand All @@ -51,11 +61,14 @@ static void deinit(void) {
static void start(int sample_rate) {
if (sample_rate != par.rate)
die("unexpected sample rate!");
writepos = 0;
sio_start(sio);
}

static void play(short buf[], int samples) {
sio_write(sio, (char *)buf, samples * par.bps * par.pchan);
int bytes_per_frame = par.bps * par.pchan;
sio_write(sio, (char *)buf, samples * bytes_per_frame);
writepos += samples;
}

static void stop(void) {
Expand All @@ -72,6 +85,12 @@ static void volume(double vol) {
sio_setvol(sio, v);
}

static long long get_delay(void) {
long long delay = writepos * 1000000LL / (long long)par.rate;

return delay;
}

audio_output audio_sndio = {
.name = "sndio",
.help = &help,
Expand All @@ -80,5 +99,6 @@ audio_output audio_sndio = {
.start = &start,
.stop = &stop,
.play = &play,
.volume = &volume
.volume = &volume,
.get_delay = get_delay
};
31 changes: 31 additions & 0 deletions common.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <math.h>
#include <memory.h>
#include <unistd.h>
#include <openssl/rsa.h>
Expand Down Expand Up @@ -202,3 +203,33 @@ void command_stop(void) {
if (!config.cmd_blocking)
exit(0);
}

void biquad_init(biquad_t *bq, double a[], double b[]) {
bq->hist[0] = bq->hist[1] = 0.0;
memcpy(bq->a, a, 2*sizeof(double));
memcpy(bq->b, b, 3*sizeof(double));
}

void biquad_lpf(biquad_t *bq, double freq, double Q) {
double w0 = 2.0 * M_PI * freq;
double alpha = sin(w0)/(2.0*Q);

double a_0 = 1.0 + alpha;
double b[3], a[2];
b[0] = (1.0-cos(w0))/(2.0*a_0);
b[1] = (1.0-cos(w0))/a_0;
b[2] = b[0];
a[0] = -2.0*cos(w0)/a_0;
a[1] = (1-alpha)/a_0;

biquad_init(bq, a, b);
}

double biquad_filt(biquad_t *bq, double in) {
double w = in - bq->a[0]*bq->hist[0] - bq->a[1]*bq->hist[1];
double out = bq->b[1]*bq->hist[0] + bq->b[2]*bq->hist[1] + bq->b[0]*w;
bq->hist[1] = bq->hist[0];
bq->hist[0] = w;

return out;
}
12 changes: 11 additions & 1 deletion common.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ typedef struct {
audio_output *output;
char *mdns_name;
mdns_backend *mdns;
int buffer_start_fill;
int delay;
int daemonise;
char *cmd_start, *cmd_stop;
int cmd_blocking;
Expand Down Expand Up @@ -63,4 +63,14 @@ extern shairport_cfg config;
void shairport_shutdown(int retval);
void shairport_startup_complete(void);

typedef struct {
double hist[2];
double a[2];
double b[3];
} biquad_t;

void biquad_init(biquad_t *bq, double a[], double b[]);
void biquad_lpf(biquad_t *bq, double freq, double Q);
double biquad_filt(biquad_t *bq, double in);

#endif // _COMMON_H
Loading