root/usr.bin/audio/record/record.c
/*      $NetBSD: record.c,v 1.60 2025/12/10 21:33:00 andvar Exp $       */

/*
 * Copyright (c) 1999, 2002, 2003, 2005, 2010 Matthew R. Green
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * SunOS compatible audiorecord(1)
 */
#include <sys/cdefs.h>

#ifndef lint
__RCSID("$NetBSD: record.c,v 1.60 2025/12/10 21:33:00 andvar Exp $");
#endif


#include <sys/param.h>
#include <sys/audioio.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/uio.h>

#include <err.h>
#include <fcntl.h>
#include <paths.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <util.h>

#include "libaudio.h"
#include "auconv.h"

static audio_info_t info, oinfo;
static const char *device;
static int      audiofd;
static int      aflag, fflag;
int     verbose;
static int      monitor_gain, omonitor_gain;
static int      gain;
static int      balance;
static int      port;
static char     *encoding_str;
static struct track_info ti;
static struct timeval record_time;
static struct timeval start_time;
static int no_time_limit = 1;

static void (*conv_func) (u_char *, int);

static void usage (void) __dead;
static int timeleft (struct timeval *, struct timeval *);
static void cleanup (int) __dead;
static void rewrite_header (void);
static void stop (int);

static void stop (int sig)
{
        no_time_limit = 0;
        timerclear(&record_time);
}

