/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2009 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* * Copyright (c) 2018, Joyent, Inc. */ /* Command-line audio record utility */ #include #include #include #include #include #include #include #include #include #include #include #include #include /* All occurances of INT_MAX used to be ~0 (by MCA) */ #include #include #include #include #include #include #include #include #include #include #define irint(d) ((int)d) /* localization stuff */ #define MGET(s) (char *)gettext(s) #if !defined(TEXT_DOMAIN) /* Should be defined by cc -D */ #define TEXT_DOMAIN "SYS_TEST" /* Use this only if it weren't */ #endif #define Error (void) fprintf /* Local variables */ static char *prog; static char prog_opts[] = "aft:v:d:i:e:s:c:T:?"; /* getopt() flags */ static char *Stdout; /* XXX - the input buffer size should depend on sample_rate */ #define AUDIO_BUFSIZ (1024 * 64) static unsigned char buf[AUDIO_BUFSIZ]; static char swapBuf[AUDIO_BUFSIZ]; /* for byte swapping */ #define MAX_GAIN (100) /* maximum gain */ static char *Info = NULL; /* pointer to info data */ static unsigned Ilen = 0; /* length of info data */ static unsigned Volume = INT_MAX; /* record volume */ static double Savevol; /* saved volume */ static unsigned Sample_rate = 0; static unsigned Channels = 0; static unsigned Precision = 0; /* based on encoding */ static unsigned Encoding = 0; static int NetEndian = TRUE; /* endian nature of the machines */ static int Append = FALSE; /* append to output file */ static int Force = FALSE; /* ignore rate differences on append */ static double Time = -1.; /* recording time */ static unsigned Limit = AUDIO_UNKNOWN_SIZE; /* recording limit */ static char *Audio_dev = "/dev/audio"; static int Audio_fd = -1; /* file descriptor for audio device */ static Audio_hdr Dev_hdr; /* audio header for device */ static Audio_hdr Save_hdr; /* saved audio device header */ static char *Ofile; /* current filename */ static int File_type = FILE_AU; /* audio file type */ static int File_type_set = FALSE; /* file type specified as arg */ static Audio_hdr File_hdr; /* audio header for file */ static int Cleanup = FALSE; /* SIGINT sets this flag */ static unsigned Size = 0; /* Size of output file */ static unsigned Oldsize = 0; /* Size of input file, if append */ /* Global variables */ extern int getopt(); extern int optind; extern char *optarg; /* Local Functions */ static void usage(void); static void sigint(int sig); static int parse_unsigned(char *str, unsigned *dst, char *flag); static int parse_sample_rate(char *s, unsigned *rate); static void usage(void) { Error(stderr, MGET("Record an audio file -- usage:\n" "\t%s [-af] [-v vol]\n" "\t%.*s [-c channels] [-s rate] [-e encoding]\n" "\t%.*s [-t time] [-i info] [-d dev] [-T au|wav|aif[f]] [file]\n" "where:\n" "\t-a\tAppend to output file\n" "\t-f\tIgnore sample rate differences on append\n" "\t-v\tSet record volume (0 - %d)\n" "\t-c\tSpecify number of channels to record\n" "\t-s\tSpecify rate in samples per second\n" "\t-e\tSpecify encoding (ulaw | alaw | [u]linear | linear8 )\n" "\t-t\tSpecify record time (hh:mm:ss.dd)\n" "\t-i\tSpecify a file header information string\n" "\t-d\tSpecify audio device (default: /dev/audio)\n" "\t-T\tSpecify the audio file type (default: au)\n" "\tfile\tRecord to named file\n" "\t\tIf no file specified, write to stdout\n" "\t\tDefault audio encoding is ulaw, 8khz, mono\n" "\t\tIf -t is not specified, record until ^C\n"), prog, strlen(prog), " ", strlen(prog), " ", MAX_GAIN); exit(1); } static void sigint(int sig) { /* If this is the first ^C, set a flag for the main loop */ if (!Cleanup && (Audio_fd >= 0)) { /* flush input queues before exiting */ Cleanup = TRUE; if (audio_pause_record(Audio_fd) == AUDIO_SUCCESS) return; Error(stderr, MGET("%s: could not flush input buffer\n"), prog); } /* If double ^C, really quit */ if (Audio_fd >= 0) { if (Volume != INT_MAX) (void) audio_set_record_gain(Audio_fd, &Savevol); if (audio_cmp_hdr(&Save_hdr, &Dev_hdr) != 0) { (void) audio_set_record_config(Audio_fd, &Save_hdr); } } exit(1); } /* * Record from the audio device to a file. */ int main(int argc, char **argv) { int i; int cnt; int err; int file_type; int ofd; int swapBytes = FALSE; double vol; struct stat st; struct pollfd pfd; char *cp; (void) setlocale(LC_ALL, ""); (void) textdomain(TEXT_DOMAIN); /* Get the program name */ prog = strrchr(argv[0], '/'); if (prog == NULL) prog = argv[0]; else prog++; Stdout = MGET("(stdout)"); /* first check AUDIODEV environment for audio device name */ if (cp = getenv("AUDIODEV")) { Audio_dev = cp; } /* Set the endian nature of the machine */ if ((ulong_t)1 != htonl((ulong_t)1)) { NetEndian = FALSE; } err = 0; while ((i = getopt(argc, argv, prog_opts)) != EOF) { switch (i) { case 'v': if (parse_unsigned(optarg, &Volume, "-v")) { err++; } else if (Volume > MAX_GAIN) { Error(stderr, MGET("%s: invalid value for " "-v\n"), prog); err++; } break; case 't': Time = audio_str_to_secs(optarg); if ((Time == HUGE_VAL) || (Time < 0.)) { Error(stderr, MGET("%s: invalid value for " "-t\n"), prog); err++; } break; case 'd': Audio_dev = optarg; break; case 'f': Force = TRUE; break; case 'a': Append = TRUE; break; case 'i': Info = optarg; /* set information string */ Ilen = strlen(Info); break; case 's': if (parse_sample_rate(optarg, &Sample_rate)) { err++; } break; case 'c': if (strncmp(optarg, "mono", strlen(optarg)) == 0) { Channels = 1; } else if (strncmp(optarg, "stereo", strlen(optarg)) == 0) { Channels = 2; } else if (parse_unsigned(optarg, &Channels, "-c")) { err++; } else if ((Channels != 1) && (Channels != 2)) { Error(stderr, "%s: invalid value for -c\n", prog); err++; } break; case 'e': if (strncmp(optarg, "ulinear", strlen(optarg)) == 0) { Encoding = AUDIO_ENCODING_LINEAR8; Precision = 8; } else if (strncmp(optarg, "linear8", strlen("linear8")) == 0) { Encoding = AUDIO_ENCODING_LINEAR; Precision = 8; } else if (strncmp(optarg, "ulaw", strlen(optarg)) == 0) { Encoding = AUDIO_ENCODING_ULAW; Precision = 8; } else if (strncmp(optarg, "alaw", strlen(optarg)) == 0) { Encoding = AUDIO_ENCODING_ALAW; Precision = 8; } else if ((strncmp(optarg, "linear", strlen(optarg)) == 0) || (strncmp(optarg, "pcm", strlen(optarg)) == 0)) { Encoding = AUDIO_ENCODING_LINEAR; Precision = 16; } else { Error(stderr, MGET("%s: invalid value for " "-e\n"), prog); err++; } break; case 'T': if (strncmp(optarg, "au", strlen(optarg)) == 0) { File_type = FILE_AU; } else if (strncmp(optarg, "wav", strlen(optarg)) == 0) { File_type = FILE_WAV; } else if (strncmp(optarg, "aif", strlen(optarg)) == 0) { File_type = FILE_AIFF; } else if (strncmp(optarg, "aiff", strlen(optarg)) == 0) { File_type = FILE_AIFF; } else { Error(stderr, MGET("%s: invalid value for " "-T\n"), prog); err++; } File_type_set = TRUE; break; case '?': usage(); /*NOTREACHED*/ } } if (Append && (Info != NULL)) { Error(stderr, MGET("%s: cannot specify -a and -i\n"), prog); err++; } if (err > 0) exit(1); argc -= optind; /* update arg pointers */ argv += optind; /* Open the output file */ if (argc <= 0) { Ofile = Stdout; } else { Ofile = *argv++; argc--; /* Interpret "-" filename to mean stdout */ if (strcmp(Ofile, "-") == 0) Ofile = Stdout; /* if -T not set then we use the file suffix */ if (File_type_set == FALSE) { char *file_name; char *start; /* get the file name without the path */ file_name = basename(Ofile); /* get the true suffix */ start = strrchr(file_name, '.'); /* if no '.' then there's no suffix */ if (start) { /* is this a .au file? */ if (strcasecmp(start, ".au") == 0) { File_type = FILE_AU; } else if (strcasecmp(start, ".wav") == 0) { File_type = FILE_WAV; } else if (strcasecmp(start, ".aif") == 0) { File_type = FILE_AIFF; } else if (strcasecmp(start, ".aiff") == 0) { File_type = FILE_AIFF; } else { /* the default is .au */ File_type = FILE_AU; } } else { /* no suffix, so default to .au */ File_type = FILE_AU; } } } if (Ofile == Stdout) { ofd = fileno(stdout); Append = FALSE; } else { ofd = open(Ofile, (O_RDWR | O_CREAT | (Append ? 0 : O_TRUNC)), 0666); if (ofd < 0) { Error(stderr, MGET("%s: cannot open "), prog); perror(Ofile); exit(1); } if (Append) { /* * Check to make sure we're appending to an audio file. * It must be a regular file (if zero-length, simply * write it from scratch). Also, its file header * must match the input device configuration. */ if ((fstat(ofd, &st) < 0) || (!S_ISREG(st.st_mode))) { Error(stderr, MGET("%s: %s is not a regular file\n"), prog, Ofile); exit(1); } if (st.st_size == 0) { Append = FALSE; goto openinput; } err = audio_read_filehdr(ofd, &File_hdr, &file_type, (char *)NULL, 0); if (err != AUDIO_SUCCESS) { Error(stderr, MGET("%s: %s is not a valid audio file\n"), prog, Ofile); exit(1); } /* we need to make sure file types match */ if (File_type_set == TRUE) { /* specified by the command line, must match */ if (File_type != file_type) { Error(stderr, MGET("%s: file types must match\n"), prog); exit(1); } } else { /* not specified, so force */ File_type = file_type; } /* * Set the format state to the format * in the file header. */ Sample_rate = File_hdr.sample_rate; Channels = File_hdr.channels; Encoding = File_hdr.encoding; Precision = File_hdr.bytes_per_unit * 8; /* make sure we support the encoding method */ switch (Encoding) { case AUDIO_ENCODING_LINEAR8: case AUDIO_ENCODING_ULAW: case AUDIO_ENCODING_ALAW: case AUDIO_ENCODING_LINEAR: break; default: { char msg[AUDIO_MAX_ENCODE_INFO]; (void) audio_enc_to_str(&File_hdr, msg); Error(stderr, MGET("%s: Append is not supported " "for "), prog); Error(stderr, MGET("this file encoding:\n\t" "[%s]\n"), msg); exit(1); } } /* Get the current size, if possible */ Oldsize = File_hdr.data_size; if ((Oldsize == AUDIO_UNKNOWN_SIZE) && ((err = (int)lseek(ofd, 0L, SEEK_CUR)) >= 0)) { if (err < 0) { Error(stderr, MGET("%s: %s is not a valid audio " "file\n"), prog, Ofile); exit(1); } Oldsize = st.st_size - err; } /* Seek to end to start append */ if ((int)lseek(ofd, st.st_size, SEEK_SET) < 0) { Error(stderr, MGET("%s: cannot find end of %s\n"), prog, Ofile); exit(1); } } } openinput: /* Validate and open the audio device */ err = stat(Audio_dev, &st); if (err < 0) { Error(stderr, MGET("%s: cannot open "), prog); perror(Audio_dev); exit(1); } if (!S_ISCHR(st.st_mode)) { Error(stderr, MGET("%s: %s is not an audio device\n"), prog, Audio_dev); exit(1); } /* * For the mixer environment we need to open the audio device before * the control device. If successful we pause right away to keep * from queueing up a bunch of useless data. */ Audio_fd = open(Audio_dev, O_RDONLY | O_NONBLOCK); if (Audio_fd < 0) { if (errno == EBUSY) { Error(stderr, MGET("%s: %s is busy\n"), prog, Audio_dev); } else { Error(stderr, MGET("%s: error opening "), prog); perror(Audio_dev); } exit(1); } if (audio_pause_record(Audio_fd) != AUDIO_SUCCESS) { Error(stderr, MGET("%s: not able to pause recording\n"), prog); exit(1); } /* get the current settings */ if (audio_get_record_config(Audio_fd, &Save_hdr) != AUDIO_SUCCESS) { (void) close(Audio_fd); Error(stderr, MGET("%s: %s is not an audio device\n"), prog, Audio_dev); exit(1); } /* make a copy into the working data structure */ bcopy(&Save_hdr, &Dev_hdr, sizeof (Save_hdr)); /* flush any queued audio data */ if (audio_flush_record(Audio_fd) != AUDIO_SUCCESS) { Error(stderr, MGET("%s: not able to flush recording\n"), prog); exit(1); } if (Sample_rate != 0) { Dev_hdr.sample_rate = Sample_rate; } if (Channels != 0) { Dev_hdr.channels = Channels; } if (Precision != 0) { Dev_hdr.bytes_per_unit = Precision / 8; } if (Encoding != 0) { Dev_hdr.encoding = Encoding; } /* * For .wav we always record 8-bit linear as unsigned. Thus we * force unsigned linear to make life a lot easier on the user. * * For .aiff we set the default to 8-bit signed linear, not * u-law, if Encoding isn't already set. */ if (File_type == FILE_WAV && Dev_hdr.encoding == AUDIO_ENCODING_LINEAR && Dev_hdr.bytes_per_unit == 1) { /* force to unsigned */ Dev_hdr.encoding = AUDIO_ENCODING_LINEAR8; } else if (File_type == FILE_AIFF && Encoding == 0) { Dev_hdr.encoding = AUDIO_ENCODING_LINEAR; if (Precision == 0) { Dev_hdr.bytes_per_unit = AUDIO_PRECISION_8 / 8; } } if (audio_set_record_config(Audio_fd, &Dev_hdr) != AUDIO_SUCCESS) { Error(stderr, MGET( "%s: Audio format not supported by the audio device\n"), prog); exit(1); } if (audio_resume_record(Audio_fd) != AUDIO_SUCCESS) { Error(stderr, MGET("%s: not able to resume recording\n"), prog); exit(1); } /* If appending to an existing file, check the configuration */ if (Append) { char msg[AUDIO_MAX_ENCODE_INFO]; switch (audio_cmp_hdr(&Dev_hdr, &File_hdr)) { case 0: /* configuration matches */ break; case 1: /* all but sample rate matches */ if (Force) { Error(stderr, MGET("%s: WARNING: appending " "%.3fkHz data to %s (%.3fkHz)\n"), prog, ((double)Dev_hdr.sample_rate / 1000.), Ofile, ((double)File_hdr.sample_rate / 1000.)); break; } /* if not -f, fall through */ /* FALLTHROUGH */ default: /* encoding mismatch */ (void) audio_enc_to_str(&Dev_hdr, msg); Error(stderr, MGET("%s: device encoding [%s]\n"), prog, msg); (void) audio_enc_to_str(&File_hdr, msg); Error(stderr, MGET("\tdoes not match file encoding [%s]\n"), msg); exit(1); } } else if (!isatty(ofd)) { if (audio_write_filehdr(ofd, &Dev_hdr, File_type, Info, Ilen) != AUDIO_SUCCESS) { Error(stderr, MGET("%s: error writing header for %s\n"), prog, Ofile); exit(1); } } /* * 8-bit audio isn't a problem, however 16-bit audio is. If the file * is an endian that is different from the machine then the bytes * will need to be swapped. * * Note: The following if() could be simplified, but then it gets * to be very hard to read. So it's left as is. */ if (Dev_hdr.bytes_per_unit == 2 && ((!NetEndian && File_type == FILE_AIFF) || (!NetEndian && File_type == FILE_AU) || (NetEndian && File_type == FILE_WAV))) { swapBytes = TRUE; } /* If -v flag, set the record volume now */ if (Volume != INT_MAX) { vol = (double)Volume / (double)MAX_GAIN; (void) audio_get_record_gain(Audio_fd, &Savevol); err = audio_set_record_gain(Audio_fd, &vol); if (err != AUDIO_SUCCESS) { Error(stderr, MGET("%s: could not set record volume for %s\n"), prog, Audio_dev); exit(1); } } if (isatty(ofd)) { Error(stderr, MGET("%s: No files and stdout is a tty\n"), prog); exit(1); } /* Set up SIGINT handler so that final buffers may be flushed */ (void) signal(SIGINT, sigint); /* * At this point, we're (finally) ready to copy the data. * Init a poll() structure, to use when there's nothing to read. */ if (Time > 0) Limit = audio_secs_to_bytes(&Dev_hdr, Time); pfd.fd = Audio_fd; pfd.events = POLLIN; while ((Limit == AUDIO_UNKNOWN_SIZE) || (Limit != 0)) { /* Fill the buffer or read to the time limit */ cnt = read(Audio_fd, (char *)buf, ((Limit != AUDIO_UNKNOWN_SIZE) && (Limit < sizeof (buf)) ? (int)Limit : sizeof (buf))); if (cnt == 0) /* normally, eof can't happen */ break; /* If error, probably have to wait for input */ if (cnt < 0) { if (Cleanup) break; /* done if ^C seen */ switch (errno) { case EAGAIN: (void) poll(&pfd, 1L, -1); break; case EOVERFLOW: /* Possibly a Large File */ Error(stderr, MGET("%s: error reading"), prog); perror("Large File"); exit(1); default: Error(stderr, MGET("%s: error reading"), prog); perror(Audio_dev); exit(1); } continue; } /* Swab the output if required. */ if (swapBytes) { swab((char *)buf, swapBuf, cnt); err = write(ofd, swapBuf, cnt); } else { err = write(ofd, (char *)buf, cnt); } if (err < 0) { Error(stderr, MGET("%s: error writing "), prog); perror(Ofile); exit(1); } if (err != cnt) { Error(stderr, MGET("%s: error writing "), prog); perror(Ofile); break; } Size += cnt; if (Limit != AUDIO_UNKNOWN_SIZE) Limit -= cnt; } /* Attempt to rewrite the data_size field of the file header */ if (!Append || (Oldsize != AUDIO_UNKNOWN_SIZE)) { if (Append) Size += Oldsize; (void) audio_rewrite_filesize(ofd, File_type, Size, Dev_hdr.channels, Dev_hdr.bytes_per_unit); } (void) close(ofd); /* close input file */ /* Check for error during record */ if (audio_get_record_error(Audio_fd, (unsigned *)&err) != AUDIO_SUCCESS) Error(stderr, MGET("%s: error reading device status\n"), prog); else if (err) Error(stderr, MGET("%s: WARNING: Data overflow occurred\n"), prog); /* Reset record volume, encoding */ if (Volume != INT_MAX) (void) audio_set_record_gain(Audio_fd, &Savevol); if (audio_cmp_hdr(&Save_hdr, &Dev_hdr) != 0) { (void) audio_set_record_config(Audio_fd, &Save_hdr); } (void) close(Audio_fd); return (0); } /* Parse an unsigned integer */ static int parse_unsigned(char *str, unsigned *dst, char *flag) { char x; if (sscanf(str, "%u%c", dst, &x) != 1) { Error(stderr, MGET("%s: invalid value for %s\n"), prog, flag); return (1); } return (0); } /* * set the sample rate. assume anything is ok. check later on to make sure * the sample rate is valid. */ static int parse_sample_rate(char *s, unsigned *rate) { char *cp; double drate; /* * check if it's "cd" or "dat" or "voice". these also set * the precision and encoding, etc. */ if (strcasecmp(s, "dat") == 0) { drate = 48000.0; } else if (strcasecmp(s, "cd") == 0) { drate = 44100.0; } else if (strcasecmp(s, "voice") == 0) { drate = 8000.0; } else { /* just do an atof */ drate = atof(s); /* * if the first non-digit is a "k" multiply by 1000, * if it's an "h", leave it alone. anything else, * return an error. */ /* * XXX bug alert: could have multiple "." in string * and mess things up. */ for (cp = s; *cp && (isdigit(*cp) || (*cp == '.')); cp++) /* NOP */; if (*cp != '\0') { if ((*cp == 'k') || (*cp == 'K')) { drate *= 1000.0; } else if ((*cp != 'h') && (*cp != 'H')) { /* bogus! */ Error(stderr, MGET("invalid sample rate: %s\n"), s); return (1); } } } *rate = irint(drate); return (0); }