/* * A simple PCM loopback utility with adaptive sample rate support * * Author: Jaroslav Kysela * * * 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 #include #include #include #include #include #include #include #include #include #include #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 = NULL; int loopbacks_count = 0; char **my_argv = NULL; int my_argc = 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 = handle->rate_req = 48000; handle->channels = 2; handle->resample = 0; *_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" "-O,--ossmixer rescan and redirect oss mixer, argument is:\n" " ALSA_ID@OSS_ID (for example: \"Master@VOLUME\")\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 + 1) * 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 add_oss_mixers(struct loopback *loop, char **mixers, int mixers_count) { struct loopback_ossmixer *mixer, *last = NULL; char *str1, *str2; while (mixers_count > 0) { mixer = calloc(1, sizeof(*mixer)); if (mixer == NULL) return -ENOMEM; if (last) last->next = mixer; else loop->oss_controls = mixer; last = mixer; str1 = strchr(*mixers, ','); if (str1) *str1 = '\0'; str2 = strchr(str1 ? str1 + 1 : *mixers, '@'); if (str2) *str2 = '\0'; mixer->alsa_id = strdup(*mixers); if (str1) mixer->alsa_index = atoi(str1); mixer->oss_id = strdup(str2 ? str2 + 1 : *mixers); if (mixer->alsa_id == NULL || mixer->oss_id == NULL) { logit(LOG_CRIT, "Not enough memory"); return -ENOMEM; } if (str1) *str1 = ','; if (str2) *str2 = ','; 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'}, {"ossmixer", 1, NULL, 'O'}, {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 = SRC_SINC_FASTEST + 1; 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; char *arg_ossmixers[MAX_MIXERS]; int arg_ossmixers_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:T:O:", 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 = 1; 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, "on") == 0) arg_slave = SLAVE_TYPE_ON; else if (strcasecmp(optarg, "off") == 0) 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 'O': if (arg_ossmixers_count >= MAX_MIXERS) { logit(LOG_CRIT, "Maximum redirected mixer controls reached (max %i)\n", (int)MAX_MIXERS); exit(EXIT_FAILURE); } arg_ossmixers[arg_ossmixers_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 = play->rate_req = capt->rate = capt->rate_req = 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); } err = add_oss_mixers(loop, arg_ossmixers, arg_ossmixers_count); if (err < 0) { logit(LOG_CRIT, "Unable to add ossmixer 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; 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'; my_argv = realloc(my_argv, my_argc + MAX_ARGS * sizeof(char *)); if (my_argv == NULL) return -ENOMEM; argv = my_argv + my_argc; argc = 0; argv[argc++] = strdup(""); my_argc++; 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) { if (*(ptr-1) == '\n') ptr--; *ptr = '\0'; if (argc >= MAX_ARGS) { logit(LOG_CRIT, "Too many arguments."); goto __error; } argv[argc++] = strdup(word); my_argc++; } } /* erase runtime variables for getopt */ optarg = NULL; optind = opterr = 1; optopt = '?'; err = parse_config(argc, argv, output); __next: if (err < 0) break; err = 0; } __error: fclose(fp); 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 || pfds_count <= 0) { 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); } while (my_argc > 0) free(my_argv[--my_argc]); free(my_argv); 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 = -1; do { k = 0x7fffffff; for (i = 0; i < loopbacks_count; i++) { if (loopbacks[i]->thread < k && loopbacks[i]->thread > j) k = loopbacks[i]->thread; } j++; for (i = 0; i < loopbacks_count; i++) { if (loopbacks[i]->thread == k) loopbacks[i]->thread = 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]); if (j > 1) { for (k = 0; k < j; k++) pthread_join(threads[k].thread, NULL); } if (use_syslog) closelog(); exit(EXIT_SUCCESS); }