tuxrecord/tuxrecord.c

894 lines
28 KiB
C

/*
* This file is part of tuxrecord.
* https://xw3.org/hanez/tuxrecord
*
* tuxrecord - record audio to a WAV file using ALSA
*
* A Linux port of the former macOS-only "macrecord" tool, rewritten in C99
* and using the ALSA capture API (libasound) as the recording source.
* macrecord was created by my good friend Armin. You can find his code here:
* https://codeberg.org/armin/macrecord
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
#define _POSIX_C_SOURCE 200809L
#include <alsa/asoundlib.h>
#include <alloca.h>
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#define DEFAULT_RATE 48000u
#define DEFAULT_CHANNELS 2u
#define DEFAULT_BITS 16u
#define DEFAULT_PREFIX "tuxrecord"
#define DEFAULT_DEVICE "default"
#define VU_WIDTH 48
#define VU_MIN_DB (-60.0)
#define VU_INTERVAL_S 0.05 /* redraw the VU meter at most every 50 ms */
#define HEADER_LENGTH 44
#define WAV_DATA_MAX ((uint64_t)UINT32_MAX - 36u) /* classic 32-bit RIFF limit */
static volatile sig_atomic_t g_stop = 0;
/*
* Handle SIGINT and SIGTERM by requesting a clean stop.
*
* This is an async-signal-safe handler: it only writes to the volatile
* sig_atomic_t flag g_stop, which the recording loop in main() checks on
* every iteration. Because no I/O or locking is done here, Ctrl+C (or a
* SIGTERM) does not terminate the process abruptly; instead the loop
* exits normally, the WAV header is patched, the file is closed and the
* terminal restored.
*
* Parameters:
* sig - the signal number that triggered the handler (unused; required
* by the sigaction handler signature).
*/
static void handle_signal(int sig)
{
(void)sig;
g_stop = 1;
}
/*
* Print a formatted message followed by a newline to stderr.
*
* Acts like vfprintf(stderr, fmt, ...) with a trailing newline, and
* always flushes stderr so messages appear immediately even when stderr
* is redirected (e.g. into a file or pipe). Used for all user-facing
* status and error output.
*
* Parameters:
* fmt - printf-style format string.
* ... - arguments matching the conversion specifiers in fmt.
*/
static void logmsg(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
fflush(stderr);
}
/*
* Print the usage/help text to stdout.
*
* Called when -h/--help is given (the program then exits 0) and for any
* unknown, missing, or malformed option (the program then exits 2). The
* text is emitted through a single printf with adjacent string literals;
* the final argument list fills in the default prefix and device names.
*/
static void usage(void)
{
printf(
"tuxrecord - record audio to a WAV file using ALSA\n"
"\n"
"USAGE\n"
" tuxrecord [options]\n"
"\n"
"OPTIONS\n"
" -o <dir> Output directory (default: current directory)\n"
" -d <secs> Stop automatically after <secs> seconds (default: until q or Ctrl+C)\n"
" -p <prefix> Filename prefix instead of \"%s\" (default: %s)\n"
" -D <device> ALSA capture device (default: \"%s\")\n"
" -h, --help Show this help\n"
"\n"
"The file is written as <prefix>__<timestamp>.wav\n"
"\n"
"By default the ALSA capture device \"default\" is used, which is usually\n"
"the microphone. To record system audio (everything playing on the\n"
"system) see the \"Recording system audio\" section in the README.\n"
"\n"
"PipeWire users: record from the PipeWire default source via the\n"
"ALSA compatibility plugin by passing the device name \"pipewire\":\n"
" tuxrecord -D pipewire (default source)\n"
" pactl set-default-source \"$(pactl get-default-sink).monitor\"\n"
" tuxrecord -D pipewire (system audio, uses the sink monitor)\n",
DEFAULT_PREFIX, DEFAULT_PREFIX, DEFAULT_DEVICE);
}
/* ------------------------------------------------------------------------ */
/* VU meter */
/* ------------------------------------------------------------------------ */
/*
* Return the current time in seconds as a double.
*
* Uses CLOCK_MONOTONIC, so the value is unaffected by wall-clock
* adjustments (NTP, manual time changes) and cannot go backwards during
* a recording. Used to measure elapsed recording time for the -d option
* and to throttle VU meter redraws to VU_INTERVAL_S.
*
* Returns:
* current monotonic time in seconds.
*/
static double now_s(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
/*
* Compute RMS levels (0.0..1.0) for the left and right channel.
*
* Scans all interleaved frames: for each frame the first sample is the
* left channel and the second sample (when channels > 1) is the right
* channel. Per-channel squared samples are summed, the mean is taken and
* the square root is normalized by the full-scale amplitude of int16_t
* (32768.0) so a full-scale sine wave reads near 0.7.
*
* Parameters:
* samples - buffer of interleaved S16_LE PCM samples.
* frames - number of sample frames (each frame holds 'channels'
* samples).
* channels - samples per frame (1 = mono, 2 = stereo).
* left - output: RMS level of the left channel (0 if empty).
* right - output: RMS level of the right channel, or the left value
* for mono input / empty input.
*/
static void rms_levels(const int16_t *samples, size_t frames, unsigned channels,
double *left, double *right)
{
double sum_l = 0.0, sum_r = 0.0;
size_t n_l = 0, n_r = 0;
for (size_t f = 0; f < frames; f++) {
double v = (double)samples[f * channels];
sum_l += v * v;
n_l++;
if (channels > 1) {
v = (double)samples[f * channels + 1];
sum_r += v * v;
n_r++;
}
}
*left = n_l > 0 ? sqrt(sum_l / (double)n_l) / 32768.0 : 0.0;
*right = channels > 1 && n_r > 0 ? sqrt(sum_r / (double)n_r) / 32768.0 : *left;
}
/*
* Convert a linear 0.0..1.0 level into decibels.
*
* Returns VU_MIN_DB for 0.0, otherwise 20*log10(level), clamped to the
* meter's dynamic range [VU_MIN_DB, 0.0] so the VU bar is never
* completely empty above the floor or offset at full scale.
*
* Parameters:
* level - linear amplitude between 0.0 and 1.0.
*
* Returns:
* the level in dB, clamped to [VU_MIN_DB, 0.0].
*/
static double to_db(double level)
{
double db;
if (level <= 0.0)
return VU_MIN_DB;
db = 20.0 * log10(level);
if (db < VU_MIN_DB)
db = VU_MIN_DB;
if (db > 0.0)
db = 0.0;
return db;
}
/*
* Render one horizontal VU meter line (label + 48 segments) into a buffer.
*
* Converts a dB level into a filled fraction of the meter width
* (VU_MIN_DB..0) and draws VU_WIDTH segments: the lit part as full blocks
* (U+2588) colored by level (red above 85 %, yellow above 60 %, green
* otherwise) and the unlit part as faint blocks (U+2591). Each emitted
* segment carries the ANSI color code and a reset, so the result is safe
* to print to a terminal. Stops early if the buffer could overflow.
*
* Parameters:
* db - level in dB (typically clamped by to_db()).
* label - short label printed before the bar, e.g. "L" or "R".
* out - destination buffer to fill.
* outsz - size of the destination buffer in bytes.
*/
static void vu_meter_line(double db, const char *label, char *out, size_t outsz)
{
double fraction = (db - VU_MIN_DB) / (0.0 - VU_MIN_DB);
int filled, n;
if (fraction < 0.0)
fraction = 0.0;
else if (fraction > 1.0)
fraction = 1.0;
filled = (int)(fraction * VU_WIDTH);
{
double pct = fraction * 100.0;
const char *fg = pct > 85.0 ? "\x1b[31m"
: pct > 60.0 ? "\x1b[33m"
: "\x1b[32m";
n = snprintf(out, outsz, "%s ", label);
for (int k = 0; k < VU_WIDTH; k++) {
if (n < 0 || (size_t)n >= outsz - 64)
break;
if (k < filled)
n += snprintf(out + n, outsz - (size_t)n, "%s\u2588\x1b[0m", fg);
else
n += snprintf(out + n, outsz - (size_t)n, "\x1b[32m\u2591\x1b[0m");
}
}
}
/*
* Draw the left and right VU meter lines to stderr.
*
* Renders both channels into local buffers and prints them as two
* consecutive lines. After the first draw, an ANSI "move cursor up two
* lines" escape (\x1b[2A) is emitted first so each redraw overwrites the
* previous meter in place instead of scrolling. stderr is flushed so the
* meter updates promptly on a TTY.
*
* Parameters:
* db_l - left channel level in dB.
* db_r - right channel level in dB.
* first - true for the initial (empty) draw before recording starts,
* false for subsequent in-place redraws.
*/
static void draw_meter(double db_l, double db_r, bool first)
{
char l[1024], r[1024];
vu_meter_line(db_l, "L", l, sizeof l);
vu_meter_line(db_r, "R", r, sizeof r);
if (!first)
fputs("\x1b[2A", stderr); /* move cursor up two lines, then redraw */
fprintf(stderr, "%s\n%s\n", l, r);
fflush(stderr);
}
/* ------------------------------------------------------------------------ */
/* WAV file writer */
/* ------------------------------------------------------------------------ */
/*
* Write a 32-bit unsigned integer to a buffer in little-endian order.
*
* Writes least-significant byte first, as required by the RIFF/WAV
* container format.
*
* Parameters:
* p - buffer to write into (must hold at least 4 bytes).
* v - value to store.
*/
static void put_u32_le(unsigned char *p, uint32_t v)
{
p[0] = (unsigned char)(v & 0xff);
p[1] = (unsigned char)((v >> 8) & 0xff);
p[2] = (unsigned char)((v >> 16) & 0xff);
p[3] = (unsigned char)((v >> 24) & 0xff);
}
/*
* Write a 16-bit unsigned integer to a buffer in little-endian order.
*
* Writes least-significant byte first, as required by the RIFF/WAV
* container format.
*
* Parameters:
* p - buffer to write into (must hold at least 2 bytes).
* v - value to store.
*/
static void put_u16_le(unsigned char *p, uint16_t v)
{
p[0] = (unsigned char)(v & 0xff);
p[1] = (unsigned char)((v >> 8) & 0xff);
}
/*
* Write the whole buffer to a file descriptor, retrying on short writes.
*
* Loops until 'len' bytes have been written or an unrecoverable error
* occurs. Partial writes advance the buffer pointer and remaining count;
* EINTR just retries. Guarantees that the entire 44-byte WAV header and
* the size patch are written in one go even on slow filesystems.
*
* Parameters:
* fd - file descriptor to write to.
* buf - data to write.
* len - number of bytes to write.
*
* Returns:
* 0 on success, -1 on a write error (errno preserved).
*/
static int write_all(int fd, const void *buf, size_t len)
{
const unsigned char *p = buf;
while (len > 0) {
ssize_t w = write(fd, p, len);
if (w < 0) {
if (errno == EINTR)
continue;
return -1;
}
p += w;
len -= (size_t)w;
}
return 0;
}
/*
* Create and open a WAV file, writing a 44-byte PCM header.
*
* Creates (or truncates) the file with mode 0644 and writes the RIFF /
* WAVE / fmt / data chunk layout for the given rate, channel count and
* bit depth. The RIFF chunk size and data chunk size fields are left as
* zero placeholders and patched later by wav_finalize() once the exact
* amount of audio data is known.
*
* Parameters:
* path - filesystem path of the file to create.
* rate - sample rate in Hz to record in the header.
* channels - number of channels to record in the header.
* bits - bits per sample to record in the header.
*
* Returns:
* the new file descriptor on success, or -1 on failure.
*/
static int wav_open(const char *path, unsigned rate, unsigned channels, unsigned bits)
{
unsigned char hdr[HEADER_LENGTH];
uint32_t byte_rate = (uint32_t)rate * channels * bits / 8;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0)
return -1;
memcpy(hdr + 0, "RIFF", 4);
put_u32_le(hdr + 4, 0); /* RIFF chunk size */
memcpy(hdr + 8, "WAVE", 4);
memcpy(hdr + 12, "fmt ", 4);
put_u32_le(hdr + 16, 16); /* fmt chunk size */
put_u16_le(hdr + 20, 1); /* PCM */
put_u16_le(hdr + 22, (uint16_t)channels);
put_u32_le(hdr + 24, rate);
put_u32_le(hdr + 28, byte_rate);
put_u16_le(hdr + 32, (uint16_t)(channels * bits / 8));
put_u16_le(hdr + 34, (uint16_t)bits);
memcpy(hdr + 36, "data", 4);
put_u32_le(hdr + 40, 0); /* data chunk size */
if (write_all(fd, hdr, HEADER_LENGTH) != 0) {
close(fd);
return -1;
}
return fd;
}
/*
* Patch the header chunk sizes and close the WAV file.
*
* Seeks back to the beginning of the file and writes the real RIFF chunk
* size (header + data - 8) at offset 4 and the data chunk size (the
* number of audio bytes) at offset 40, then fsyncs and closes the file.
* Called once after the recording loop with the total bytes accumulated.
*
* Parameters:
* fd - file descriptor from wav_open().
* data_bytes - total number of audio data bytes that were recorded.
*
* Returns:
* 0 on success, -1 on failure. The descriptor is always closed.
*/
static int wav_finalize(int fd, uint64_t data_bytes)
{
unsigned char b[4];
if (lseek(fd, 4, SEEK_SET) != 4)
goto fail;
put_u32_le(b, (uint32_t)HEADER_LENGTH + (uint32_t)data_bytes - 8u);
if (write_all(fd, b, 4) != 0)
goto fail;
if (lseek(fd, 40, SEEK_SET) != 40)
goto fail;
put_u32_le(b, (uint32_t)data_bytes);
if (write_all(fd, b, 4) != 0)
goto fail;
fsync(fd);
close(fd);
return 0;
fail:
close(fd);
return -1;
}
/* ------------------------------------------------------------------------ */
/* ALSA capture */
/* ------------------------------------------------------------------------ */
/*
* Open an ALSA capture device and negotiate the stream parameters.
*
* Opens the named PCM in capture mode and starts from the device's
* current ("any") hardware parameters, then requests interleaved S16_LE
* access, a channel count/sample rate "near" the requested values, and
* commits the configuration. The actually negotiated rate, channel
* count and period size are read back so the caller can allocate sane
* buffers and record a correct WAV header even when the device adjusted
* the values.
*
* On failure a descriptive error is printed to stderr (including a hint
* to use 'plug:<device>' when S16_LE is rejected) and NULL is returned.
*
* Parameters:
* device - ALSA PCM name, e.g. "default" or "hw:Loopback,1".
* rate_out - in: requested sample rate in Hz; out: actual rate.
* channels_out - in: requested channel count; out: actual count.
* period_out - out: period size in frames actually used.
*
* Returns:
* the open PCM handle on success, or NULL on failure.
*/
static snd_pcm_t *open_capture(const char *device, unsigned *rate_out,
unsigned *channels_out, snd_pcm_uframes_t *period_out)
{
snd_pcm_t *pcm = NULL;
snd_pcm_hw_params_t *hw;
unsigned rate = *rate_out;
unsigned channels = *channels_out;
snd_pcm_uframes_t period;
int err;
err = snd_pcm_open(&pcm, device, SND_PCM_STREAM_CAPTURE, 0);
if (err < 0) {
logmsg("Error: cannot open ALSA capture device '%s': %s", device,
snd_strerror(err));
logmsg("Run 'arecord -l' to list available capture devices.");
return NULL;
}
snd_pcm_hw_params_alloca(&hw);
if ((err = snd_pcm_hw_params_any(pcm, hw)) < 0)
goto fail;
if ((err = snd_pcm_hw_params_set_access(pcm, hw,
SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
goto fail;
if ((err = snd_pcm_hw_params_set_format(pcm, hw,
SND_PCM_FORMAT_S16_LE)) < 0) {
logmsg("Error: device '%s' does not support 16-bit signed PCM; "
"try 'plug:%s'", device, device);
snd_pcm_close(pcm);
return NULL;
}
if ((err = snd_pcm_hw_params_set_channels_near(pcm, hw, &channels)) < 0)
goto fail;
if ((err = snd_pcm_hw_params_set_rate_near(pcm, hw, &rate, NULL)) < 0)
goto fail;
if ((err = snd_pcm_hw_params(pcm, hw)) < 0)
goto fail;
/* Read back the values that were actually negotiated. */
if ((err = snd_pcm_hw_params_get_rate(hw, &rate, NULL)) < 0)
goto fail;
if ((err = snd_pcm_hw_params_get_channels(hw, &channels)) < 0)
goto fail;
period = 1024;
if ((err = snd_pcm_hw_params_get_period_size(hw, &period, NULL)) < 0)
goto fail;
if (period == 0)
period = 1024;
*rate_out = rate;
*channels_out = channels;
*period_out = period;
return pcm;
fail:
logmsg("Error: failed to configure ALSA capture on '%s': %s", device,
snd_strerror(err));
snd_pcm_close(pcm);
return NULL;
}
/* ------------------------------------------------------------------------ */
/* Terminal */
/* ------------------------------------------------------------------------ */
static struct termios g_old_term;
static bool g_raw_mode = false;
/*
* Switch stdin into raw (non-canonical, no-echo) terminal mode.
*
* Only active when stdin is a TTY; otherwise it returns false and leaves
* the terminal alone (recording works, 'q' just is not available). Raw
* mode makes each keystroke immediately available to read() (VMIN=1,
* VTIME=0) with echo disabled, so the recording loop can detect 'q' as
* it is typed. TCSANOW applies the change without flushing a key that
* may already have been typed.
*
* Returns:
* true if raw mode was enabled (restore_terminal() must be called
* later), false if stdin is not a TTY or the change failed.
*/
static bool enable_raw_mode(void)
{
struct termios raw;
if (!isatty(STDIN_FILENO))
return false;
if (tcgetattr(STDIN_FILENO, &g_old_term) != 0)
return false;
raw = g_old_term;
raw.c_lflag &= (tcflag_t)~(ICANON | ECHO);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
/* TCSANOW: don't flush a key that was already typed before raw mode */
if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) != 0)
return false;
g_raw_mode = true;
return true;
}
/*
* Restore the terminal settings saved by enable_raw_mode().
*
* Re-applies the saved termios with TCSAFLUSH (drops any buffered
* input/output) so the terminal is always left in its original state,
* and clears the raw-mode flag so it runs at most once. Safe to call
* even if raw mode was never enabled.
*/
static void restore_terminal(void)
{
if (g_raw_mode) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_old_term);
g_raw_mode = false;
}
}
/* ------------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------------ */
/*
* Program entry point: parse options, record audio, write a WAV file.
*
* Parses the command line (-o dir, -d seconds, -p prefix, -D device,
* -h/--help), validates the output directory, builds the output filename
* "<dir>/<prefix>__<timestamp>.wav" and opens the ALSA capture device
* and the WAV file. It then installs SIGINT/SIGTERM handlers and runs a
* poll() loop that reads captured PCM frames from ALSA and appends them
* to the file, redrawing the VU meter on a tty stderr at most every
* VU_INTERVAL_S. The loop stops on 'q' (raw mode), Ctrl+C/SIGINT,
* SIGTERM, the -d duration elapsing, or on error. Finally the WAV header
* is patched, the terminal restored and the result announced.
*
* Returns:
* 0 on success, 1 on a runtime error, 2 on a command-line usage error.
*/
int main(int argc, char **argv)
{
const char *output_dir = ".";
const char *prefix = DEFAULT_PREFIX;
const char *device = DEFAULT_DEVICE;
double duration = 0.0; /* 0 = record until stopped */
struct stat st;
char path[4096];
char stamp[32];
struct tm ltm;
time_t t;
snd_pcm_t *pcm;
snd_pcm_uframes_t period;
unsigned rate = DEFAULT_RATE;
unsigned channels = DEFAULT_CHANNELS;
snd_pcm_sframes_t got;
struct sigaction sa;
int fd, i, result = 0;
bool stderr_tty, use_stdin;
uint64_t data_bytes = 0;
double start_t, last_draw;
/* ---- argument parsing ------------------------------------------- */
i = 1;
while (i < argc) {
const char *a = argv[i];
if (strcmp(a, "-o") == 0) {
if (++i >= argc) {
usage();
return 2;
}
output_dir = argv[i];
} else if (strcmp(a, "-d") == 0) {
char *end = NULL;
double d;
if (++i >= argc) {
usage();
return 2;
}
d = strtod(argv[i], &end);
if (end == argv[i] || *end != '\0' || !isfinite(d) || d <= 0.0) {
usage();
return 2;
}
duration = d;
} else if (strcmp(a, "-p") == 0) {
if (++i >= argc || argv[i][0] == '\0') {
usage();
return 2;
}
prefix = argv[i];
} else if (strcmp(a, "-D") == 0) {
if (++i >= argc || argv[i][0] == '\0') {
usage();
return 2;
}
device = argv[i];
} else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage();
return 0;
} else {
usage();
return 2;
}
i++;
}
/* ---- output directory and filename ------------------------------- */
if (stat(output_dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
fprintf(stderr, "Error: output directory does not exist: %s\n",
output_dir);
return 1;
}
t = time(NULL);
localtime_r(&t, &ltm);
if (strftime(stamp, sizeof stamp, "%Y%m%d_%H%M%S", &ltm) == 0)
strcpy(stamp, "00000000_000000");
{
int need = snprintf(path, sizeof path, "%s/%s__%s.wav",
output_dir, prefix, stamp);
if (need < 0 || need >= (int)sizeof path) {
fputs("Error: output path too long\n", stderr);
return 1;
}
}
/* ---- audio device and output file -------------------------------- */
pcm = open_capture(device, &rate, &channels, &period);
if (pcm == NULL)
return 1;
fd = wav_open(path, rate, channels, DEFAULT_BITS);
if (fd < 0) {
fprintf(stderr, "Error: cannot open output file: %s: %s\n",
path, strerror(errno));
snd_pcm_close(pcm);
return 1;
}
/* ---- signals ------------------------------------------------------ */
memset(&sa, 0, sizeof sa);
sa.sa_handler = handle_signal;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
stderr_tty = isatty(STDERR_FILENO);
use_stdin = isatty(STDIN_FILENO);
logmsg("Recording to %s", path);
logmsg("Using ALSA device '%s' at %u Hz, %u channel(s), %d-bit",
device, rate, channels, DEFAULT_BITS);
logmsg("Press q or Ctrl+C to stop.");
if (snd_pcm_start(pcm) < 0) {
logmsg("Error: could not start capture on '%s'", device);
close(fd);
snd_pcm_close(pcm);
return 1;
}
fputs("Recording started.\n", stderr);
if (stderr_tty)
draw_meter(VU_MIN_DB, VU_MIN_DB, true);
fflush(stderr);
/* ---- recording loop ----------------------------------------------- */
{
size_t frames_per_read = (size_t)period;
int16_t *samples = malloc(frames_per_read * channels * sizeof(int16_t));
struct pollfd *pfds;
int pcm_poll_count = snd_pcm_poll_descriptors_count(pcm);
int nfds;
if (samples == NULL) {
logmsg("Error: out of memory");
restore_terminal();
wav_finalize(fd, data_bytes);
snd_pcm_close(pcm);
return 1;
}
if (pcm_poll_count < 1)
pcm_poll_count = 1;
nfds = pcm_poll_count + (use_stdin ? 1 : 0);
pfds = calloc((size_t)nfds, sizeof *pfds);
if (pfds == NULL) {
logmsg("Error: out of memory");
free(samples);
restore_terminal();
wav_finalize(fd, data_bytes);
snd_pcm_close(pcm);
return 1;
}
snd_pcm_poll_descriptors(pcm, pfds, (unsigned)pcm_poll_count);
if (use_stdin) {
pfds[pcm_poll_count].fd = STDIN_FILENO;
pfds[pcm_poll_count].events = POLLIN;
(void)enable_raw_mode();
}
start_t = now_s();
last_draw = start_t;
while (!g_stop) {
int pr;
int timeout = -1;
/* automatic stop timer */
if (duration > 0.0) {
double remaining = duration - (now_s() - start_t);
if (remaining <= 0.0)
break;
timeout = (int)(remaining * 1000.0);
if (timeout < 1)
timeout = 1;
}
pr = poll(pfds, (nfds_t)nfds, timeout);
if (pr < 0) {
if (errno == EINTR)
continue; /* interrupted by a signal */
logmsg("Error: poll failed: %s", strerror(errno));
result = 1;
break;
}
if (pr == 0)
continue; /* timed out; re-check timer */
/* keyboard input (raw mode, 'q' to quit) */
if (use_stdin && (pfds[pcm_poll_count].revents & POLLIN)) {
char buf[16];
ssize_t n = read(STDIN_FILENO, buf, sizeof buf);
for (ssize_t k = 0; n > 0 && k < n; k++) {
if (buf[k] == 'q' || buf[k] == 'Q') {
logmsg("Quit key received, stopping...");
g_stop = 1;
}
}
}
if (g_stop)
break;
/* capture audio */
if (pcm_poll_count > 0 && nfds - (use_stdin ? 1 : 0) > 0) {
unsigned short pcm_rev = 0;
snd_pcm_poll_descriptors_revents(pcm, pfds,
(unsigned)pcm_poll_count,
&pcm_rev);
if (!(pcm_rev & (POLLIN | POLLERR)))
continue;
got = snd_pcm_readi(pcm, samples, period);
if (got == -EINTR) {
continue;
} else if (got == -EPIPE || got == -ESTRPIPE) {
logmsg("Capture underrun/device suspended, resuming...");
if (snd_pcm_recover(pcm, (int)got, 1) < 0) {
logmsg("Error: could not recover the ALSA stream");
result = 1;
break;
}
continue;
} else if (got < 0) {
logmsg("Error: failed to read audio: %s",
snd_strerror((int)got));
result = 1;
break;
} else if (got > 0) {
size_t bytes = (size_t)got * channels * sizeof(int16_t);
double meter_now;
if (data_bytes + bytes > WAV_DATA_MAX) {
logmsg("Warning: 4 GiB WAV limit reached, stopping.");
break;
}
if (write_all(fd, samples, bytes) != 0) {
logmsg("Error: failed to write audio: %s",
strerror(errno));
result = 1;
break;
}
data_bytes += bytes;
/* VU meter, redrawn at most every 50 ms */
meter_now = now_s();
if (stderr_tty && meter_now - last_draw >= VU_INTERVAL_S) {
double l, r;
rms_levels(samples, (size_t)got, channels, &l, &r);
draw_meter(to_db(l), to_db(r), false);
last_draw = meter_now;
}
}
}
}
free(pfds);
free(samples);
}
/* ---- wrap up ------------------------------------------------------ */
restore_terminal();
logmsg("\nRecording ended.");
if (wav_finalize(fd, data_bytes) != 0) {
logmsg("Error: could not finalize WAV file %s", path);
result = 1;
} else if (result == 0) {
logmsg("Saved %s", path);
}
snd_pcm_close(pcm);
return result;
}