alsa-utils/alsaloop/alsaloop.c
Jaroslav Kysela 1e75673035 Introduce alsaloop utility
alsaloop allows create a PCM loopback between a PCM capture device
and a PCM playback device.

alsaloop supports multiple soundcards, adaptive clock synchronization,
adaptive rate resampling using the samplerate library (if available in
the system). Also, mixer controls can be redirected from one card to
another (for example Master and PCM).

Signed-off-by: Jaroslav Kysela <perex@perex.cz>
2010-10-06 10:01:52 +02:00

740 lines
19 KiB
C

/*
* A simple PCM loopback utility with adaptive sample rate support
*
* Author: Jaroslav Kysela <perex@perex.cz>
*
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sched.h>
#include <errno.h>
#include <getopt.h>
#include <alsa/asoundlib.h>
#include <sys/time.h>
#include <math.h>
#include <pthread.h>
#include <syslog.h>
#include "alsaloop.h"
struct loopback_thread {
int threaded;
pthread_t thread;
int exitcode;
struct loopback **loopbacks;
int loopbacks_count;
snd_output_t *output;
};
int verbose = 0;
int daemonize = 0;
int use_syslog = 0;
struct loopback **loopbacks;
int loopbacks_count = 0;
static void my_exit(struct loopback_thread *thread, int exitcode)
{
int i;
for (i = 0; i < thread->loopbacks_count; i++)
pcmjob_done(thread->loopbacks[i]);
if (thread->threaded) {
thread->exitcode = exitcode;
pthread_exit(0);
}
exit(exitcode);
}
static int create_loopback_handle(struct loopback_handle **_handle,
const char *device,
const char *id)
{
char idbuf[1024];
struct loopback_handle *handle;
handle = calloc(1, sizeof(*handle));
if (handle == NULL)
return -ENOMEM;
if (device == NULL)
device = "hw:0,0";
handle->device = strdup(device);
snprintf(idbuf, sizeof(idbuf)-1, "%s %s", id, device);
idbuf[sizeof(idbuf)-1] = '\0';
handle->id = strdup(idbuf);
handle->access = SND_PCM_ACCESS_RW_INTERLEAVED;
handle->format = SND_PCM_FORMAT_S16_LE;
handle->rate = 48000;
handle->channels = 2;
handle->resample = 1;
*_handle = handle;
return 0;
}
static int create_loopback(struct loopback **_handle,
struct loopback_handle *play,
struct loopback_handle *capt,
snd_output_t *output)
{
struct loopback *handle;
handle = calloc(1, sizeof(*handle));
if (handle == NULL)
return -ENOMEM;
handle->play = play;
handle->capt = capt;
play->loopback = handle;
capt->loopback = handle;
handle->latency_req = 0;
handle->latency_reqtime = 10000;
handle->loop_time = ~0UL;
handle->loop_limit = ~0ULL;
handle->output = output;
#ifdef USE_SAMPLERATE
handle->src_enable = 1;
handle->src_converter_type = SRC_SINC_BEST_QUALITY;
#endif
*_handle = handle;
return 0;
}
static void set_loop_time(struct loopback *loop, unsigned long loop_time)
{
loop->loop_time = loop_time;
loop->loop_limit = loop->capt->rate * loop_time;
}
static void setscheduler(void)
{
struct sched_param sched_param;
if (sched_getparam(0, &sched_param) < 0) {
logit(LOG_WARNING, "Scheduler getparam failed.\n");
return;
}
sched_param.sched_priority = sched_get_priority_max(SCHED_RR);
if (!sched_setscheduler(0, SCHED_RR, &sched_param)) {
if (verbose)
logit(LOG_WARNING, "Scheduler set to Round Robin with priority %i\n", sched_param.sched_priority);
return;
}
if (verbose)
logit(LOG_INFO, "!!!Scheduler set to Round Robin with priority %i FAILED!\n", sched_param.sched_priority);
}
void help(void)
{
int k;
printf(
"Usage: alsaloop [OPTION]...\n\n"
"-h,--help help\n"
"-g,--config configuration file (one line = one job specified)\n"
"-d,--daemonize daemonize the main process and use syslog for errors\n"
"-P,--pdevice playback device\n"
"-C,--cdevice capture device\n"
"-l,--latency requested latency in frames\n"
"-t,--tlatency requested latency in usec (1/1000000sec)\n"
"-f,--format sample format\n"
"-c,--channels channels\n"
"-r,--rate rate\n"
"-n,--resample resample in alsa-lib\n"
"-A,--samplerate use converter (0=sincbest,1=sincmedium,2=sincfastest,\n"
" 3=zerohold,4=linear)\n"
"-B,--buffer buffer size in frames\n"
"-E,--period period size in frames\n"
"-s,--seconds duration of loop in seconds\n"
"-b,--nblock non-block mode (very early process wakeup)\n"
"-S,--sync sync mode(0=none,1=simple,2=captshift,3=playshift,4=samplerate,\n"
" 5=auto)\n"
"-a,--slave stream parameters slave mode (0=auto, 1=on, 2=off)\n"
"-T,--thread thread number (-1 = create unique)\n"
"-m,--mixer redirect mixer, argument is:\n"
" SRC_SLAVE_ID(PLAYBACK)@DST_SLAVE_ID(CAPTURE)\n"
"-e,--effect apply an effect (bandpass filter sweep)\n"
"-v,--verbose verbose mode (more -v means more verbose)\n"
);
printf("\nRecognized sample formats are:");
for (k = 0; k < SND_PCM_FORMAT_LAST; ++k) {
const char *s = snd_pcm_format_name(k);
if (s)
printf(" %s", s);
}
printf("\n\n");
printf(
"Tip #1 (usable 500ms latency, good CPU usage, superb xrun prevention):\n"
" alsaloop -t 500000\n"
"Tip #2 (superb 1ms latency, but heavy CPU usage):\n"
" alsaloop -t 1000\n"
);
}
static long timediff(struct timeval t1, struct timeval t2)
{
signed long l;
t1.tv_sec -= t2.tv_sec;
l = (signed long) t1.tv_usec - (signed long) t2.tv_usec;
if (l < 0) {
t1.tv_sec--;
l = 1000000 + l;
l %= 1000000;
}
return (t1.tv_sec * 1000000) + l;
}
static void add_loop(struct loopback *loop)
{
loopbacks = realloc(loopbacks, loopbacks_count * sizeof(struct loopback *));
if (loopbacks == NULL) {
logit(LOG_CRIT, "No enough memory\n");
exit(EXIT_FAILURE);
}
loopbacks[loopbacks_count++] = loop;
}
static int init_mixer_control(struct loopback_control *control,
char *id)
{
int err;
err = snd_ctl_elem_id_malloc(&control->id);
if (err < 0)
return err;
err = snd_ctl_elem_info_malloc(&control->info);
if (err < 0)
return err;
err = snd_ctl_elem_value_malloc(&control->value);
if (err < 0)
return err;
err = control_parse_id(id, control->id);
if (err < 0)
return err;
return 0;
}
static int add_mixers(struct loopback *loop,
char **mixers,
int mixers_count)
{
struct loopback_mixer *mixer, *last = NULL;
char *str1;
int err;
while (mixers_count > 0) {
mixer = calloc(1, sizeof(*mixer));
if (mixer == NULL)
return -ENOMEM;
if (last)
last->next = mixer;
else
loop->controls = mixer;
last = mixer;
str1 = strchr(*mixers, '@');
if (str1)
*str1 = '\0';
err = init_mixer_control(&mixer->src, *mixers);
if (err < 0) {
logit(LOG_CRIT, "Wrong mixer control ID syntax '%s'\n", *mixers);
return -EINVAL;
}
err = init_mixer_control(&mixer->dst, str1 ? str1 + 1 : *mixers);
if (err < 0) {
logit(LOG_CRIT, "Wrong mixer control ID syntax '%s'\n", str1 ? str1 + 1 : *mixers);
return -EINVAL;
}
if (str1)
*str1 = '@';
mixers++;
mixers_count--;
}
return 0;
}
static int parse_config_file(const char *file, snd_output_t *output);
static int parse_config(int argc, char *argv[], snd_output_t *output)
{
struct option long_option[] =
{
{"help", 0, NULL, 'h'},
{"config", 1, NULL, 'g'},
{"daemonize", 0, NULL, 'd'},
{"pdevice", 1, NULL, 'P'},
{"cdevice", 1, NULL, 'C'},
{"latency", 1, NULL, 'l'},
{"tlatency", 1, NULL, 't'},
{"format", 1, NULL, 'f'},
{"channels", 1, NULL, 'c'},
{"rate", 1, NULL, 'r'},
{"buffer", 1, NULL, 'B'},
{"period", 1, NULL, 'E'},
{"seconds", 1, NULL, 's'},
{"nblock", 0, NULL, 'b'},
{"effect", 0, NULL, 'e'},
{"verbose", 0, NULL, 'v'},
{"resample", 0, NULL, 'n'},
{"samplerate", 1, NULL, 'A'},
{"sync", 1, NULL, 'S'},
{"slave", 1, NULL, 'a'},
{"thread", 1, NULL, 'T'},
{"mixer", 1, NULL, 'm'},
{NULL, 0, NULL, 0},
};
int err, morehelp;
char *arg_config = NULL;
char *arg_pdevice = NULL;
char *arg_cdevice = NULL;
unsigned int arg_latency_req = 0;
unsigned int arg_latency_reqtime = 10000;
snd_pcm_format_t arg_format = SND_PCM_FORMAT_S16_LE;
unsigned int arg_channels = 2;
unsigned int arg_rate = 48000;
snd_pcm_uframes_t arg_buffer_size = 0;
snd_pcm_uframes_t arg_period_size = 0;
unsigned long arg_loop_time = ~0UL;
int arg_nblock = 0;
int arg_effect = 0;
int arg_resample = 0;
int arg_samplerate = 0;
int arg_sync = SYNC_TYPE_AUTO;
int arg_slave = SLAVE_TYPE_AUTO;
int arg_thread = 0;
struct loopback *loop = NULL;
char *arg_mixers[MAX_MIXERS];
int arg_mixers_count = 0;
morehelp = 0;
while (1) {
int c;
if ((c = getopt_long(argc, argv, "hdg:P:C:l:t:F:f:c:r:s:benvA:S:a:m:", long_option, NULL)) < 0)
break;
switch (c) {
case 'h':
morehelp++;
break;
case 'g':
arg_config = strdup(optarg);
break;
case 'd':
daemonize = 1;
use_syslog = 1;
openlog("alsaloop", LOG_NDELAY|LOG_PID, LOG_DAEMON);
break;
case 'P':
arg_pdevice = strdup(optarg);
break;
case 'C':
arg_cdevice = strdup(optarg);
break;
case 'l':
err = atoi(optarg);
arg_latency_req = err >= 4 ? err : 4;
break;
case 't':
err = atoi(optarg);
arg_latency_reqtime = err >= 500 ? err : 500;
break;
case 'f':
arg_format = snd_pcm_format_value(optarg);
if (arg_format == SND_PCM_FORMAT_UNKNOWN) {
logit(LOG_WARNING, "Unknown format, setting to default S16_LE\n");
arg_format = SND_PCM_FORMAT_S16_LE;
}
break;
case 'c':
err = atoi(optarg);
arg_channels = err >= 1 && err < 1024 ? err : 1;
break;
case 'r':
err = atoi(optarg);
arg_rate = err >= 4000 && err < 200000 ? err : 44100;
break;
case 'B':
err = atoi(optarg);
arg_buffer_size = err >= 32 && err < 200000 ? err : 0;
break;
case 'E':
err = atoi(optarg);
arg_period_size = err >= 32 && err < 200000 ? err : 0;
break;
case 's':
err = atoi(optarg);
arg_loop_time = err >= 1 && err <= 100000 ? err : 30;
break;
case 'b':
arg_nblock = 1;
break;
case 'e':
arg_effect = 1;
break;
case 'n':
arg_resample = 0;
break;
case 'A':
if (strcasecmp(optarg, "sincbest") == 0)
arg_samplerate = SRC_SINC_BEST_QUALITY;
else if (strcasecmp(optarg, "sincmedium") == 0)
arg_samplerate = SRC_SINC_MEDIUM_QUALITY;
else if (strcasecmp(optarg, "sincfastest") == 0)
arg_samplerate = SRC_SINC_FASTEST;
else if (strcasecmp(optarg, "zerohold") == 0)
arg_samplerate = SRC_ZERO_ORDER_HOLD;
else if (strcasecmp(optarg, "linear") == 0)
arg_samplerate = SRC_LINEAR;
else
arg_samplerate = atoi(optarg);
if (arg_samplerate < 0 || arg_samplerate > SRC_LINEAR)
arg_sync = SRC_SINC_FASTEST;
arg_samplerate += 1;
break;
case 'S':
if (strcasecmp(optarg, "samplerate") == 0)
arg_sync = SYNC_TYPE_SAMPLERATE;
else if (optarg[0] == 'n')
arg_sync = SYNC_TYPE_NONE;
else if (optarg[0] == 's')
arg_sync = SYNC_TYPE_SIMPLE;
else if (optarg[0] == 'c')
arg_sync = SYNC_TYPE_CAPTRATESHIFT;
else if (optarg[0] == 'p')
arg_sync = SYNC_TYPE_PLAYRATESHIFT;
else if (optarg[0] == 'r')
arg_sync = SYNC_TYPE_SAMPLERATE;
else
arg_sync = atoi(optarg);
if (arg_sync < 0 || arg_sync > SYNC_TYPE_LAST)
arg_sync = SYNC_TYPE_AUTO;
break;
case 'a':
if (optarg[0] == 'a')
arg_slave = SLAVE_TYPE_AUTO;
else if (strcasecmp(optarg, "off"))
arg_slave = SLAVE_TYPE_ON;
else if (strcasecmp(optarg, "on"))
arg_slave = SLAVE_TYPE_OFF;
else
arg_slave = atoi(optarg);
if (arg_slave < 0 || arg_slave > SLAVE_TYPE_LAST)
arg_slave = SLAVE_TYPE_AUTO;
break;
case 'T':
arg_thread = atoi(optarg);
if (arg_thread < 0)
arg_thread = 10000000 + loopbacks_count;
break;
case 'm':
if (arg_mixers_count >= MAX_MIXERS) {
logit(LOG_CRIT, "Maximum redirected mixer controls reached (max %i)\n", (int)MAX_MIXERS);
exit(EXIT_FAILURE);
}
arg_mixers[arg_mixers_count++] = optarg;
break;
case 'v':
verbose++;
break;
}
}
if (morehelp) {
help();
exit(EXIT_SUCCESS);
}
if (arg_config == NULL) {
struct loopback_handle *play;
struct loopback_handle *capt;
err = create_loopback_handle(&play, arg_pdevice, "playback");
if (err < 0) {
logit(LOG_CRIT, "Unable to create playback handle.\n");
exit(EXIT_FAILURE);
}
err = create_loopback_handle(&capt, arg_cdevice, "capture");
if (err < 0) {
logit(LOG_CRIT, "Unable to create capture handle.\n");
exit(EXIT_FAILURE);
}
err = create_loopback(&loop, play, capt, output);
if (err < 0) {
logit(LOG_CRIT, "Unable to create loopback handle.\n");
exit(EXIT_FAILURE);
}
play->format = capt->format = arg_format;
play->rate = capt->rate = arg_rate;
play->channels = capt->channels = arg_channels;
play->buffer_size_req = capt->buffer_size_req = arg_buffer_size;
play->period_size_req = capt->period_size_req = arg_period_size;
play->resample = capt->resample = arg_resample;
play->nblock = capt->nblock = arg_nblock ? 1 : 0;
loop->latency_req = arg_latency_req;
loop->latency_reqtime = arg_latency_reqtime;
loop->sync = arg_sync;
loop->slave = arg_slave;
loop->thread = arg_thread;
err = add_mixers(loop, arg_mixers, arg_mixers_count);
if (err < 0) {
logit(LOG_CRIT, "Unable to add mixer controls.\n");
exit(EXIT_FAILURE);
}
#ifdef USE_SAMPLERATE
loop->src_enable = arg_samplerate > 0;
if (loop->src_enable)
loop->src_converter_type = arg_samplerate - 1;
#else
if (arg_samplerate > 0) {
logit(LOG_CRIT, "No libsamplerate support.\n");
exit(EXIT_FAILURE);
}
#endif
set_loop_time(loop, arg_loop_time);
add_loop(loop);
return 0;
}
return parse_config_file(arg_config, output);
}
static int parse_config_file(const char *file, snd_output_t *output)
{
FILE *fp;
char line[2048], word[2048];
char *str, *ptr;
int argc, c, err = 0;
char **argv;
argv = malloc(sizeof(char *) * MAX_ARGS);
if (argv == NULL)
return -ENOMEM;
fp = fopen(file, "r");
if (fp == NULL) {
logit(LOG_CRIT, "Unable to open file '%s': %s\n", file, strerror(errno));
return -EIO;
}
while (!feof(fp)) {
if (fgets(line, sizeof(line)-1, fp) == NULL)
break;
line[sizeof(line)-1] = '\0';
argc = 0;
argv[argc++] = strdup("<prog>");
str = line;
while (*str) {
ptr = word;
while (*str && (*str == ' ' || *str < ' '))
str++;
if (*str == '#')
goto __next;
if (*str == '\'' || *str == '\"') {
c = *str++;
while (*str && *str != c)
*ptr++ = *str++;
if (*str == c)
str++;
} else {
while (*str && *str != ' ' && *str != '\t')
*ptr++ = *str++;
}
if (ptr != word) {
*ptr = '\0';
argv[argc++] = strdup(word);
}
}
/* erase runtime variables for getopt */
optarg = NULL;
optind = opterr = 1;
optopt = 63;
err = parse_config(argc, argv, output);
__next:
while (argc > 0)
free(argv[--argc]);
if (err < 0)
break;
err = 0;
}
fclose(fp);
free(argv);
return err;
}
static void thread_job1(void *_data)
{
struct loopback_thread *thread = _data;
snd_output_t *output = thread->output;
struct pollfd *pfds = NULL;
int pfds_count = 0;
int i, j, err;
setscheduler();
for (i = 0; i < thread->loopbacks_count; i++) {
err = pcmjob_init(thread->loopbacks[i]);
if (err < 0) {
logit(LOG_CRIT, "Loopback initialization failure.\n");
my_exit(thread, EXIT_FAILURE);
}
}
for (i = 0; i < thread->loopbacks_count; i++) {
err = pcmjob_start(thread->loopbacks[i]);
if (err < 0) {
logit(LOG_CRIT, "Loopback start failure.\n");
my_exit(thread, EXIT_FAILURE);
}
pfds_count += thread->loopbacks[i]->pollfd_count;
}
pfds = calloc(pfds_count, sizeof(struct pollfd));
if (pfds == NULL) {
logit(LOG_CRIT, "Poll FDs allocation failed.\n");
my_exit(thread, EXIT_FAILURE);
}
while (1) {
struct timeval tv1, tv2;
for (i = j = 0; i < thread->loopbacks_count; i++) {
err = pcmjob_pollfds_init(thread->loopbacks[i], &pfds[j]);
if (err < 0) {
logit(LOG_CRIT, "Poll FD initialization failed.\n");
my_exit(thread, EXIT_FAILURE);
}
j += err;
}
if (verbose > 10)
gettimeofday(&tv1, NULL);
err = poll(pfds, j, -1);
if (verbose > 10) {
gettimeofday(&tv2, NULL);
snd_output_printf(output, "pool took %lius\n", timediff(tv2, tv1));
}
if (err < 0) {
logit(LOG_CRIT, "Poll failed.\n");
my_exit(thread, EXIT_FAILURE);
}
for (i = j = 0; i < thread->loopbacks_count; i++) {
struct loopback *loop = thread->loopbacks[i];
if (j < loop->active_pollfd_count) {
err = pcmjob_pollfds_handle(loop, &pfds[j]);
if (err < 0) {
logit(LOG_CRIT, "pcmjob failed.\n");
exit(EXIT_FAILURE);
}
}
j += loop->active_pollfd_count;
}
}
my_exit(thread, EXIT_SUCCESS);
}
static void thread_job(struct loopback_thread *thread)
{
if (!thread->threaded) {
thread_job1(thread);
return;
}
pthread_create(&thread->thread, NULL, (void *) &thread_job1,
(void *) thread);
}
int main(int argc, char *argv[])
{
snd_output_t *output;
int i, j, k, l, err;
struct loopback_thread *threads;
err = snd_output_stdio_attach(&output, stdout, 0);
if (err < 0) {
logit(LOG_CRIT, "Output failed: %s\n", snd_strerror(err));
exit(EXIT_FAILURE);
}
err = parse_config(argc, argv, output);
if (err < 0) {
logit(LOG_CRIT, "Unable to parse arguments or configuration...\n");
exit(EXIT_FAILURE);
}
if (loopbacks_count <= 0) {
logit(LOG_CRIT, "No loopback defined...\n");
exit(EXIT_FAILURE);
}
if (daemonize) {
if (daemon(0, 0) < 0) {
logit(LOG_CRIT, "daemon() failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
i = fork();
if (i < 0) {
logit(LOG_CRIT, "fork() failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
if (i > 0) {
/* wait(&i); */
exit(EXIT_SUCCESS);
}
}
/* we must sort thread IDs */
j = 0;
do {
k = 0x7fffffff;
for (i = 0; i < loopbacks_count; i++) {
if (loopbacks[i]->thread < k &&
loopbacks[i]->thread > j)
k = loopbacks[i]->thread;
}
for (i = 0; i < loopbacks_count; i++) {
if (loopbacks[i]->thread == k)
loopbacks[i]->thread = j;
}
j++;
} while (k != 0x7fffffff);
/* fix maximum thread id */
for (i = 0, j = -1; i < loopbacks_count; i++) {
if (loopbacks[i]->thread > j)
j = loopbacks[i]->thread;
}
j += 1;
threads = calloc(1, sizeof(struct loopback_thread) * j);
if (threads == NULL) {
logit(LOG_CRIT, "No enough memory\n");
exit(EXIT_FAILURE);
}
/* sort all threads */
for (k = 0; k < j; k++) {
for (i = l = 0; i < loopbacks_count; i++)
if (loopbacks[i]->thread == k)
l++;
threads[k].loopbacks = malloc(l * sizeof(struct loopback *));
threads[k].loopbacks_count = l;
threads[k].output = output;
threads[k].threaded = j > 1;
for (i = l = 0; i < loopbacks_count; i++)
if (loopbacks[i]->thread == k)
threads[k].loopbacks[l++] = loopbacks[i];
}
for (k = 0; k < j; k++)
thread_job(&threads[k]);
logit(LOG_CRIT, "threads = %i %i\n", j, loopbacks_count);
if (j > 1) {
for (k = 0; k < j; k++)
pthread_join(threads[k].thread, NULL);
}
if (use_syslog)
closelog();
exit(EXIT_SUCCESS);
}