/*
 * pcm timer test - check drift
 */

#include <stdio.h>
#include <alsa/asoundlib.h>

#define TOTAL_LEN	(5 * 60)
#define	FREQ		44100
#define PRD_SIZE	32
#define CHANNELS	2
#define TEMPO		500000
#define PPQ		480

#define PCM_CARD	0
#define PCM_DEV		0

static void usage(void)
{
	fprintf(stderr, "timertest [-options]\n");
	fprintf(stderr, "  -d sec    measured time length in seconds (default = %d)\n", TOTAL_LEN);
	fprintf(stderr, "  -r freq   pcm rate in Hz (default = %d)\n", FREQ);
	fprintf(stderr, "  -p frms   pcm period size in frames (default = %d)\n", PRD_SIZE);
	fprintf(stderr, "  -c ch     pcm channels (default = %d)\n", CHANNELS);
	fprintf(stderr, "  -t tempo  MIDI tempo in usec/beat (default = %d)\n", TEMPO);
	fprintf(stderr, "  -q ppq    MIDI resolution (PPQ) (default = %d)\n", PPQ);
	fprintf(stderr, "  -C card   pcm card number (default = %d)\n", PCM_CARD);
	fprintf(stderr, "  -D dev    pcm device number (default = %d)\n", PCM_DEV);
}


