1284 lines
39 KiB
C
1284 lines
39 KiB
C
/*
|
|
* This file is part of tuxrecord.
|
|
* https://xw3.org/hanez/tuxrecord
|
|
*
|
|
* tuxrecord - record audio to WAV, FLAC or MP3 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. WAV
|
|
* output is written directly, while FLAC and MP3 are encoded in real time
|
|
* through the system libFLAC and libmp3lame libraries, so no intermediate
|
|
* WAV file is needed. 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 <FLAC/stream_encoder.h>
|
|
#include <lame/lame.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 <strings.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 */
|
|
|
|
#define FLAC_COMPRESSION_LEVEL 5u /* libFLAC level (0 = fastest, 8 = best) */
|
|
#define MP3_BITRATE_KBPS 192 /* constant bitrate for MP3 output */
|
|
#define MP3_QUALITY 2 /* LAME quality (0 = best, 9 = worst) */
|
|
#define MP3_OUTPUT_SLOP 7200u /* extra space for the LAME output buffer */
|
|
#define MP3_MAX_SAMPLES_PER_CALL 100000u /* LAME's per-call frame 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 WAV, FLAC or MP3 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"
|
|
" -f, --format <fmt> Output format: wav, flac or mp3 (default: wav)\n"
|
|
" -h, --help Show this help\n"
|
|
"\n"
|
|
"The file is written as <prefix>__<timestamp>.<ext> where <ext> is wav,\n"
|
|
"flac or mp3 depending on -f/--format\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);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Output format */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/* Container/codec selected with -f/--format (default: WAV). */
|
|
typedef enum {
|
|
FORMAT_WAV = 0,
|
|
FORMAT_FLAC,
|
|
FORMAT_MP3
|
|
} Format;
|
|
|
|
/*
|
|
* Return the file extension (without the dot) for a format.
|
|
*
|
|
* Parameters:
|
|
* format - the selected output format.
|
|
*
|
|
* Returns:
|
|
* "wav", "flac" or "mp3".
|
|
*/
|
|
static const char *format_extension(Format format)
|
|
{
|
|
switch (format) {
|
|
case FORMAT_FLAC:
|
|
return "flac";
|
|
case FORMAT_MP3:
|
|
return "mp3";
|
|
case FORMAT_WAV:
|
|
default:
|
|
return "wav";
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Parse a user-supplied format name (case-insensitive).
|
|
*
|
|
* Parameters:
|
|
* name - the value given to -f/--format, e.g. "wav", "FLAC" or "Mp3".
|
|
* out - receives the parsed format on success.
|
|
*
|
|
* Returns:
|
|
* true if the name is one of the supported formats, false otherwise.
|
|
*/
|
|
static bool format_parse(const char *name, Format *out)
|
|
{
|
|
if (strcasecmp(name, "wav") == 0)
|
|
*out = FORMAT_WAV;
|
|
else if (strcasecmp(name, "flac") == 0)
|
|
*out = FORMAT_FLAC;
|
|
else if (strcasecmp(name, "mp3") == 0)
|
|
*out = FORMAT_MP3;
|
|
else
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* 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;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Output encoders */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/*
|
|
* Streaming output encoder selected by -f/--format.
|
|
*
|
|
* WAV data is appended to a plain file descriptor whose header is patched on
|
|
* close. FLAC and MP3 are handed the captured buffers as they arrive and
|
|
* encode them in real time: libFLAC expands the S16 samples into a reusable
|
|
* i32 conversion buffer, LAME encodes into a reusable byte buffer. No
|
|
* intermediate WAV file is ever written. All resources are released by
|
|
* encoder_close(), which also finalizes the stream.
|
|
*/
|
|
typedef struct {
|
|
Format format;
|
|
unsigned channels;
|
|
bool finished;
|
|
|
|
/* WAV (and MP3) output file */
|
|
int fd;
|
|
|
|
/* FLAC stream encoder and its reusable conversion buffer */
|
|
FLAC__StreamEncoder *flac;
|
|
FLAC__int32 *flac_pcm;
|
|
size_t flac_pcm_cap;
|
|
|
|
/* LAME encoder and its reusable encoded-output buffer */
|
|
lame_global_flags *lame;
|
|
unsigned char *mp3_buf;
|
|
size_t mp3_buf_cap;
|
|
} Encoder;
|
|
|
|
/*
|
|
* Create the output encoder and open the output file.
|
|
*
|
|
* The WAV and MP3 branches open the file themselves, while libFLAC opens its
|
|
* own file from the path passed to FLAC__stream_encoder_init_file(). On
|
|
* failure a descriptive error is printed and -1 is returned; nothing is
|
|
* leaked (caller must not call encoder_close() after a failure).
|
|
*
|
|
* Parameters:
|
|
* enc - encoder to initialize.
|
|
* format - selected output format.
|
|
* path - output file path.
|
|
* rate - negotiated sample rate in Hz.
|
|
* channels - negotiated channel count.
|
|
* bits - bit depth per sample.
|
|
*
|
|
* Returns:
|
|
* 0 on success, -1 on failure.
|
|
*/
|
|
static int encoder_open(Encoder *enc, Format format, const char *path,
|
|
unsigned rate, unsigned channels, unsigned bits)
|
|
{
|
|
memset(enc, 0, sizeof *enc);
|
|
enc->format = format;
|
|
enc->channels = channels;
|
|
enc->fd = -1;
|
|
|
|
switch (format) {
|
|
case FORMAT_WAV:
|
|
enc->fd = wav_open(path, rate, channels, bits);
|
|
if (enc->fd < 0) {
|
|
fprintf(stderr, "Error: cannot open output file: %s: %s\n",
|
|
path, strerror(errno));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
|
|
case FORMAT_FLAC: {
|
|
FLAC__StreamEncoder *fe = FLAC__stream_encoder_new();
|
|
FLAC__StreamEncoderInitStatus status;
|
|
|
|
if (fe == NULL) {
|
|
logmsg("Error: could not allocate the FLAC encoder");
|
|
return -1;
|
|
}
|
|
if (!FLAC__stream_encoder_set_compression_level(fe, FLAC_COMPRESSION_LEVEL) ||
|
|
!FLAC__stream_encoder_set_channels(fe, channels) ||
|
|
!FLAC__stream_encoder_set_bits_per_sample(fe, bits) ||
|
|
!FLAC__stream_encoder_set_sample_rate(fe, rate)) {
|
|
logmsg("Error: could not configure the FLAC encoder");
|
|
FLAC__stream_encoder_delete(fe);
|
|
return -1;
|
|
}
|
|
status = FLAC__stream_encoder_init_file(fe, path, NULL, NULL);
|
|
if (status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) {
|
|
logmsg("Error: cannot open the FLAC output file %s: %s", path,
|
|
FLAC__StreamEncoderInitStatusString[status]);
|
|
FLAC__stream_encoder_delete(fe);
|
|
return -1;
|
|
}
|
|
enc->flac = fe;
|
|
return 0;
|
|
}
|
|
|
|
case FORMAT_MP3: {
|
|
lame_global_flags *gfp;
|
|
|
|
if (channels != 1 && channels != 2) {
|
|
logmsg("Error: MP3 supports only 1 or 2 channels "
|
|
"(device negotiated %u)", channels);
|
|
return -1;
|
|
}
|
|
gfp = lame_init();
|
|
if (gfp == NULL) {
|
|
logmsg("Error: could not allocate the LAME encoder");
|
|
return -1;
|
|
}
|
|
if (lame_set_in_samplerate(gfp, (int)rate) < 0 ||
|
|
lame_set_num_channels(gfp, (int)channels) < 0 ||
|
|
lame_set_brate(gfp, MP3_BITRATE_KBPS) < 0 ||
|
|
lame_set_quality(gfp, MP3_QUALITY) < 0) {
|
|
logmsg("Error: could not configure the LAME encoder");
|
|
lame_close(gfp);
|
|
return -1;
|
|
}
|
|
if (lame_init_params(gfp) < 0) {
|
|
logmsg("Error: could not initialize the LAME encoder");
|
|
lame_close(gfp);
|
|
return -1;
|
|
}
|
|
enc->fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
if (enc->fd < 0) {
|
|
fprintf(stderr, "Error: cannot open output file: %s: %s\n",
|
|
path, strerror(errno));
|
|
lame_close(gfp);
|
|
return -1;
|
|
}
|
|
enc->lame = gfp;
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Upper bound on the number of audio bytes the format can hold.
|
|
*
|
|
* Only the WAV container has a hard limit (the classic 32-bit RIFF size
|
|
* field); the streaming FLAC and MP3 encoders have none.
|
|
*
|
|
* Returns:
|
|
* WAV_DATA_MAX for WAV, UINT64_MAX (effectively unlimited) otherwise.
|
|
*/
|
|
static uint64_t encoder_max_data_bytes(const Encoder *enc)
|
|
{
|
|
return enc->format == FORMAT_WAV ? WAV_DATA_MAX : UINT64_MAX;
|
|
}
|
|
|
|
/*
|
|
* Encode one buffer of interleaved S16_LE samples in real time.
|
|
*
|
|
* WAV writes the raw little-endian bytes, FLAC converts them to i32 in a
|
|
* reusable buffer and hands the whole buffer to libFLAC, and MP3 passes the
|
|
* samples to LAME in chunks (LAME caps the frames per call) using a reusable
|
|
* output buffer. On failure an error has been printed.
|
|
*
|
|
* Parameters:
|
|
* enc - open encoder.
|
|
* samples - buffer of interleaved S16_LE samples.
|
|
* frames - number of sample frames (samples per channel).
|
|
*
|
|
* Returns:
|
|
* 0 on success, -1 on failure.
|
|
*/
|
|
static int encoder_write(Encoder *enc, const int16_t *samples, size_t frames)
|
|
{
|
|
size_t n_samples = frames * enc->channels;
|
|
|
|
switch (enc->format) {
|
|
case FORMAT_WAV:
|
|
if (write_all(enc->fd, samples, n_samples * sizeof(int16_t)) != 0) {
|
|
logmsg("Error: failed to write audio: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
|
|
case FORMAT_FLAC: {
|
|
size_t i;
|
|
|
|
if (n_samples == 0)
|
|
return 0;
|
|
if (enc->flac_pcm_cap < n_samples) {
|
|
FLAC__int32 *p = realloc(enc->flac_pcm, n_samples * sizeof *p);
|
|
if (p == NULL) {
|
|
logmsg("Error: out of memory");
|
|
return -1;
|
|
}
|
|
enc->flac_pcm = p;
|
|
enc->flac_pcm_cap = n_samples;
|
|
}
|
|
for (i = 0; i < n_samples; i++)
|
|
enc->flac_pcm[i] = (FLAC__int32)samples[i];
|
|
if (!FLAC__stream_encoder_process_interleaved(enc->flac, enc->flac_pcm,
|
|
(unsigned)frames)) {
|
|
logmsg("Error: FLAC encoding failed");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
case FORMAT_MP3: {
|
|
size_t offset = 0;
|
|
|
|
while (offset < frames) {
|
|
size_t chunk = frames - offset;
|
|
size_t cap;
|
|
int written;
|
|
|
|
if (chunk > MP3_MAX_SAMPLES_PER_CALL)
|
|
chunk = MP3_MAX_SAMPLES_PER_CALL;
|
|
cap = chunk + chunk / 4 + MP3_OUTPUT_SLOP;
|
|
if (enc->mp3_buf_cap < cap) {
|
|
unsigned char *p = realloc(enc->mp3_buf, cap);
|
|
if (p == NULL) {
|
|
logmsg("Error: out of memory");
|
|
return -1;
|
|
}
|
|
enc->mp3_buf = p;
|
|
enc->mp3_buf_cap = cap;
|
|
}
|
|
/* LAME declares the input as mutable but does not modify it. */
|
|
written = lame_encode_buffer_interleaved(enc->lame,
|
|
(short int *)(samples + offset * enc->channels),
|
|
(int)chunk, enc->mp3_buf, (int)cap);
|
|
if (written < 0) {
|
|
logmsg("Error: MP3 encoding failed (code %d)", written);
|
|
return -1;
|
|
}
|
|
if (write_all(enc->fd, enc->mp3_buf, (size_t)written) != 0) {
|
|
logmsg("Error: failed to write audio: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
offset += chunk;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Finalize the stream and release all encoder resources.
|
|
*
|
|
* For WAV the RIFF/data sizes are patched and the file fsynced and closed;
|
|
* FLAC is finished (writing the final STREAMINFO block); MP3 is flushed
|
|
* (writing LAME's encoder delay) and the file closed. Safe to call once and
|
|
* only after a successful encoder_open().
|
|
*
|
|
* Parameters:
|
|
* enc - open encoder.
|
|
* data_bytes - total number of PCM bytes captured (used to patch WAV).
|
|
*
|
|
* Returns:
|
|
* 0 on success, -1 if finalizing failed.
|
|
*/
|
|
static int encoder_close(Encoder *enc, uint64_t data_bytes)
|
|
{
|
|
int rc = 0;
|
|
|
|
if (enc->finished)
|
|
return 0;
|
|
enc->finished = true;
|
|
|
|
switch (enc->format) {
|
|
case FORMAT_WAV:
|
|
if (wav_finalize(enc->fd, data_bytes) != 0)
|
|
rc = -1;
|
|
enc->fd = -1;
|
|
break;
|
|
|
|
case FORMAT_FLAC:
|
|
if (!FLAC__stream_encoder_finish(enc->flac))
|
|
rc = -1;
|
|
FLAC__stream_encoder_delete(enc->flac);
|
|
enc->flac = NULL;
|
|
break;
|
|
|
|
case FORMAT_MP3:
|
|
if (enc->mp3_buf_cap < MP3_OUTPUT_SLOP) {
|
|
unsigned char *p = realloc(enc->mp3_buf, MP3_OUTPUT_SLOP);
|
|
if (p == NULL) {
|
|
logmsg("Error: out of memory");
|
|
rc = -1;
|
|
} else {
|
|
enc->mp3_buf = p;
|
|
enc->mp3_buf_cap = MP3_OUTPUT_SLOP;
|
|
}
|
|
}
|
|
if (enc->mp3_buf_cap >= MP3_OUTPUT_SLOP) {
|
|
int written = lame_encode_flush(enc->lame, enc->mp3_buf,
|
|
(int)enc->mp3_buf_cap);
|
|
if (written < 0) {
|
|
logmsg("Error: could not flush the MP3 stream (code %d)", written);
|
|
rc = -1;
|
|
} else if (write_all(enc->fd, enc->mp3_buf, (size_t)written) != 0) {
|
|
logmsg("Error: failed to write audio: %s", strerror(errno));
|
|
rc = -1;
|
|
}
|
|
}
|
|
lame_close(enc->lame);
|
|
enc->lame = NULL;
|
|
if (enc->fd >= 0 && close(enc->fd) != 0)
|
|
rc = -1;
|
|
enc->fd = -1;
|
|
break;
|
|
}
|
|
|
|
free(enc->flac_pcm);
|
|
free(enc->mp3_buf);
|
|
enc->flac_pcm = NULL;
|
|
enc->mp3_buf = NULL;
|
|
enc->flac_pcm_cap = 0;
|
|
enc->mp3_buf_cap = 0;
|
|
return rc;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* 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, encode the output.
|
|
*
|
|
* Parses the command line (-o dir, -d seconds, -p prefix, -D device,
|
|
* -f/--format, -h/--help), validates the output directory, builds the output
|
|
* filename "<dir>/<prefix>__<timestamp>.<ext>" and opens the ALSA capture
|
|
* device and the output encoder. It then installs SIGINT/SIGTERM handlers
|
|
* and runs a poll() loop that reads captured PCM frames from ALSA and feeds
|
|
* them to the encoder as they arrive, 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
|
|
* output is finalized, 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;
|
|
Format format = FORMAT_WAV;
|
|
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;
|
|
Encoder enc;
|
|
int 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, "-f") == 0 || strcmp(a, "--format") == 0) {
|
|
if (++i >= argc || !format_parse(argv[i], &format)) {
|
|
usage();
|
|
return 2;
|
|
}
|
|
} 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, <m);
|
|
if (strftime(stamp, sizeof stamp, "%Y%m%d_%H%M%S", <m) == 0)
|
|
strcpy(stamp, "00000000_000000");
|
|
|
|
{
|
|
int need = snprintf(path, sizeof path, "%s/%s__%s.%s",
|
|
output_dir, prefix, stamp, format_extension(format));
|
|
if (need < 0 || need >= (int)sizeof path) {
|
|
fputs("Error: output path too long\n", stderr);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* ---- audio device and output encoder ----------------------------- */
|
|
pcm = open_capture(device, &rate, &channels, &period);
|
|
if (pcm == NULL)
|
|
return 1;
|
|
|
|
if (encoder_open(&enc, format, path, rate, channels, DEFAULT_BITS) != 0) {
|
|
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, %s format",
|
|
device, rate, channels, DEFAULT_BITS, format_extension(format));
|
|
logmsg("Press q or Ctrl+C to stop.");
|
|
|
|
if (snd_pcm_start(pcm) < 0) {
|
|
logmsg("Error: could not start capture on '%s'", device);
|
|
encoder_close(&enc, data_bytes);
|
|
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();
|
|
encoder_close(&enc, 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();
|
|
encoder_close(&enc, 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 > encoder_max_data_bytes(&enc)) {
|
|
logmsg("Warning: 4 GiB WAV limit reached, stopping.");
|
|
break;
|
|
}
|
|
if (encoder_write(&enc, samples, (size_t)got) != 0) {
|
|
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 (encoder_close(&enc, data_bytes) != 0) {
|
|
logmsg("Error: could not finalize output file %s", path);
|
|
result = 1;
|
|
} else if (result == 0) {
|
|
logmsg("Saved %s", path);
|
|
}
|
|
|
|
snd_pcm_close(pcm);
|
|
return result;
|
|
}
|
|
|