int
main(int argc, char *argv[])
{
        u_char  *buffer;
        size_t  len, bufsize = 0;
        ssize_t nread;
        int     ch;
        const char *defdevice = _PATH_SOUND;

        /*
         * Initialise the track_info.
         */
        ti.format = AUDIO_FORMAT_DEFAULT;
        ti.total_size = -1;

        while ((ch = getopt(argc, argv, "ab:B:C:F:c:d:e:fhi:m:P:p:qt:s:Vv:")) != -1) {
                switch (ch) {
                case 'a':
                        aflag++;
                        break;
                case 'b':
                        decode_int(optarg, &balance);
                        if (balance < 0 || balance > 63)
                                errx(1, "balance must be between 0 and 63");
                        break;
                case 'B':
                        bufsize = strsuftoll("read buffer size", optarg,
                                             1, UINT_MAX);
                        break;
                case 'C':
                        /* Ignore, compatibility */
                        break;
                case 'F':
                        ti.format = audio_format_from_str(optarg);
                        if (ti.format < 0)
                                errx(1, "Unknown audio format; supported "
                                    "formats: \"sun\", \"wav\", and \"none\"");
                        break;
                case 'c':
                        decode_int(optarg, &ti.channels);
                        if (ti.channels < 0 || ti.channels > 16)
                                errx(1, "channels must be between 0 and 16");
                        break;
                case 'd':
                        device = optarg;
                        break;
                case 'e':
                        encoding_str = optarg;
                        break;
                case 'f':
                        fflag++;
                        break;
                case 'i':
                        ti.header_info = optarg;
                        break;
                case 'm':
                        decode_int(optarg, &monitor_gain);
                        if (monitor_gain < 0 || monitor_gain > 255)
                                errx(1, "monitor volume must be between 0 and 255");
                        break;
                case 'P':
                        decode_int(optarg, &ti.precision);
                        if (ti.precision != 4 && ti.precision != 8 &&
                            ti.precision != 16 && ti.precision != 24 &&
                            ti.precision != 32)
                                errx(1, "precision must be between 4, 8, 16, 24 or 32");
                        break;
                case 'p':
                        len = strlen(optarg);

                        if (strncmp(optarg, "mic", len) == 0)
                                port |= AUDIO_MICROPHONE;
                        else if (strncmp(optarg, "cd", len) == 0 ||
                                   strncmp(optarg, "internal-cd", len) == 0)
                                port |= AUDIO_CD;
                        else if (strncmp(optarg, "line", len) == 0)
                                port |= AUDIO_LINE_IN;
                        else
                                errx(1,
                            "port must be `cd', `internal-cd', `mic', or `line'");
                        break;
                case 'q':
                        ti.qflag++;
                        break;
                case 's':
                        decode_int(optarg, &ti.sample_rate);
                        if (ti.sample_rate < 0 || ti.sample_rate > 48000 * 2)   /* XXX */
                                errx(1, "sample rate must be between 0 and 96000");
                        break;
                case 't':
                        no_time_limit = 0;
                        decode_time(optarg, &record_time);
                        break;
                case 'V':
                        verbose++;
                        break;
                case 'v':
                        decode_int(optarg, &gain);
                        if (gain < 0 || gain > 255)
                                errx(1, "volume must be between 0 and 255");
                        break;
                /* case 'h': */
                default:
                        usage();
                        /* NOTREACHED */
                }
        }
        argc -= optind;
        argv += optind;

        if (argc != 1)
                usage();

        /*
         * convert the encoding string into a value.
         */
        if (encoding_str) {
                ti.encoding = audio_enc_to_val(encoding_str);
                if (ti.encoding == -1)
                        errx(1, "unknown encoding, bailing...");
        }

        /*
         * open the output file
         */
        if (argv[0][0] != '-' || argv[0][1] != '\0') {
                /* intuit the file type from the name */
                if (ti.format == AUDIO_FORMAT_DEFAULT)
                {
                        size_t flen = strlen(*argv);
                        const char *arg = *argv;

                        if (strcasecmp(arg + flen - 3, ".au") == 0)
                                ti.format = AUDIO_FORMAT_SUN;
                        else if (strcasecmp(arg + flen - 4, ".wav") == 0)
                                ti.format = AUDIO_FORMAT_WAV;
                }
                ti.outfd = open(*argv, O_CREAT|(aflag ? O_APPEND : O_TRUNC)|O_WRONLY, 0666);
                if (ti.outfd < 0)
                        err(1, "could not open %s", *argv);
        } else
                ti.outfd = STDOUT_FILENO;

        /*
         * open the audio device
         */
        if (device == NULL && (device = getenv("AUDIODEVICE")) == NULL &&
            (device = getenv("AUDIODEV")) == NULL) /* Sun compatibility */
                device = defdevice;

        audiofd = open(device, O_RDONLY);
        if (audiofd < 0 && device == defdevice) {
                device = _PATH_SOUND0;
                audiofd = open(device, O_RDONLY);
        }
        if (audiofd < 0)
                err(1, "failed to open %s", device);

        /*
         * work out the buffer size to use, and allocate it.  also work out
         * what the old monitor gain value is, so that we can reset it later.
         */
        if (ioctl(audiofd, AUDIO_GETINFO, &oinfo) < 0)
                err(1, "failed to get audio info");
        if (bufsize == 0) {
                bufsize = oinfo.record.buffer_size;
                if (bufsize < 32 * 1024)
                        bufsize = 32 * 1024;
        }
        omonitor_gain = oinfo.monitor_gain;

        buffer = malloc(bufsize);
        if (buffer == NULL)
                err(1, "couldn't malloc buffer of %d size", (int)bufsize);

        /*
         * set up audio device for recording with the specified parameters
         */
        AUDIO_INITINFO(&info);

        /*
         * for these, get the current values for stuffing into the header
         */
#define SETINFO2(x, y)  if (x) \
                                info.record.y = x; \
                        else \
                                info.record.y = x = oinfo.record.y;
#define SETINFO(x)      SETINFO2(ti.x, x)

        SETINFO (sample_rate)
        SETINFO (channels)
        SETINFO (precision)
        SETINFO (encoding)
        SETINFO2 (gain, gain)
        SETINFO2 (port, port)
        SETINFO2 (balance, balance)
#undef SETINFO
#undef SETINFO2

        if (monitor_gain)
                info.monitor_gain = monitor_gain;
        else
                monitor_gain = oinfo.monitor_gain;

        info.mode = AUMODE_RECORD;
        if (ioctl(audiofd, AUDIO_SETINFO, &info) < 0)
                err(1, "failed to set audio info");

        signal(SIGINT, stop);

        ti.total_size = 0;

        write_header(&ti);
        if (ti.format == AUDIO_FORMAT_NONE)
                errx(1, "unable to determine audio format");
        conv_func = write_get_conv_func(&ti);

        if (verbose && conv_func) {
                const char *s = NULL;

                if (conv_func == swap_bytes)
                        s = "swap bytes (16 bit)";
                else if (conv_func == swap_bytes32)
                        s = "swap bytes (32 bit)";
                else if (conv_func == change_sign16_be)
                        s = "change sign (big-endian, 16 bit)";
                else if (conv_func == change_sign16_le)
                        s = "change sign (little-endian, 16 bit)";
                else if (conv_func == change_sign24_be)
                        s = "change sign (big-endian, 24 bit)";
                else if (conv_func == change_sign24_le)
                        s = "change sign (little-endian, 24 bit)";
                else if (conv_func == change_sign32_be)
                        s = "change sign (big-endian, 32 bit)";
                else if (conv_func == change_sign32_le)
                        s = "change sign (little-endian, 32 bit)";
                else if (conv_func == change_sign16_swap_bytes_be)
                        s = "change sign & swap bytes (big-endian, 16 bit)";
                else if (conv_func == change_sign16_swap_bytes_le)
                        s = "change sign & swap bytes (little-endian, 16 bit)";
                else if (conv_func == change_sign24_swap_bytes_be)
                        s = "change sign & swap bytes (big-endian, 24 bit)";
                else if (conv_func == change_sign24_swap_bytes_le)
                        s = "change sign & swap bytes (little-endian, 24 bit)";
                else if (conv_func == change_sign32_swap_bytes_be)
                        s = "change sign (big-endian, 32 bit)";
                else if (conv_func == change_sign32_swap_bytes_le)
                        s = "change sign & swap bytes (little-endian, 32 bit)";

                if (s)
                        fprintf(stderr, "%s: converting, using function: %s\n",
                            getprogname(), s);
                else
                        fprintf(stderr, "%s: using unnamed conversion "
                                        "function\n", getprogname());
        }

        if (verbose)
                fprintf(stderr,
                   "sample_rate=%d channels=%d precision=%d encoding=%s\n",
                   info.record.sample_rate, info.record.channels,
                   info.record.precision,
                   audio_enc_from_val(info.record.encoding));

        if (!no_time_limit && verbose)
                fprintf(stderr, "recording for %lu seconds, %lu microseconds\n",
                    (u_long)record_time.tv_sec, (u_long)record_time.tv_usec);

        (void)gettimeofday(&start_time, NULL);
        while (no_time_limit || timeleft(&start_time, &record_time)) {
                if ((nread = read(audiofd, buffer, bufsize)) == -1)
                        err(1, "read failed");
                if (nread == 0)
                        break;
                if (conv_func)
                        (*conv_func)(buffer, nread);
                if (write(ti.outfd, buffer, nread) != nread)
                        err(1, "write failed");
                ti.total_size += nread;
        }
        cleanup(0);
}