int main(int argc, char **argv)
{
	int q, port;
	long c, len, total;
	int total_len = TOTAL_LEN;
	int freq = FREQ;
	int channels = CHANNELS;
	int seq_tempo = TEMPO;
	int seq_ppq = PPQ;
	int period_size = PRD_SIZE;
	int buffer_size;
	int pcm_card = PCM_CARD;
	int pcm_dev = PCM_DEV;
	char pcm_name[256];
	char *buf;
	snd_seq_t *seq;
	snd_pcm_t *pcm;
	snd_timer_id_t *id;
	snd_seq_queue_timer_t *qt;
	snd_seq_queue_tempo_t *tempo;
	snd_pcm_hw_params_t *hw;
	snd_seq_queue_status_t *st;
	snd_pcm_status_t *pstat;
	snd_timestamp_t start_time, end_time;
	snd_seq_real_time_t seq_time;
	unsigned int seq_tick;
	long long d;
	long long t;
	int x;

	while ((x = getopt(argc, argv, "d:r:p:t:c:q:C:D:h")) != -1) {
		switch (x) {
		case 'd':
			total_len = atoi(optarg);
			break;
		case 'r':
			freq = atoi(optarg);
			break;
		case 'p':
			period_size = atoi(optarg);
			break;
		case 'c':
			channels = atoi(optarg);
			break;
		case 't':
			seq_tempo = atoi(optarg);
			break;
		case 'q':
			seq_ppq = atoi(optarg);
			break;
		case 'C':
			pcm_card = atoi(optarg);
			break;
		case 'D':
			pcm_dev = atoi(optarg);
			break;
		case 'h':
		default:
			usage();
			return 1;
		}
	}

	sprintf(pcm_name, "hw:%d,%d", pcm_card, pcm_dev);

	if (snd_seq_open(&seq, "hw", SND_SEQ_OPEN_DUPLEX, 0) < 0) {
		perror("snd_seq_open");
		return 1;
	}
	
	q = snd_seq_alloc_queue(seq);
	if (q < 0) {
		perror("alloc queue");
		return 1;
	}

	snd_seq_queue_timer_alloca(&qt);
	snd_timer_id_alloca(&id);
	snd_timer_id_set_class(id, SND_TIMER_CLASS_PCM);
	snd_timer_id_set_card(id, pcm_card);
	snd_timer_id_set_device(id, pcm_dev);
	snd_timer_id_set_subdevice(id, 0);
	snd_seq_queue_timer_set_type(qt, SND_SEQ_TIMER_ALSA);
	snd_seq_queue_timer_set_id(qt, id);
	if (snd_seq_set_queue_timer(seq, q, qt) < 0) {
		perror("snd_seq_set_queue_timer");
		return 1;
	}

	port = snd_seq_create_simple_port(seq, "port",
					  SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_WRITE|
					  SND_SEQ_PORT_CAP_SUBS_READ|
					  SND_SEQ_PORT_CAP_SUBS_WRITE,
					  SND_SEQ_PORT_TYPE_APPLICATION);
	if (port < 0) {
		perror("create port");
		return 1;
	}

	snd_seq_queue_tempo_alloca(&tempo);
	snd_seq_queue_tempo_set_tempo(tempo, seq_tempo);
	snd_seq_queue_tempo_set_ppq(tempo, seq_ppq);
	if (snd_seq_set_queue_tempo(seq, q, tempo) < 0) {
		perror("set queue tempo");
		return 1;
	}

	if (snd_pcm_open(&pcm, pcm_name, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
		perror("pcm open");
		return 1;
	}

	snd_pcm_hw_params_alloca(&hw);
	snd_pcm_hw_params_any(pcm, hw);
	snd_pcm_hw_params_set_access(pcm, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
	snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16_LE);
	snd_pcm_hw_params_set_channels(pcm, hw, channels);
	snd_pcm_hw_params_set_rate_near(pcm, hw, freq, 0);
	period_size = snd_pcm_hw_params_set_period_size_near(pcm, hw, period_size, 0);
	buffer_size = snd_pcm_hw_params_set_buffer_size_near(pcm, hw, period_size * 128);
	if (snd_pcm_hw_params(pcm, hw) < 0) {
		perror("pcm hw params");
		return 1;
	}

	buf = malloc(2 * channels * period_size);
	if (! buf) {
		perror("malloc buffer");
		return 1;
	}
	memset(buf, 0, 2 * channels * period_size);
	printf("period size = %d frames\n", period_size);
	printf("buffer size = %d frames\n", buffer_size);

	snd_pcm_status_alloca(&pstat);

	snd_seq_start_queue(seq, q, NULL);
	snd_seq_drain_output(seq);

	total = c = ((total_len * freq + period_size - 1) / period_size) * period_size;
	while (c > 0) {
		len = snd_pcm_writei(pcm, buf, period_size);
		if (len != period_size) {
			fprintf(stderr, "underrun %ld\n", len);
		} else
			c -= period_size;
	}
	if (snd_pcm_status(pcm, pstat) < 0) {
		perror("snd_pcm_status");
		return 1;
	}
	snd_pcm_status_get_trigger_tstamp(pstat, &start_time);

	snd_pcm_drain(pcm);

	if (snd_pcm_status(pcm, pstat) < 0) {
		perror("snd_pcm_status");
		return 1;
	}
	snd_pcm_status_get_trigger_tstamp(pstat, &end_time);

	snd_seq_stop_queue(seq, q, NULL);

	snd_seq_queue_status_alloca(&st);
	snd_seq_get_queue_status(seq, q, st);

	seq_time = *snd_seq_queue_status_get_real_time(st);
	seq_tick = snd_seq_queue_status_get_tick_time(st);

	printf("queue ticks = %d\n", seq_tick);
	printf("queue time = %d.%09d\n", seq_time.tv_sec, seq_time.tv_nsec);
	t = end_time.tv_sec - start_time.tv_sec;
	t *= (long long)1000000;
	if (end_time.tv_usec < start_time.tv_usec) {
		t -= start_time.tv_usec - end_time.tv_usec;
	} else {
		t += end_time.tv_usec - start_time.tv_usec;
	}
	t *= (long long)1000;
	printf("measured time = %d.%09d\n",
		(int)(t / 1000000000L),
		(int)(t % 1000000000L));
	printf("total samples = %ld\n", total);
	d = ((long long)total * 1000000000) / freq;
	printf("time from samples = %d.%09d\n",
		(int)(d / 1000000000),
		(int)(d % 1000000000));
	
	{ double timer_res, tick_res, exp_ticks;

	timer_res = (double)period_size / (double)freq;
	tick_res = (double)seq_tempo * 1e-6 / (double)seq_ppq;
	exp_ticks = t * 1e-9 / tick_res;
	printf("calculated ticks from measured time = %g\n", exp_ticks);
	d = (long long)seq_time.tv_sec * 1000000000L + (long long)seq_time.tv_nsec;
	exp_ticks = d * 1e-9 / tick_res;
	printf("calculated ticks from seq time = %g\n", exp_ticks);
	}

	snd_seq_close(seq);
	return 0;
}
