/* ** Vsound - a virtual audio loopback cable used for recording /dev/dsp streams ** Copyright (C) 2004 Nathan Chantrell ** Copyright (C) 2003 Richard Taylor ** Copyright (C) 2000,2001 Erik de Castro Lopo ** Copyright (C) 1999 James Henstridge ** ** Based on the esddsp utility that is part of esound. Esddsp's copyright: ** Copyright (C) 1998, 1999 Manish Singh ** ** This library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Library General Public ** License as published by the Free Software Foundation; either ** version 2 of the License, or (at your option) any later version. ** ** This library 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 ** Library General Public License for more details. ** ** You should have received a copy of the GNU Library General Public ** License along with this library; 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 #ifdef ENABLE_DEBUG #include #define DPRINTF(format, args...) fprintf(stderr, format, ## args) #else #define DPRINTF(format, args...) #endif #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_MACHINE_SOUNDCARD_H #include #else #ifdef HAVE_SOUNDCARD_H #include #else #include #endif #endif /* It seems that Debian Woody (and possibly others) do not define RTLD_NEXT. */ #include #ifndef RTLD_NEXT #define RTLD_NEXT ((void *) -1l) #endif #define REAL_LIBC RTLD_NEXT #ifdef __FreeBSD__ typedef unsigned long request_t; #else typedef int request_t; #endif /*------------------------------------------------------------------------------ ** Macros to handle big/little endian issues. */ #ifdef HAVE_ENDIAN_H /* This is the best way to do it. Unfortunately Sparc Solaris (and ** possibly others) don't have */ #include #if (__BYTE_ORDER == __LITTLE_ENDIAN) #define CPU_IS_LITTLE_ENDIAN 1 #define CPU_IS_BIG_ENDIAN 0 #elif (__BYTE_ORDER == __BIG_ENDIAN) #define CPU_IS_LITTLE_ENDIAN 0 #define CPU_IS_BIG_ENDIAN 1 #else #error "A bit confused about endian-ness! Have but not __BYTEORDER." #endif #else /* If we don't have use the guess based on target processor ** from the autoconf process. */ #if GUESS_LITTLE_ENDIAN #define CPU_IS_LITTLE_ENDIAN 1 #define CPU_IS_BIG_ENDIAN 0 #elif GUESS_BIG_ENDIAN #define CPU_IS_LITTLE_ENDIAN 0 #define CPU_IS_BIG_ENDIAN 1 #else #error "Endian guess seems incorrect." #endif #endif #define ENDSWAP_SHORT(x) ((((x)>>8)&0xFF)|(((x)&0xFF)<<8)) #define ENDSWAP_INT(x) ((((x)>>24)&0xFF)|(((x)>>8)&0xFF00)|(((x)&0xFF00)<<8)|(((x)&0xFF)<<24)) #if (CPU_IS_LITTLE_ENDIAN == 1) #define MAKE_MARKER(a,b,c,d) ((a)|((b)<<8)|((c)<<16)|((d)<<24)) #define H2LE_SHORT(x) (x) #define H2LE_INT(x) (x) #define H2BE_SHORT(x) ENDSWAP_SHORT(x) #define H2BE_INT(x) ENDSWAP_INT(x) #elif (CPU_IS_BIG_ENDIAN == 1) #define MAKE_MARKER(a,b,c,d) (((a)<<24)|((b)<<16)|((c)<<8)|(d)) #define H2LE_SHORT(x) ENDSWAP_SHORT(x) #define H2LE_INT(x) ENDSWAP_INT(x) #define H2BE_SHORT(x) (x) #define H2BE_INT(x) (x) #else #error "Cannot determine endian-ness of processor." #endif #define DOTSND_MARKER (MAKE_MARKER ('.', 's', 'n', 'd')) #define DNSDOT_MARKER (MAKE_MARKER ('d', 'n', 's', '.')) /*---------------------------------------------------------------------------- ** Typedefs and enums. */ typedef struct { int magic ; int dataoffset ; int datasize ; int encoding ; int samplerate ; int channels ; } AU_HEADER ; enum { AU_ENCODING_ULAW_8 = 1, /* 8-bit u-law samples */ AU_ENCODING_PCM_8 = 2, /* 8-bit linear samples */ AU_ENCODING_PCM_16 = 3, /* 16-bit linear samples */ AU_ENCODING_ALAW_8 = 27, /* 8-bit A-law samples */ } ; /*---------------------------------------------------------------------------- ** Function Prototypes. */ static void dsp_init (void) ; static int dspctl (request_t request, void *argp) ; static void fix_header (AU_HEADER *au_header) ; static void endswap_short_array (short* buffer, int count) ; static int au_bytes_per_sample (AU_HEADER *au_header) ; static u_long usec_diff_timeval (struct timeval *start, struct timeval *end) ; static void start_autostop_timer(void); static void stop_autostop_timer(void); /*---------------------------------------------------------------------------- ** Static Data. */ static int filefd = -1, dspfd = -1 ; static int enable_dspout = 0, enable_stdout = 0, enable_timing = 0 ; static int devdsp_format = 0, done_header = 0 ; static int stopdelay = 0, ignore_autostop = 1; static char *datafile = NULL ; static AU_HEADER au_header = { DNSDOT_MARKER, /* magic */ sizeof (AU_HEADER), /* dataoffset */ 0xFFFFFFFF, /* datasize - unknown length. */ AU_ENCODING_PCM_16, /* encoding */ 44100, /* samplerate */ 2 /* channels */ } ; static struct { u_long max_samples ; struct timeval start_time ; struct timeval current_time ; u_long bytes_per_sample ; u_long samples_written ; u_long sample_rate ; } virtual_device ; /*============================================================================ ** Public Functions. */ int open (const char *pathname, int flags, ...) { static int (*func_open) (const char *, int, mode_t) = NULL; va_list args; mode_t mode; int fd ; if (!func_open) func_open = (int (*) (const char *, int, mode_t)) dlsym (REAL_LIBC, "open") ; dsp_init () ; va_start (args, flags) ; mode = va_arg (args, int) ; va_end (args) ; if (strcmp (pathname, "/dev/dsp")) { fd = func_open (pathname, flags, mode) ; /*DPRINTF ("Not /dev/dsp. open (%s) = %d\n", pathname, fd) ;*/ return fd ; } ; stop_autostop_timer(); /* Opening /dev/dsp device so reset done_header. */ done_header = 0 ; au_header.datasize = 0xFFFFFFFF ; /* Set time to zero. */ memset (&virtual_device, 0, sizeof (virtual_device)) ; if (enable_dspout && enable_stdout) { filefd = 1 ; dspfd = func_open (pathname, flags, mode) ; } else if (enable_dspout && ! enable_stdout) { filefd = func_open (datafile, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR) ; dspfd = func_open (pathname, flags, mode) ; } else if (! enable_stdout) filefd = dspfd = func_open (datafile, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR) ; else filefd = dspfd = 1 ; DPRINTF ("open (%s) = (filefd: %d, dspfd = %d)\n", pathname, filefd, dspfd) ; return dspfd ; } /* open */ int ioctl (int fd, request_t request, ...) { static int (*func_ioctl) (int, request_t, void *) = NULL; va_list args; void *argp; if (! func_ioctl) func_ioctl = (int (*) (int, request_t, void *)) dlsym (REAL_LIBC, "ioctl") ; va_start (args, request) ; argp = va_arg (args, void *) ; va_end (args) ; if (fd != dspfd) return func_ioctl (fd, request, argp) ; /* Capture the ioctl() calls to /dev/dsp. */ dspctl (request, argp) ; if (enable_dspout) /* Call the real ioctl() on the/dev/dsp descriptor. */ return func_ioctl (dspfd, request, argp) ; /* The RealPlayer calls this ioctl() and expects sensible results ** back. So, feed it some bullshit it will believe :-). */ if (request == SNDCTL_DSP_GETOSPACE) { audio_buf_info *info ; info = (audio_buf_info*) argp ; info->fragments = 32 ; info->fragstotal = 32 ; info->fragsize = 4096 ; info->bytes = 131072 ; } ; return 0 ; } /* ioctl */ int close (int fd) { static int (*func_close) (int) = NULL; int retval = 0 ; DPRINTF ("close (%d)\n", fd) ; if (! func_close) func_close = (int (*) (int)) dlsym (REAL_LIBC, "close") ; /* If data is going to stdout and the header hasn't been ** written yet, don't close the descriptor. */ if (fd == 1 && filefd == 1 && ! done_header) return 0 ; retval = func_close (fd) ; if (fd == dspfd) { dspfd = -1; filefd = -1; /* Closing /dev/dsp so reset done_header. */ done_header = 0 ; au_header.datasize = 0xFFFFFFFF ; start_autostop_timer(); } ; return retval ; } /* close */ ssize_t write (int fd, const void *buf, size_t count) { static ssize_t (*func_write) (int, const void*, size_t) = NULL ; ssize_t retval = 0 ; if (! func_write) func_write = (ssize_t (*) (int, const void*, size_t)) dlsym (REAL_LIBC, "write") ; if (fd != dspfd) return func_write (fd, buf, count) ; if (! done_header) { fix_header (&au_header) ; /* Byte swap header if required. */ if (enable_dspout) { DPRINTF ("Writing AU header to fd = %d.\n", filefd) ; func_write (filefd, &au_header, sizeof (au_header)) ; } else { DPRINTF ("Writing AU header to fd = %d.\n", dspfd) ; func_write (dspfd, &au_header, sizeof (au_header)) ; } ; fix_header (&au_header) ; /* Reverse effects of earlier byte swap. */ done_header = 1 ; } if (! virtual_device.start_time.tv_sec) { gettimeofday (& (virtual_device.start_time), NULL) ; virtual_device.bytes_per_sample = au_bytes_per_sample (&au_header) ; virtual_device.sample_rate = au_header.samplerate ; /* Number of bytes was stored previously in an ioctl () call. */ virtual_device.max_samples /= virtual_device.bytes_per_sample ; } ; if (enable_dspout) func_write (dspfd, buf, count) ; if (CPU_IS_LITTLE_ENDIAN) endswap_short_array ((short*) buf, count / sizeof (short)) ; retval = func_write (filefd, buf, count) ; if (retval != count) DPRINTF ("write (%d) returns %d\n", filefd, retval) ; /* Count bytes written to the file so that timing can be calculated. */ virtual_device.samples_written += retval / virtual_device.bytes_per_sample ; gettimeofday (& (virtual_device.current_time), NULL) ; if (enable_timing) { u_long diff_time = usec_diff_timeval (&virtual_device.start_time, &virtual_device.current_time) ; u_long usec_sleep = (1000000.0 * virtual_device.samples_written) / virtual_device.sample_rate - diff_time ; DPRINTF ("time = %lu\n", diff_time / 1000000) ; if (usec_sleep > 0 && usec_sleep < 1000000) usleep (usec_sleep) ; } ; return retval ; } /* write */ /*------------------------------------------------------------------------------ ** Static functions. */ static void fix_header (AU_HEADER *pheader) { if (CPU_IS_LITTLE_ENDIAN) { pheader->magic = DOTSND_MARKER ; pheader->dataoffset = H2BE_INT (sizeof (AU_HEADER)) ; pheader->datasize = 0xFFFFFFFF ; /* Unknown length. */ pheader->encoding = H2BE_INT (pheader->encoding) ; pheader->samplerate = H2BE_INT (pheader->samplerate) ; pheader->channels = H2BE_INT (pheader->channels) ; } ; return ; } /* fix_header */ static void endswap_short_array (short* buffer, int count) { int k ; for (k = 0 ; k < count ; k++) buffer [k] = ENDSWAP_SHORT (buffer [k]) ; } /* endswap_short_array */ static void dsp_init (void) { static int inited = 0; char *cptr ; if (inited) return; inited = 1; datafile = getenv ("VSOUND_DATA") ; if (! datafile) datafile = "./vsound.data"; cptr = getenv ("VSOUND_DSPOUT") ; if (cptr) { DPRINTF ("Enabling /dev/dsp output.\n") ; enable_dspout = 1 ; } else DPRINTF ("No /dev/dsp output.\n") ; cptr = getenv ("VSOUND_TIMING") ; if (cptr && ! enable_dspout) { DPRINTF ("Enabling timing delays for streaming data.\n") ; enable_timing = 1 ; } else DPRINTF ("No timing delays.\n") ; cptr = getenv ("VSOUND_STDOUT") ; if (cptr) { DPRINTF ("Enabling output to stdout.\n") ; enable_stdout = 1 ; } else DPRINTF ("Output goes to file.\n") ; cptr = getenv("VSOUND_STOPDELAY"); if (cptr) stopdelay = atoi(cptr); DPRINTF("Autostoping after %d seconds of inactivity\n", stopdelay); } /* dsp_init */ static int dspctl (request_t request, void *argp) { int *arg = (int *) argp; switch (request) { case SNDCTL_DSP_RESET: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_RESET, %p)\n", argp) ; break ; case SNDCTL_DSP_POST: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_POST, %p)\n", argp) ; break ; case SNDCTL_DSP_SETFMT: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_SETFMT, 0x%08X)\n", *((int*) argp)) ; au_header.magic = CPU_IS_BIG_ENDIAN ? DOTSND_MARKER : DNSDOT_MARKER ; switch (*arg) { case AFMT_QUERY: if (CPU_IS_BIG_ENDIAN) *arg = AFMT_S16_BE; /* this is arbitrary */ else *arg = AFMT_S16_LE; /* this is arbitrary */ DPRINTF ("AFMT_QUERY\n") ; au_header.encoding = AU_ENCODING_PCM_16 ; break ; case AFMT_MU_LAW: DPRINTF ("AFMT_MU_LAW\n") ; au_header.encoding = AU_ENCODING_ULAW_8 ; break ; case AFMT_A_LAW: DPRINTF ("AFMT_A_LAW\n") ; au_header.encoding = AU_ENCODING_ALAW_8 ; break ; case AFMT_S8: DPRINTF ("AFMT_S8\n") ; au_header.encoding = AU_ENCODING_PCM_8 ; break ; case AFMT_U8: DPRINTF ("AFMT_U8\n") ; au_header.encoding = 0 ; break ; case AFMT_S16_LE: DPRINTF ("AFMT_S16_LE\n") ; au_header.magic = DNSDOT_MARKER ; au_header.encoding = AU_ENCODING_PCM_16 ; break ; case AFMT_S16_BE: DPRINTF ("AFMT_S16_BE\n") ; au_header.magic = DOTSND_MARKER ; au_header.encoding = AU_ENCODING_PCM_16 ; break ; case AFMT_U16_LE: DPRINTF ("AFMT_U16_LE\n") ; au_header.magic = DNSDOT_MARKER ; au_header.encoding = 0 ; break ; case AFMT_U16_BE: DPRINTF ("AFMT_U16_BE\n") ; au_header.magic = DOTSND_MARKER ; au_header.encoding = 0 ; break ; default: DPRINTF ("Unknown format : %08X\n", *((int*) arg)) ; break ; } devdsp_format = *((int*) arg) ; break ; case SNDCTL_DSP_SPEED: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_SPEED, %d)\n", *((int*) argp)) ; au_header.samplerate = *((int*) argp) ; break ; case SNDCTL_DSP_STEREO: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_STEREO, %p)\n", argp) ; au_header.channels = (*arg) ? 2 : 1 ; break ; case SNDCTL_DSP_CHANNELS: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_CHANNELS, %d)\n", *((int*) argp)) ; au_header.channels = *((int *) arg) ; break ; case SNDCTL_DSP_GETBLKSIZE: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_GETBLKSIZE, %p)\n", argp) ; /* ** DPRINTF ("dsp_getblksize (returning 4k)\n") ; ** *arg = 4 * 1024; */ break ; case SNDCTL_DSP_GETFMTS: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_GETFMTS, %p)\n", argp) ; /* ** DPRINTF ("dsp_getfmts (returning 0x38)\n") ; ** *arg = AFMT_MU_LAW|AFMT_A_LAW|AFMT_U8|AFMT_S16_LE|AFMT_S16_BE| ** AFMT_S8|AFMT_U16_LE|AFMT_U16_BE; */ *arg = AFMT_S16_LE | AFMT_S16_BE ; DPRINTF ("dsp_getfmts (returning %d)\n", *((int*) arg)) ; break ; case SNDCTL_DSP_GETCAPS: DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_GETCAPS, %p)\n", argp) ; /* ** *arg = 0; */ break ; case SNDCTL_DSP_GETOSPACE: { int value ; /* ** audio_buf_info *info ; ** info = (audio_buf_info*) argp ; ** ** DPRINTF (" fragments : %d\n", info->fragments) ; ** DPRINTF (" fragstotal : %d\n", info->fragstotal) ; ** DPRINTF (" fragsize : %d\n", info->fragsize) ; ** DPRINTF (" bytes : %d\n", info->bytes) ; ** ** audio_buf_info *bufinfo = (audio_buf_info *) argp; ** bufinfo->bytes = 4 * 1024; ** */ value = *((int*) argp) ; value = value < 0 ? -value : value ; DPRINTF ("ioctl (/dev/dsp, SNDCTL_DSP_GETOSPACE, %p) = %d (%08X)\n", argp, value, value) ; virtual_device.max_samples = value ; } break ; default: /* DPRINTF ("Unknown ioctl() : %08X\n", request) ; */ break ; } ; return 0; } /* dspctl */ static int au_bytes_per_sample (AU_HEADER *au_header) { int bytes = au_header->channels ; if (au_header->encoding == AU_ENCODING_PCM_16) bytes *= 2 ; DPRINTF ("au_bytes_per_sample : %d\n", bytes) ; return bytes ; } /* au_bytes_per_sample */ static u_long usec_diff_timeval (struct timeval *start, struct timeval *end) { u_long diff ; if (end->tv_sec < start->tv_sec) { DPRINTF ("warning : end->tv_sec < start->tv_sec. Possible wrap around???\n") ; return 0 ; /* Just to be safe. */ } ; diff = (end->tv_sec - start->tv_sec) * 1000000 ; diff += end->tv_usec - start->tv_usec ; return diff ; } /* usec_diff_timeval */ /* Signal handler for SIGQUIT. */ void SIGALRM_handler (int signum) { DPRINTF ("Autostop alarm\n") ; if (!ignore_autostop){ DPRINTF ("Sending sigint\n") ; exit(1); } } static void start_autostop_timer(void) { static int firsttime = 1; static struct itimerval timerval; struct itimerval otimerval; struct sigaction sa; if (firsttime && stopdelay) { sigemptyset (&sa.sa_mask); sa.sa_flags = 0; /* Register the handler for SIGALARM. */ sa.sa_handler = SIGALRM_handler; sigaction (SIGALRM, &sa, 0); timerval.it_interval.tv_sec = (long)stopdelay; timerval.it_interval.tv_usec = 0; timerval.it_value.tv_sec = (long)stopdelay; timerval.it_value.tv_usec = 0; } if (stopdelay) { ignore_autostop = 0; setitimer (ITIMER_REAL, &timerval, &otimerval); DPRINTF ("Watchdog started from -> %ld \n", otimerval.it_value.tv_sec) ; } } /* start_autostop_timer */ static void stop_autostop_timer(void) { static int firsttime = 1; static struct itimerval timerval; struct itimerval otimerval; if (firsttime && stopdelay) { timerval.it_interval.tv_sec = (long)0; timerval.it_interval.tv_usec = 0; timerval.it_value.tv_sec = (long)0; timerval.it_value.tv_usec = 0; } if (stopdelay) { ignore_autostop = 1; setitimer (ITIMER_REAL, &timerval, &otimerval); DPRINTF ("Watchdog stopped at -> %ld \n", otimerval.it_value.tv_sec) ; } } /* stop_autostop_timer */