int
timeleft(struct timeval *start_tvp, struct timeval *record_tvp)
{
        struct timeval now, diff;

        (void)gettimeofday(&now, NULL);
        timersub(&now, start_tvp, &diff);
        timersub(record_tvp, &diff, &now);

        return (now.tv_sec > 0 || (now.tv_sec == 0 && now.tv_usec > 0));
}

void
cleanup(int signo)
{

        rewrite_header();
        close(ti.outfd);
        if (omonitor_gain) {
                AUDIO_INITINFO(&info);
                info.monitor_gain = omonitor_gain;
                if (ioctl(audiofd, AUDIO_SETINFO, &info) < 0)
                        err(1, "failed to reset audio info");
        }
        close(audiofd);
        if (signo != 0) {
                (void)raise_default_signal(signo);
        }
        exit(0);
}

static void
rewrite_header(void)
{

        /* can't do this here! */
        if (ti.outfd == STDOUT_FILENO)
                return;
        if (lseek(ti.outfd, (off_t)0, SEEK_SET) == (off_t)-1)
                err(1, "could not seek to start of file for header rewrite");
        write_header(&ti);
}

static void
usage(void)
{

        fprintf(stderr, "Usage: %s [-afhqV] [options] {files ...|-}\n",
            getprogname());
        fprintf(stderr, "Options:\n\t"
            "-B buffer size\n\t"
            "-b balance (0-63)\n\t"
            "-c channels\n\t"
            "-d audio device\n\t"
            "-e encoding\n\t"
            "-F format\n\t"
            "-i header information\n\t"
            "-m monitor volume\n\t"
            "-P precision (4, 8, 16, 24, or 32 bits)\n\t"
            "-p input port\n\t"
            "-s sample rate\n\t"
            "-t recording time\n\t"
            "-v volume\n");
        exit(EXIT_FAILURE);
}