mirror of
https://github.com/alsa-project/alsa-utils
synced 2024-11-10 00:35:42 +01:00
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>
This commit is contained in:
parent
87c58b59b5
commit
1e75673035
11 changed files with 3189 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -39,6 +39,7 @@ seq/aplaymidi/arecordmidi
|
||||||
seq/aseqdump/aseqdump
|
seq/aseqdump/aseqdump
|
||||||
seq/aseqnet/aseqnet
|
seq/aseqnet/aseqnet
|
||||||
speaker-test/speaker-test
|
speaker-test/speaker-test
|
||||||
|
alsaloop/alsaloop
|
||||||
|
|
||||||
include/aconfig.h*
|
include/aconfig.h*
|
||||||
include/stamp-*
|
include/stamp-*
|
||||||
|
|
|
@ -15,6 +15,9 @@ SUBDIRS += alsaconf
|
||||||
endif
|
endif
|
||||||
if HAVE_PCM
|
if HAVE_PCM
|
||||||
SUBDIRS += aplay iecset speaker-test
|
SUBDIRS += aplay iecset speaker-test
|
||||||
|
if ALSALOOP
|
||||||
|
SUBDIRS += alsaloop
|
||||||
|
endif
|
||||||
endif
|
endif
|
||||||
if HAVE_SEQ
|
if HAVE_SEQ
|
||||||
SUBDIRS += seq
|
SUBDIRS += seq
|
||||||
|
|
13
alsaloop/Makefile.am
Normal file
13
alsaloop/Makefile.am
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
INCLUDES = -I$(top_srcdir)/include
|
||||||
|
LDADD = -lm
|
||||||
|
if HAVE_SAMPLERATE
|
||||||
|
LDADD += -lsamplerate
|
||||||
|
endif
|
||||||
|
# LDFLAGS = -static
|
||||||
|
# CFLAGS += -g -Wall
|
||||||
|
|
||||||
|
bin_PROGRAMS = alsaloop
|
||||||
|
alsaloop_SOURCES = alsaloop.c pcmjob.c control.c
|
||||||
|
noinst_HEADERS = alsaloop.h
|
||||||
|
man_MANS = alsaloop.1
|
||||||
|
EXTRA_DIST = alsaloop.1
|
170
alsaloop/alsaloop.1
Normal file
170
alsaloop/alsaloop.1
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
.TH ALSALOOP 1 "5 Aug 2010"
|
||||||
|
.SH NAME
|
||||||
|
alsaloop \- command-line PCM loopback
|
||||||
|
.SH SYNOPSIS
|
||||||
|
\fBalsaloop\fP [\fI\-option\fP] [\fIcmd\fP]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
|
||||||
|
\fBalsaloop\fP allows create a PCM loopback between a PCM capture device
|
||||||
|
and a PCM playback device.
|
||||||
|
|
||||||
|
\fBalsaloop\fP 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).
|
||||||
|
|
||||||
|
.SH OPTIONS
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-h\fP | \fI\-\-help\fP
|
||||||
|
|
||||||
|
Prints the help information.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-g <file>\fP | \fI\-\-config=<file>\fP
|
||||||
|
|
||||||
|
Use given configuration file. The syntax of this file is simple: one line
|
||||||
|
contains the command line options for one job. The '#' means comment and
|
||||||
|
rest of line is ignored. Example:
|
||||||
|
|
||||||
|
# First line - comment, second line - first job
|
||||||
|
-C hw:1,0 -P hw:0,0 -t 50000 -T 1
|
||||||
|
# Third line - comment, fourth line - second job
|
||||||
|
-C hw:1,1 -P hw:0,1 -t 40000 -T 2
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-d\fP | \fI\-\-daemonize\fP
|
||||||
|
|
||||||
|
Daemonize the main process and use syslog for messages.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-P <device>\fP | \fI\-\-pdevice=<device>\fP
|
||||||
|
|
||||||
|
Use given playback device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-C <device>\fP | \fI\-\-cdevice=<device>\fP
|
||||||
|
|
||||||
|
Use given capture device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-l <latency>\fP | \fI\-\-latency=<frames>\fP
|
||||||
|
|
||||||
|
Requested latency in frames.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-t <usec>\fP | \fI\-\-tlatency=<usec>\fP
|
||||||
|
|
||||||
|
Requested latency in usec (1/1000000sec).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-f <format>\fP | \fI\-\-format=<format>\fP
|
||||||
|
|
||||||
|
Format specification (usually S16_LE S32_LE). Use -h to list all formats.
|
||||||
|
Default format is S16_LE.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-c <channels>\fP | \fI\-\-channels=<channels>\fP
|
||||||
|
|
||||||
|
Channel count specification. Default value is 2.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-c <rate>\fP | \fI\-\-rate=<rate>\fP
|
||||||
|
|
||||||
|
Rate specification. Default value is 48000 (Hz).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-n\fP | \fI\-\-resample\fP
|
||||||
|
|
||||||
|
Allow rate resampling using alsa-lib.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-A <converter>\fP | \fI\-\-samplerate=<converter>\fP
|
||||||
|
|
||||||
|
Use libsamplerate and choose a converter:
|
||||||
|
|
||||||
|
0 or sincbest - best quality
|
||||||
|
1 or sincmedium - medium quality
|
||||||
|
2 or sincfastest - lowest quality
|
||||||
|
3 or zerohold - hold zero samples
|
||||||
|
4 or linear - worst quality - linear resampling
|
||||||
|
5 or auto - choose best method
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-B <size>\fP | \fI\-\-buffer=<size>\fP
|
||||||
|
|
||||||
|
Buffer size in frames.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-E <size>\fP | \fI\-\-period=<size>\fP
|
||||||
|
|
||||||
|
Period size in frames.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-s <secs>\fP | \fI\-\-seconds=<secs>\fP
|
||||||
|
|
||||||
|
Duration of loop in seconds.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-b\fP | \fI\-\-nblock\fP
|
||||||
|
|
||||||
|
Non-block mode (very early process wakeup). Eats more CPU.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-S <mode>\fP | \fI\-\-sync=<mode>\fP
|
||||||
|
|
||||||
|
Sync mode specification for capture to playback stream:
|
||||||
|
0 or none - do not touch the stream
|
||||||
|
1 or simple - add or remove samples to keep
|
||||||
|
both streams synchronized
|
||||||
|
2 or captshift - use driver for the capture device
|
||||||
|
(if supported) to compensate
|
||||||
|
the rate shift
|
||||||
|
3 or playshift - use driver for the playback device
|
||||||
|
(if supported) to compensate
|
||||||
|
the rate shift
|
||||||
|
4 or samplerate - use samplerate library to do rate resampling
|
||||||
|
5 or auto - automatically selects the best method
|
||||||
|
in this order: captshift, playshift,
|
||||||
|
samplerate, simple
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-T <num>\fP | \fI\-\-thread=<num>\fP
|
||||||
|
|
||||||
|
Thread number (-1 means create a unique thread). All jobs with same
|
||||||
|
thread numbers are run within one thread.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-m <mixid>\fP | \fI\-\-mixer=<midid>\fP
|
||||||
|
|
||||||
|
Redirect mixer control from the playback card to the capture card. Format of
|
||||||
|
\fImixid\fP is SRCID(PLAYBACK)[@DSTID(PLAYBACK)]:
|
||||||
|
|
||||||
|
"name='Master Playback Switch'@name='Another Switch'"
|
||||||
|
"name='PCM Playback Volume'"
|
||||||
|
|
||||||
|
Known attributes:
|
||||||
|
|
||||||
|
name - control ID name
|
||||||
|
index - control ID index
|
||||||
|
device - control ID device
|
||||||
|
subdevice - control ID subdevice
|
||||||
|
iface - control ID interface
|
||||||
|
numid - control ID numid
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fI\-v\fP | \fI\-\-verbose\fP
|
||||||
|
|
||||||
|
Verbose mode. Use multiple times to increase verbosity.
|
||||||
|
|
||||||
|
|
||||||
|
.SH EXAMPLES
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fBalsaloop \-C hw:0,0 \-P hw:1,0 \-t 50000\fR
|
||||||
|
|
||||||
|
.SH BUGS
|
||||||
|
None known.
|
||||||
|
.SH AUTHOR
|
||||||
|
\fBalsaloop\fP is by Jaroslav Kysela <perex@perex.cz>.
|
||||||
|
This document is by Jaroslav Kysela <perex@perex.cz>.
|
740
alsaloop/alsaloop.c
Normal file
740
alsaloop/alsaloop.c
Normal file
|
@ -0,0 +1,740 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
181
alsaloop/alsaloop.h
Normal file
181
alsaloop/alsaloop.h
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
* A simple PCM loopback utility
|
||||||
|
* Copyright (c) 2010 by 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 "aconfig.h"
|
||||||
|
#ifdef HAVE_SAMPLERATE_H
|
||||||
|
#define USE_SAMPLERATE
|
||||||
|
#include <samplerate.h>
|
||||||
|
#else
|
||||||
|
enum {
|
||||||
|
SRC_SINC_BEST_QUALITY = 0,
|
||||||
|
SRC_SINC_MEDIUM_QUALITY = 1,
|
||||||
|
SRC_SINC_FASTEST = 2,
|
||||||
|
SRC_ZERO_ORDER_HOLD = 3,
|
||||||
|
SRC_LINEAR = 4
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MAX_ARGS 128
|
||||||
|
#define MAX_MIXERS 64
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
#define FILE_PWRITE "/tmp/alsaloop.praw"
|
||||||
|
#define FILE_CWRITE "/tmp/alsaloop.craw"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum _sync_type {
|
||||||
|
SYNC_TYPE_NONE = 0,
|
||||||
|
SYNC_TYPE_SIMPLE, /* add or remove samples */
|
||||||
|
SYNC_TYPE_CAPTRATESHIFT,
|
||||||
|
SYNC_TYPE_PLAYRATESHIFT,
|
||||||
|
SYNC_TYPE_SAMPLERATE,
|
||||||
|
SYNC_TYPE_AUTO, /* order: CAPTRATESHIFT, PLAYRATESHIFT, */
|
||||||
|
/* SAMPLERATE, SIMPLE */
|
||||||
|
SYNC_TYPE_LAST = SYNC_TYPE_AUTO
|
||||||
|
} sync_type_t;
|
||||||
|
|
||||||
|
typedef enum _slave_type {
|
||||||
|
SLAVE_TYPE_AUTO = 0,
|
||||||
|
SLAVE_TYPE_ON = 1,
|
||||||
|
SLAVE_TYPE_OFF = 2,
|
||||||
|
SLAVE_TYPE_LAST = SLAVE_TYPE_OFF
|
||||||
|
} slave_type_t;
|
||||||
|
|
||||||
|
struct loopback_control {
|
||||||
|
snd_ctl_elem_id_t *id;
|
||||||
|
snd_ctl_elem_info_t *info;
|
||||||
|
snd_ctl_elem_value_t *value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct loopback_mixer {
|
||||||
|
unsigned int skip: 1;
|
||||||
|
struct loopback_control src;
|
||||||
|
struct loopback_control dst;
|
||||||
|
struct loopback_mixer *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct loopback_handle {
|
||||||
|
struct loopback *loopback;
|
||||||
|
char *device;
|
||||||
|
char *id;
|
||||||
|
snd_pcm_t *handle;
|
||||||
|
snd_pcm_access_t access;
|
||||||
|
snd_pcm_format_t format;
|
||||||
|
unsigned int rate;
|
||||||
|
unsigned int channels;
|
||||||
|
unsigned int buffer_size;
|
||||||
|
unsigned int period_size;
|
||||||
|
unsigned int buffer_size_req;
|
||||||
|
unsigned int period_size_req;
|
||||||
|
unsigned int frame_size;
|
||||||
|
unsigned int resample:1; /* do resample */
|
||||||
|
unsigned int nblock:1; /* do block (period size) transfers */
|
||||||
|
unsigned int xrun_pending:1;
|
||||||
|
unsigned int pollfd_count;
|
||||||
|
/* I/O job */
|
||||||
|
char *buf; /* I/O buffer */
|
||||||
|
snd_pcm_uframes_t buf_pos; /* I/O position */
|
||||||
|
snd_pcm_uframes_t buf_count; /* filled samples */
|
||||||
|
snd_pcm_uframes_t buf_size; /* buffer size in frames */
|
||||||
|
snd_pcm_uframes_t buf_over; /* capture buffer overflow */
|
||||||
|
/* statistics */
|
||||||
|
snd_pcm_uframes_t max;
|
||||||
|
unsigned long long counter;
|
||||||
|
unsigned long sync_point; /* in samples */
|
||||||
|
snd_pcm_sframes_t last_delay;
|
||||||
|
/* control */
|
||||||
|
snd_ctl_t *ctl;
|
||||||
|
unsigned int ctl_pollfd_count;
|
||||||
|
snd_ctl_elem_value_t *ctl_notify;
|
||||||
|
snd_ctl_elem_value_t *ctl_rate_shift;
|
||||||
|
snd_ctl_elem_value_t *ctl_active;
|
||||||
|
snd_ctl_elem_value_t *ctl_format;
|
||||||
|
snd_ctl_elem_value_t *ctl_rate;
|
||||||
|
snd_ctl_elem_value_t *ctl_channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct loopback {
|
||||||
|
char *id;
|
||||||
|
struct loopback_handle *capt;
|
||||||
|
struct loopback_handle *play;
|
||||||
|
snd_pcm_uframes_t latency; /* final latency */
|
||||||
|
unsigned int latency_req; /* in frames / 2 */
|
||||||
|
unsigned int latency_reqtime; /* in us / 2 */
|
||||||
|
unsigned long loop_time; /* ~0 = unlimited (in seconds) */
|
||||||
|
unsigned long long loop_limit; /* ~0 = unlimited (in frames) */
|
||||||
|
snd_output_t *output;
|
||||||
|
int pollfd_count;
|
||||||
|
int active_pollfd_count;
|
||||||
|
unsigned int linked:1; /* linked streams */
|
||||||
|
unsigned int reinit:1;
|
||||||
|
unsigned int running:1;
|
||||||
|
sync_type_t sync; /* type of sync */
|
||||||
|
slave_type_t slave;
|
||||||
|
int thread; /* thread number */
|
||||||
|
/* statistics */
|
||||||
|
double pitch;
|
||||||
|
double pitch_delta;
|
||||||
|
snd_pcm_sframes_t pitch_diff;
|
||||||
|
snd_pcm_sframes_t pitch_diff_min;
|
||||||
|
snd_pcm_sframes_t pitch_diff_max;
|
||||||
|
snd_pcm_uframes_t total_queued;
|
||||||
|
unsigned int total_queued_count;
|
||||||
|
snd_timestamp_t tstamp_start;
|
||||||
|
snd_timestamp_t tstamp_end;
|
||||||
|
/* control mixer */
|
||||||
|
struct loopback_mixer *controls;
|
||||||
|
/* sample rate */
|
||||||
|
#ifdef USE_SAMPLERATE
|
||||||
|
unsigned int src_enable: 1;
|
||||||
|
int src_converter_type;
|
||||||
|
SRC_STATE *src_state;
|
||||||
|
SRC_DATA src_data;
|
||||||
|
unsigned int src_out_frames;
|
||||||
|
#endif
|
||||||
|
#ifdef FILE_CWRITE
|
||||||
|
FILE *cfile;
|
||||||
|
#endif
|
||||||
|
#ifdef FILE_PWRITE
|
||||||
|
FILE *pfile;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
extern int verbose;
|
||||||
|
extern int use_syslog;
|
||||||
|
|
||||||
|
#define logit(priority, fmt, args...) do { \
|
||||||
|
if (use_syslog) \
|
||||||
|
syslog(priority, fmt, ##args); \
|
||||||
|
else \
|
||||||
|
fprintf(stderr, fmt, ##args); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int pcmjob_init(struct loopback *loop);
|
||||||
|
int pcmjob_done(struct loopback *loop);
|
||||||
|
int pcmjob_start(struct loopback *loop);
|
||||||
|
int pcmjob_stop(struct loopback *loop);
|
||||||
|
int pcmjob_pollfds_init(struct loopback *loop, struct pollfd *fds);
|
||||||
|
int pcmjob_pollfds_handle(struct loopback *loop, struct pollfd *fds);
|
||||||
|
|
||||||
|
int control_parse_id(const char *str, snd_ctl_elem_id_t *id);
|
||||||
|
int control_id_match(snd_ctl_elem_id_t *id1, snd_ctl_elem_id_t *id2);
|
||||||
|
int control_init(struct loopback *loop);
|
||||||
|
int control_done(struct loopback *loop);
|
||||||
|
int control_event(struct loopback_handle *lhandle, snd_ctl_event_t *ev);
|
376
alsaloop/control.c
Normal file
376
alsaloop/control.c
Normal file
|
@ -0,0 +1,376 @@
|
||||||
|
/*
|
||||||
|
* A simple PCM loopback utility
|
||||||
|
* Copyright (c) 2010 by Jaroslav Kysela <perex@perex.cz>
|
||||||
|
*
|
||||||
|
* 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 <ctype.h>
|
||||||
|
#include <syslog.h>
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#include "alsaloop.h"
|
||||||
|
|
||||||
|
static char *id_str(snd_ctl_elem_id_t *id)
|
||||||
|
{
|
||||||
|
static char str[128];
|
||||||
|
|
||||||
|
sprintf(str, "%i,%s,%i,%i,%s,%i",
|
||||||
|
snd_ctl_elem_id_get_numid(id),
|
||||||
|
snd_ctl_elem_iface_name(snd_ctl_elem_id_get_interface(id)),
|
||||||
|
snd_ctl_elem_id_get_device(id),
|
||||||
|
snd_ctl_elem_id_get_subdevice(id),
|
||||||
|
snd_ctl_elem_id_get_name(id),
|
||||||
|
snd_ctl_elem_id_get_index(id));
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
int control_parse_id(const char *str, snd_ctl_elem_id_t *id)
|
||||||
|
{
|
||||||
|
int c, size, numid;
|
||||||
|
char *ptr;
|
||||||
|
|
||||||
|
while (*str == ' ' || *str == '\t')
|
||||||
|
str++;
|
||||||
|
if (!(*str))
|
||||||
|
return -EINVAL;
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); /* default */
|
||||||
|
while (*str) {
|
||||||
|
if (!strncasecmp(str, "numid=", 6)) {
|
||||||
|
str += 6;
|
||||||
|
numid = atoi(str);
|
||||||
|
if (numid <= 0) {
|
||||||
|
logit(LOG_CRIT, "Invalid numid %d\n", numid);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
snd_ctl_elem_id_set_numid(id, atoi(str));
|
||||||
|
while (isdigit(*str))
|
||||||
|
str++;
|
||||||
|
} else if (!strncasecmp(str, "iface=", 6)) {
|
||||||
|
str += 6;
|
||||||
|
if (!strncasecmp(str, "card", 4)) {
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD);
|
||||||
|
str += 4;
|
||||||
|
} else if (!strncasecmp(str, "mixer", 5)) {
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
|
||||||
|
str += 5;
|
||||||
|
} else if (!strncasecmp(str, "pcm", 3)) {
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM);
|
||||||
|
str += 3;
|
||||||
|
} else if (!strncasecmp(str, "rawmidi", 7)) {
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_RAWMIDI);
|
||||||
|
str += 7;
|
||||||
|
} else if (!strncasecmp(str, "timer", 5)) {
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_TIMER);
|
||||||
|
str += 5;
|
||||||
|
} else if (!strncasecmp(str, "sequencer", 9)) {
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_SEQUENCER);
|
||||||
|
str += 9;
|
||||||
|
} else {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
} else if (!strncasecmp(str, "name=", 5)) {
|
||||||
|
char buf[64];
|
||||||
|
str += 5;
|
||||||
|
ptr = buf;
|
||||||
|
size = 0;
|
||||||
|
if (*str == '\'' || *str == '\"') {
|
||||||
|
c = *str++;
|
||||||
|
while (*str && *str != c) {
|
||||||
|
if (size < (int)sizeof(buf)) {
|
||||||
|
*ptr++ = *str;
|
||||||
|
size++;
|
||||||
|
}
|
||||||
|
str++;
|
||||||
|
}
|
||||||
|
if (*str == c)
|
||||||
|
str++;
|
||||||
|
} else {
|
||||||
|
while (*str && *str != ',') {
|
||||||
|
if (size < (int)sizeof(buf)) {
|
||||||
|
*ptr++ = *str;
|
||||||
|
size++;
|
||||||
|
}
|
||||||
|
str++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*ptr = '\0';
|
||||||
|
snd_ctl_elem_id_set_name(id, buf);
|
||||||
|
} else if (!strncasecmp(str, "index=", 6)) {
|
||||||
|
str += 6;
|
||||||
|
snd_ctl_elem_id_set_index(id, atoi(str));
|
||||||
|
while (isdigit(*str))
|
||||||
|
str++;
|
||||||
|
} else if (!strncasecmp(str, "device=", 7)) {
|
||||||
|
str += 7;
|
||||||
|
snd_ctl_elem_id_set_device(id, atoi(str));
|
||||||
|
while (isdigit(*str))
|
||||||
|
str++;
|
||||||
|
} else if (!strncasecmp(str, "subdevice=", 10)) {
|
||||||
|
str += 10;
|
||||||
|
snd_ctl_elem_id_set_subdevice(id, atoi(str));
|
||||||
|
while (isdigit(*str))
|
||||||
|
str++;
|
||||||
|
}
|
||||||
|
if (*str == ',') {
|
||||||
|
str++;
|
||||||
|
} else {
|
||||||
|
if (*str)
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int control_id_match(snd_ctl_elem_id_t *id1, snd_ctl_elem_id_t *id2)
|
||||||
|
{
|
||||||
|
if (snd_ctl_elem_id_get_interface(id1) !=
|
||||||
|
snd_ctl_elem_id_get_interface(id2))
|
||||||
|
return 0;
|
||||||
|
if (snd_ctl_elem_id_get_device(id1) !=
|
||||||
|
snd_ctl_elem_id_get_device(id2))
|
||||||
|
return 0;
|
||||||
|
if (snd_ctl_elem_id_get_subdevice(id1) !=
|
||||||
|
snd_ctl_elem_id_get_subdevice(id2))
|
||||||
|
return 0;
|
||||||
|
if (strcmp(snd_ctl_elem_id_get_name(id1),
|
||||||
|
snd_ctl_elem_id_get_name(id2)) != 0)
|
||||||
|
return 0;
|
||||||
|
if (snd_ctl_elem_id_get_index(id1) !=
|
||||||
|
snd_ctl_elem_id_get_index(id2))
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int control_init1(struct loopback_handle *lhandle,
|
||||||
|
struct loopback_control *ctl)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
snd_ctl_elem_info_set_id(ctl->info, ctl->id);
|
||||||
|
snd_ctl_elem_value_set_id(ctl->value, ctl->id);
|
||||||
|
err = snd_ctl_elem_info(lhandle->ctl, ctl->info);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_WARNING, "Unable to read control info '%s': %s\n", id_str(ctl->id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
err = snd_ctl_elem_read(lhandle->ctl, ctl->value);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_WARNING, "Unable to read control value (init1) '%s': %s\n", id_str(ctl->id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int copy_value(struct loopback_control *dst,
|
||||||
|
struct loopback_control *src)
|
||||||
|
{
|
||||||
|
snd_ctl_elem_type_t type;
|
||||||
|
unsigned int count;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
type = snd_ctl_elem_info_get_type(dst->info);
|
||||||
|
count = snd_ctl_elem_info_get_count(dst->info);
|
||||||
|
switch (type) {
|
||||||
|
case SND_CTL_ELEM_TYPE_BOOLEAN:
|
||||||
|
for (i = 0; i < count; i++)
|
||||||
|
snd_ctl_elem_value_set_boolean(dst->value,
|
||||||
|
i, snd_ctl_elem_value_get_boolean(src->value, i));
|
||||||
|
break;
|
||||||
|
case SND_CTL_ELEM_TYPE_INTEGER:
|
||||||
|
for (i = 0; i < count; i++) {
|
||||||
|
snd_ctl_elem_value_set_integer(dst->value,
|
||||||
|
i, snd_ctl_elem_value_get_integer(src->value, i));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logit(LOG_CRIT, "Unable to copy control value for type %s\n", snd_ctl_elem_type_name(type));
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int control_init2(struct loopback *loop,
|
||||||
|
struct loopback_mixer *mix)
|
||||||
|
{
|
||||||
|
snd_ctl_elem_type_t type;
|
||||||
|
unsigned int count;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
snd_ctl_elem_info_copy(mix->dst.info, mix->src.info);
|
||||||
|
snd_ctl_elem_info_set_id(mix->dst.info, mix->dst.id);
|
||||||
|
snd_ctl_elem_value_clear(mix->dst.value);
|
||||||
|
snd_ctl_elem_value_set_id(mix->dst.value, mix->dst.id);
|
||||||
|
type = snd_ctl_elem_info_get_type(mix->dst.info);
|
||||||
|
count = snd_ctl_elem_info_get_count(mix->dst.info);
|
||||||
|
snd_ctl_elem_remove(loop->capt->ctl, mix->dst.id);
|
||||||
|
switch (type) {
|
||||||
|
case SND_CTL_ELEM_TYPE_BOOLEAN:
|
||||||
|
err = snd_ctl_elem_add_boolean(loop->capt->ctl,
|
||||||
|
mix->dst.id, count);
|
||||||
|
copy_value(&mix->dst, &mix->src);
|
||||||
|
break;
|
||||||
|
case SND_CTL_ELEM_TYPE_INTEGER:
|
||||||
|
err = snd_ctl_elem_add_integer(loop->capt->ctl,
|
||||||
|
mix->dst.id, count,
|
||||||
|
snd_ctl_elem_info_get_min(mix->dst.info),
|
||||||
|
snd_ctl_elem_info_get_max(mix->dst.info),
|
||||||
|
snd_ctl_elem_info_get_step(mix->dst.info));
|
||||||
|
copy_value(&mix->dst, &mix->src);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logit(LOG_CRIT, "Unable to handle control type %s\n", snd_ctl_elem_type_name(type));
|
||||||
|
err = -EINVAL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to create control '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
err = snd_ctl_elem_unlock(loop->capt->ctl, mix->dst.id);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to unlock control info '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
err = snd_ctl_elem_info(loop->capt->ctl, mix->dst.info);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to read control info '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
if (snd_ctl_elem_info_is_tlv_writable(mix->dst.info)) {
|
||||||
|
unsigned int tlv[64];
|
||||||
|
err = snd_ctl_elem_tlv_read(loop->play->ctl,
|
||||||
|
mix->src.id,
|
||||||
|
tlv, sizeof(tlv));
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to read TLV for '%s': %s\n", id_str(mix->src.id), snd_strerror(err));
|
||||||
|
tlv[0] = tlv[1] = 0;
|
||||||
|
}
|
||||||
|
err = snd_ctl_elem_tlv_write(loop->capt->ctl,
|
||||||
|
mix->dst.id,
|
||||||
|
tlv);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to write TLV for '%s': %s\n", id_str(mix->src.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = snd_ctl_elem_write(loop->capt->ctl, mix->dst.value);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to write control value '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int control_init(struct loopback *loop)
|
||||||
|
{
|
||||||
|
struct loopback_mixer *mix;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
for (mix = loop->controls; mix; mix = mix->next) {
|
||||||
|
err = control_init1(loop->play, &mix->src);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_WARNING, "Disabling playback control '%s'\n", id_str(mix->src.id));
|
||||||
|
mix->skip = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
err = control_init2(loop, mix);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int control_done(struct loopback *loop)
|
||||||
|
{
|
||||||
|
struct loopback_mixer *mix;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if (loop->capt->ctl == NULL)
|
||||||
|
return 0;
|
||||||
|
for (mix = loop->controls; mix; mix = mix->next) {
|
||||||
|
if (mix->skip)
|
||||||
|
continue;
|
||||||
|
err = snd_ctl_elem_remove(loop->capt->ctl, mix->dst.id);
|
||||||
|
if (err < 0)
|
||||||
|
logit(LOG_WARNING, "Unable to remove control '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int control_event1(struct loopback *loop,
|
||||||
|
struct loopback_mixer *mix,
|
||||||
|
snd_ctl_event_t *ev,
|
||||||
|
int capture)
|
||||||
|
{
|
||||||
|
unsigned int mask = snd_ctl_event_elem_get_mask(ev);
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if (mask == SND_CTL_EVENT_MASK_REMOVE)
|
||||||
|
return 0;
|
||||||
|
if ((mask & SND_CTL_EVENT_MASK_VALUE) == 0)
|
||||||
|
return 0;
|
||||||
|
if (!capture) {
|
||||||
|
snd_ctl_elem_value_set_id(mix->src.value, mix->src.id);
|
||||||
|
err = snd_ctl_elem_read(loop->play->ctl, mix->src.value);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to read control value (event1) '%s': %s\n", id_str(mix->src.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
copy_value(&mix->dst, &mix->src);
|
||||||
|
err = snd_ctl_elem_write(loop->capt->ctl, mix->dst.value);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to write control value (event1) '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = snd_ctl_elem_read(loop->capt->ctl, mix->dst.value);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to read control value (event2) '%s': %s\n", id_str(mix->dst.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
copy_value(&mix->src, &mix->dst);
|
||||||
|
err = snd_ctl_elem_write(loop->play->ctl, mix->src.value);
|
||||||
|
if (err < 0) {
|
||||||
|
logit(LOG_CRIT, "Unable to write control value (event2) '%s': %s\n", id_str(mix->src.id), snd_strerror(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int control_event(struct loopback_handle *lhandle, snd_ctl_event_t *ev)
|
||||||
|
{
|
||||||
|
snd_ctl_elem_id_t *id2;
|
||||||
|
struct loopback_mixer *mix;
|
||||||
|
int capt = lhandle == lhandle->loopback->capt;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
snd_ctl_elem_id_alloca(&id2);
|
||||||
|
snd_ctl_event_elem_get_id(ev, id2);
|
||||||
|
for (mix = lhandle->loopback->controls; mix; mix = mix->next) {
|
||||||
|
if (mix->skip)
|
||||||
|
continue;
|
||||||
|
if (control_id_match(id2, capt ? mix->dst.id : mix->src.id)) {
|
||||||
|
err = control_event1(lhandle->loopback, mix, ev, capt);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
128
alsaloop/effect-sweep.c
Normal file
128
alsaloop/effect-sweep.c
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* Bandpass filter sweep effect
|
||||||
|
* Copyright (c) Maarten de Boer <mdeboer@iua.upf.es>
|
||||||
|
*
|
||||||
|
* 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 <math.h>
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
|
||||||
|
struct effect_private {
|
||||||
|
/* filter the sweep variables */
|
||||||
|
float lfo,dlfo,fs,fc,BW,C,D,a0,a1,a2,b1,b2,*x[3],*y[3];
|
||||||
|
float lfo_depth, lfo_center;
|
||||||
|
unsigned int channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int effect_init(struct lookback *loopback,
|
||||||
|
void *private_data,
|
||||||
|
snd_pcm_access_t access,
|
||||||
|
unsigned int channels,
|
||||||
|
unsigned int rate,
|
||||||
|
snd_pcm_format_t format)
|
||||||
|
{
|
||||||
|
struct effect_private *priv = private_data;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
#if __BYTE_ORDER == __LITTLE_ENDIAN
|
||||||
|
if (format != SND_PCM_FORMAT_S16_LE)
|
||||||
|
return -EIO;
|
||||||
|
#elif __BYTE_ORDER == __BIG_ENDIAN
|
||||||
|
if (format != SND_PCM_FORMAT_S16_BE)
|
||||||
|
return -EIO;
|
||||||
|
#else
|
||||||
|
return -EIO;
|
||||||
|
#endif
|
||||||
|
priv->fs = (float) rate;
|
||||||
|
priv->channels = channels;
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
priv->x[i] = calloc(channels * sizeof(float));
|
||||||
|
priv->y[i] = calloc(channels * sizeof(float));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int effect_done(struct loopback *loopback,
|
||||||
|
void *private_data)
|
||||||
|
{
|
||||||
|
struct effect_private *priv = private_data;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
free(priv->x[i]);
|
||||||
|
free(priv->y[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int effect_apply(struct loopback *loopback,
|
||||||
|
void *private_data,
|
||||||
|
const snd_pcm_channel_area_t *areas,
|
||||||
|
snd_uframes_t offset,
|
||||||
|
snd_uframes_t frames)
|
||||||
|
{
|
||||||
|
struct effect_private *priv = private_data;
|
||||||
|
short *samples = (short*)areas[0].addr + offset*priv->channels;
|
||||||
|
snd_uframes_t i;
|
||||||
|
|
||||||
|
for (i=0; i < frames; i++) {
|
||||||
|
int chn;
|
||||||
|
|
||||||
|
fc = sin(priv->lfo)*priv->lfo_depth+priv->lfo_center;
|
||||||
|
priv->lfo += priv->dlfo;
|
||||||
|
if (priv->lfo>2.*M_PI) priv->lfo -= 2.*M_PI;
|
||||||
|
priv->C = 1./tan(M_PI*priv->BW/priv->fs);
|
||||||
|
priv->D = 2.*cos(2*M_PI*fc/fs);
|
||||||
|
priv->a0 = 1./(1.+priv->C);
|
||||||
|
priv->a1 = 0;
|
||||||
|
priv->a2 = -priv->a0;
|
||||||
|
priv->b1 = -priv->C*priv->D*a0;
|
||||||
|
priv->b2 = (priv->C-1)*priv->a0;
|
||||||
|
|
||||||
|
for (chn=0; chn < priv->channels; chn++)
|
||||||
|
{
|
||||||
|
priv->x[chn][2] = priv->x[chn][1];
|
||||||
|
priv->x[chn][1] = priv->x[chn][0];
|
||||||
|
|
||||||
|
priv->y[chn][2] = priv->y[chn][1];
|
||||||
|
priv->y[chn][1] = priv->y[chn][0];
|
||||||
|
|
||||||
|
priv->x[chn][0] = samples[i*channels+chn];
|
||||||
|
priv->y[chn][0] = priv->a0*priv->x[0][chn]
|
||||||
|
+ priv->a1*priv->x[1][chn] + priv->a2*x[2][chn]
|
||||||
|
- priv->b1*priv->y[1][chn] - priv->b2*y[2][chn];
|
||||||
|
samples[i*channels+chn] = priv->y[chn][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void effect_init_sweep(void)
|
||||||
|
{
|
||||||
|
struct effect_private *priv;
|
||||||
|
|
||||||
|
priv = register_effect(effect_init,
|
||||||
|
effect_apply,
|
||||||
|
effect_done,
|
||||||
|
sizeof(struct effectprivate));
|
||||||
|
if (priv) {
|
||||||
|
priv->lfo_center = 2000.;
|
||||||
|
priv->lfo_depth = 1800.;
|
||||||
|
priv->lfo_freq = 0.2;
|
||||||
|
priv->BW = 50;
|
||||||
|
}
|
||||||
|
}
|
1528
alsaloop/pcmjob.c
Normal file
1528
alsaloop/pcmjob.c
Normal file
File diff suppressed because it is too large
Load diff
34
alsaloop/test.sh
Executable file
34
alsaloop/test.sh
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#DBG="gdb --args "
|
||||||
|
#DBG="strace"
|
||||||
|
CFGFILE="/tmp/alsaloop.test.cfg"
|
||||||
|
|
||||||
|
test1() {
|
||||||
|
echo "TEST1"
|
||||||
|
$DBG ./alsaloop -C hw:1,0 -P hw:0,0 \
|
||||||
|
--tlatency 50000 \
|
||||||
|
--mixer "name='Master Playback Volume'@name='Master Playback Volume'" \
|
||||||
|
--mixer "name='Master Playback Switch'@name='Master Playback Switch'" \
|
||||||
|
--mixer "name='PCM Playback Volume'"
|
||||||
|
}
|
||||||
|
|
||||||
|
test2() {
|
||||||
|
echo "TEST2"
|
||||||
|
cat > $CFGFILE <<EOF
|
||||||
|
# first job
|
||||||
|
-C hw:1,0,0 -P hw:0,0,0 --tlatency 50000 --thread 1 \
|
||||||
|
--mixer "name='Master Playback Volume'@name='Master Playback Volume'" \
|
||||||
|
--mixer "name='Master Playback Switch'@name='Master Playback Switch'" \
|
||||||
|
--mixer "name='PCM Playback Volume'"
|
||||||
|
# next line - second job
|
||||||
|
-C hw:1,0,1 -P hw:0,1,0 --tlatency 50000 --thread 2
|
||||||
|
EOF
|
||||||
|
$DBG ./alsaloop -d --config $CFGFILE
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
test1) test1 ;;
|
||||||
|
test2) test2 ;;
|
||||||
|
*) test1 ;;
|
||||||
|
esac
|
16
configure.in
16
configure.in
|
@ -38,11 +38,14 @@ AC_CHECK_HEADERS([alsa/rawmidi.h], [have_rawmidi="yes"], [have_rawmidi="no"],
|
||||||
[#include <alsa/asoundlib.h>])
|
[#include <alsa/asoundlib.h>])
|
||||||
AC_CHECK_HEADERS([alsa/seq.h], [have_seq="yes"], [have_seq="no"],
|
AC_CHECK_HEADERS([alsa/seq.h], [have_seq="yes"], [have_seq="no"],
|
||||||
[#include <alsa/asoundlib.h>])
|
[#include <alsa/asoundlib.h>])
|
||||||
|
AC_CHECK_HEADERS([samplerate.h], [have_samplerate="yes"], [have_samplerate="no"],
|
||||||
|
[#include <samplerate.h>])
|
||||||
|
|
||||||
AM_CONDITIONAL(HAVE_PCM, test "$have_pcm" = "yes")
|
AM_CONDITIONAL(HAVE_PCM, test "$have_pcm" = "yes")
|
||||||
AM_CONDITIONAL(HAVE_MIXER, test "$have_mixer" = "yes")
|
AM_CONDITIONAL(HAVE_MIXER, test "$have_mixer" = "yes")
|
||||||
AM_CONDITIONAL(HAVE_RAWMIDI, test "$have_rawmidi" = "yes")
|
AM_CONDITIONAL(HAVE_RAWMIDI, test "$have_rawmidi" = "yes")
|
||||||
AM_CONDITIONAL(HAVE_SEQ, test "$have_seq" = "yes")
|
AM_CONDITIONAL(HAVE_SEQ, test "$have_seq" = "yes")
|
||||||
|
AM_CONDITIONAL(HAVE_SAMPLERATE, test "$have_samplerate" = "yes")
|
||||||
|
|
||||||
dnl Check for librt
|
dnl Check for librt
|
||||||
LIBRT=""
|
LIBRT=""
|
||||||
|
@ -87,6 +90,16 @@ AC_ARG_ENABLE(alsaconf,
|
||||||
esac],[alsaconf=true])
|
esac],[alsaconf=true])
|
||||||
AM_CONDITIONAL(ALSACONF, test x$alsaconf = xtrue)
|
AM_CONDITIONAL(ALSACONF, test x$alsaconf = xtrue)
|
||||||
|
|
||||||
|
dnl Disable alsaloop
|
||||||
|
AC_ARG_ENABLE(alsaloop,
|
||||||
|
[ --disable-alsaloop Disable alsaloop packaging],
|
||||||
|
[case "${enableval}" in
|
||||||
|
yes) alsaloop=true ;;
|
||||||
|
no) alsaloop=false ;;
|
||||||
|
*) AC_MSG_ERROR(bad value ${enableval} for --enable-alsaloop) ;;
|
||||||
|
esac],[alsaloop=true])
|
||||||
|
AM_CONDITIONAL(ALSALOOP, test x$alsaloop = xtrue)
|
||||||
|
|
||||||
xmlto=""
|
xmlto=""
|
||||||
if test x"$alsaconf" = xtrue; then
|
if test x"$alsaconf" = xtrue; then
|
||||||
AC_ARG_ENABLE(xmlto,
|
AC_ARG_ENABLE(xmlto,
|
||||||
|
@ -273,4 +286,5 @@ AC_OUTPUT(Makefile alsactl/Makefile alsactl/init/Makefile \
|
||||||
aplay/Makefile include/Makefile iecset/Makefile utils/Makefile \
|
aplay/Makefile include/Makefile iecset/Makefile utils/Makefile \
|
||||||
utils/alsa-utils.spec seq/Makefile seq/aconnect/Makefile \
|
utils/alsa-utils.spec seq/Makefile seq/aconnect/Makefile \
|
||||||
seq/aplaymidi/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \
|
seq/aplaymidi/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \
|
||||||
speaker-test/Makefile speaker-test/samples/Makefile)
|
speaker-test/Makefile speaker-test/samples/Makefile \
|
||||||
|
alsaloop/Makefile)
|
||||||
|
|
Loading…
Reference in a new issue