Added flac and mp3 output format.

This commit is contained in:
Johannes Findeisen 2026-10-15 02:49:31 +02:00
commit 3ff9ea07f5
4 changed files with 481 additions and 46 deletions

4
.gitignore vendored
View file

@ -14,8 +14,10 @@ recordings/
# Vim swap files # Vim swap files
*.swp *.swp
# Misc # Recordings
*.wav *.wav
*.flac
*.mp3
# Temporary files # Temporary files
tmp/ tmp/

View file

@ -3,10 +3,21 @@ CFLAGS = -std=c99 -O2 -Wall -Wextra
PKGCONF = pkg-config PKGCONF = pkg-config
ALSA_CFLAGS := $(shell $(PKGCONF) --cflags alsa 2>/dev/null) ALSA_CFLAGS := $(shell $(PKGCONF) --cflags alsa 2>/dev/null)
ALSA_LIBS := $(shell $(PKGCONF) --libs alsa 2>/dev/null) ALSA_LIBS := $(shell $(PKGCONF) --libs alsa 2>/dev/null)
FLAC_CFLAGS := $(shell $(PKGCONF) --cflags flac 2>/dev/null)
FLAC_LIBS := $(shell $(PKGCONF) --libs flac 2>/dev/null)
LAME_CFLAGS := $(shell $(PKGCONF) --cflags lame 2>/dev/null)
LAME_LIBS := $(shell $(PKGCONF) --libs lame 2>/dev/null)
ifeq ($(strip $(ALSA_LIBS)),) ifeq ($(strip $(ALSA_LIBS)),)
ALSA_LIBS := -lasound ALSA_LIBS := -lasound
endif endif
LDLIBS = $(ALSA_LIBS) -lm ifeq ($(strip $(FLAC_LIBS)),)
FLAC_LIBS := -lFLAC
endif
ifeq ($(strip $(LAME_LIBS)),)
LAME_LIBS := -lmp3lame
endif
CPPFLAGS += $(ALSA_CFLAGS) $(FLAC_CFLAGS) $(LAME_CFLAGS)
LDLIBS = $(ALSA_LIBS) $(FLAC_LIBS) $(LAME_LIBS) -lm
PREFIX ?= /usr/local PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin BINDIR = $(PREFIX)/bin
@ -17,7 +28,7 @@ SRC = tuxrecord.c
all: $(BINARY) all: $(BINARY)
$(BINARY): $(SRC) $(BINARY): $(SRC)
$(CC) $(CFLAGS) $(ALSA_CFLAGS) -o $@ $< $(LDLIBS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(LDLIBS)
install: $(BINARY) install: $(BINARY)
install -d $(DESTDIR)$(BINDIR) install -d $(DESTDIR)$(BINDIR)

View file

@ -1,10 +1,13 @@
# tuxrecord # tuxrecord
Record audio to a WAV file on **Linux** using **ALSA** as the recording source. Record audio to WAV, FLAC or MP3 files on **Linux** using **ALSA** as the
recording source.
`tuxrecord` is the Linux port of the former macOS-only **macrecord** tool. The `tuxrecord` is the Linux port of the former macOS-only **macrecord** tool. The
Swift/AVFoundation code was rewritten in **C99** and now uses the native ALSA Swift/AVFoundation code was rewritten in **C99** and now uses the native ALSA
capture API (`libasound`); it builds with **GCC**. capture API (`libasound`); it builds with **GCC**. 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 written.
macrecord was created by my good friend Armin. You can find his code here: macrecord was created by my good friend Armin. You can find his code here:
[https://codeberg.org/armin/macrecord](https://codeberg.org/armin/macrecord) [https://codeberg.org/armin/macrecord](https://codeberg.org/armin/macrecord)
@ -13,6 +16,11 @@ macrecord was created by my good friend Armin. You can find his code here:
- GCC and GNU make - GCC and GNU make
- ALSA development headers (`libasound2-dev` on Debian/Ubuntu, - ALSA development headers (`libasound2-dev` on Debian/Ubuntu,
`alsa-lib-devel` on Fedora, `alsa-lib` on Arch) `alsa-lib-devel` on Fedora, `alsa-lib` on Arch)
- FLAC and LAME (MP3) development headers, used for the real-time encoders
(`libflac-dev` and `libmp3lame-dev` on Debian/Ubuntu, `flac-devel` and
`lame-devel` on Fedora, `flac` and `lame` on Arch). They are only needed to
build; at run time the shared `libFLAC` / `libmp3lame` libraries are used.
- `pkg-config` (used to locate ALSA, FLAC and LAME)
## Usage ## Usage
@ -32,6 +40,10 @@ tuxrecord -p myrec
# Use a specific ALSA capture device # Use a specific ALSA capture device
tuxrecord -D hw:1,0 tuxrecord -D hw:1,0
# Encode to FLAC (lossless) or MP3 (lossy) in real time
tuxrecord -f flac
tuxrecord --format mp3
# Record from PipeWire (the PipeWire sound server) # Record from PipeWire (the PipeWire sound server)
tuxrecord -D pipewire tuxrecord -D pipewire
``` ```
@ -44,6 +56,7 @@ tuxrecord -D pipewire
| `-d <secs>` | Stop automatically after `<secs>` seconds | | `-d <secs>` | Stop automatically after `<secs>` seconds |
| `-p <prefix>` | Filename prefix instead of `tuxrecord` | | `-p <prefix>` | Filename prefix instead of `tuxrecord` |
| `-D <device>` | ALSA capture device (default: `default`; `pipewire` on PipeWire systems) | | `-D <device>` | ALSA capture device (default: `default`; `pipewire` on PipeWire systems) |
| `-f, --format <fmt>` | Output format: `wav`, `flac` or `mp3` (default: `wav`) |
| `-h, --help` | Show help | | `-h, --help` | Show help |
Run `arecord -l` to list available ALSA capture devices. On systems with Run `arecord -l` to list available ALSA capture devices. On systems with
@ -55,9 +68,27 @@ or `wpctl status` (see "Recording from PipeWire" below).
- **q** - Stop recording and save the file - **q** - Stop recording and save the file
- **Ctrl+C** - Stop recording and save the file - **Ctrl+C** - Stop recording and save the file
The file is written as `<prefix>__<timestamp>.wav` (default prefix The file is written as `<prefix>__<timestamp>.<ext>` where `<ext>` is `wav`,
`tuxrecord`). The VU meter shows the input level of the left and right channels `flac` or `mp3` depending on `-f/--format` (default prefix `tuxrecord`, default
in real time when stderr is a terminal. format `wav`). The VU meter shows the input level of the left and right
channels in real time when stderr is a terminal.
## Output formats
Audio is captured once and handed to the selected encoder as it arrives, so
FLAC and MP3 are produced **in real time** — no intermediate WAV file is
written and no second conversion pass runs afterwards. A small reusable buffer
converts the captured 16-bit samples to the layout the encoder expects.
- **wav** (default) — uncompressed 16-bit little-endian PCM in a RIFF/WAVE
container, written directly by tuxrecord.
- **flac** — lossless compression via libFLAC (streaming, default
compression level 5).
- **mp3** — lossy compression via LAME at a constant 192 kbit/s, quality 2.
`wav` and `flac` preserve the captured stream exactly; `mp3` is lossy. The
classic 4 GiB RIFF limit applies to `wav` only (the program stops cleanly
before exceeding it); `flac` and `mp3` have no such limit.
## Recording system audio ## Recording system audio
@ -128,9 +159,9 @@ tuxrecord -D pipewire
``` ```
The `.monitor` source carries exactly what the sink is playing, so anything The `.monitor` source carries exactly what the sink is playing, so anything
your players output ends up in the WAV file. Every capture follows the same your players output ends up in the recorded file. Every capture follows the
`<prefix>__<timestamp>.wav` naming and can be stopped with `q` or **Ctrl+C**. same `<prefix>__<timestamp>.<ext>` naming and can be stopped with `q` or
The default setting can be restored later with **Ctrl+C**. The default setting can be restored later with
`pactl set-default-source <your-input-source>`. `pactl set-default-source <your-input-source>`.
### Troubleshooting ### Troubleshooting
@ -150,7 +181,8 @@ The default setting can be restored later with
```bash ```bash
make make
# or directly: # or directly:
# gcc -std=c99 -O2 -Wall -Wextra tuxrecord.c -o tuxrecord $(pkg-config --libs alsa) -lm # gcc -std=c99 -O2 -Wall -Wextra tuxrecord.c -o tuxrecord \
# $(pkg-config --cflags --libs alsa flac lame) -lm
``` ```
Install (installs to `$(PREFIX)/bin`, default `/usr/local/bin`): Install (installs to `$(PREFIX)/bin`, default `/usr/local/bin`):

View file

@ -2,12 +2,14 @@
* This file is part of tuxrecord. * This file is part of tuxrecord.
* https://xw3.org/hanez/tuxrecord * https://xw3.org/hanez/tuxrecord
* *
* tuxrecord - record audio to a WAV file using ALSA * tuxrecord - record audio to WAV, FLAC or MP3 using ALSA
* *
* A Linux port of the former macOS-only "macrecord" tool, rewritten in C99 * A Linux port of the former macOS-only "macrecord" tool, rewritten in C99
* and using the ALSA capture API (libasound) as the recording source. * and using the ALSA capture API (libasound) as the recording source. WAV
* macrecord was created by my good friend Armin. You can find his code here: * output is written directly, while FLAC and MP3 are encoded in real time
* https://codeberg.org/armin/macrecord * 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> * Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license. * Licensed under the terms of the Apache-2.0 license.
@ -18,6 +20,8 @@
#include <alsa/asoundlib.h> #include <alsa/asoundlib.h>
#include <alloca.h> #include <alloca.h>
#include <FLAC/stream_encoder.h>
#include <lame/lame.h>
#include <errno.h> #include <errno.h>
#include <limits.h> #include <limits.h>
@ -30,6 +34,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <termios.h> #include <termios.h>
@ -48,6 +53,12 @@
#define HEADER_LENGTH 44 #define HEADER_LENGTH 44
#define WAV_DATA_MAX ((uint64_t)UINT32_MAX - 36u) /* classic 32-bit RIFF limit */ #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; static volatile sig_atomic_t g_stop = 0;
/* /*
@ -103,7 +114,7 @@ static void logmsg(const char *fmt, ...)
static void usage(void) static void usage(void)
{ {
printf( printf(
"tuxrecord - record audio to a WAV file using ALSA\n" "tuxrecord - record audio to WAV, FLAC or MP3 using ALSA\n"
"\n" "\n"
"USAGE\n" "USAGE\n"
" tuxrecord [options]\n" " tuxrecord [options]\n"
@ -113,9 +124,11 @@ static void usage(void)
" -d <secs> Stop automatically after <secs> seconds (default: until q or Ctrl+C)\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" " -p <prefix> Filename prefix instead of \"%s\" (default: %s)\n"
" -D <device> ALSA capture device (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" " -h, --help Show this help\n"
"\n" "\n"
"The file is written as <prefix>__<timestamp>.wav\n" "The file is written as <prefix>__<timestamp>.<ext> where <ext> is wav,\n"
"flac or mp3 depending on -f/--format\n"
"\n" "\n"
"By default the ALSA capture device \"default\" is used, which is usually\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" "the microphone. To record system audio (everything playing on the\n"
@ -289,6 +302,62 @@ static void draw_meter(double db_l, double db_r, bool first)
fflush(stderr); 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 */ /* WAV file writer */
/* ------------------------------------------------------------------------ */ /* ------------------------------------------------------------------------ */
@ -447,6 +516,325 @@ fail:
return -1; 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 */ /* ALSA capture */
/* ------------------------------------------------------------------------ */ /* ------------------------------------------------------------------------ */
@ -599,17 +987,17 @@ static void restore_terminal(void)
/* ------------------------------------------------------------------------ */ /* ------------------------------------------------------------------------ */
/* /*
* Program entry point: parse options, record audio, write a WAV file. * Program entry point: parse options, record audio, encode the output.
* *
* Parses the command line (-o dir, -d seconds, -p prefix, -D device, * Parses the command line (-o dir, -d seconds, -p prefix, -D device,
* -h/--help), validates the output directory, builds the output filename * -f/--format, -h/--help), validates the output directory, builds the output
* "<dir>/<prefix>__<timestamp>.wav" and opens the ALSA capture device * filename "<dir>/<prefix>__<timestamp>.<ext>" and opens the ALSA capture
* and the WAV file. It then installs SIGINT/SIGTERM handlers and runs a * device and the output encoder. It then installs SIGINT/SIGTERM handlers
* poll() loop that reads captured PCM frames from ALSA and appends them * and runs a poll() loop that reads captured PCM frames from ALSA and feeds
* to the file, redrawing the VU meter on a tty stderr at most every * them to the encoder as they arrive, redrawing the VU meter on a tty
* VU_INTERVAL_S. The loop stops on 'q' (raw mode), Ctrl+C/SIGINT, * stderr at most every VU_INTERVAL_S. The loop stops on 'q' (raw mode),
* SIGTERM, the -d duration elapsing, or on error. Finally the WAV header * Ctrl+C/SIGINT, SIGTERM, the -d duration elapsing, or on error. Finally the
* is patched, the terminal restored and the result announced. * output is finalized, the terminal restored and the result announced.
* *
* Returns: * Returns:
* 0 on success, 1 on a runtime error, 2 on a command-line usage error. * 0 on success, 1 on a runtime error, 2 on a command-line usage error.
@ -619,6 +1007,7 @@ int main(int argc, char **argv)
const char *output_dir = "."; const char *output_dir = ".";
const char *prefix = DEFAULT_PREFIX; const char *prefix = DEFAULT_PREFIX;
const char *device = DEFAULT_DEVICE; const char *device = DEFAULT_DEVICE;
Format format = FORMAT_WAV;
double duration = 0.0; /* 0 = record until stopped */ double duration = 0.0; /* 0 = record until stopped */
struct stat st; struct stat st;
char path[4096]; char path[4096];
@ -631,7 +1020,8 @@ int main(int argc, char **argv)
unsigned channels = DEFAULT_CHANNELS; unsigned channels = DEFAULT_CHANNELS;
snd_pcm_sframes_t got; snd_pcm_sframes_t got;
struct sigaction sa; struct sigaction sa;
int fd, i, result = 0; Encoder enc;
int i, result = 0;
bool stderr_tty, use_stdin; bool stderr_tty, use_stdin;
uint64_t data_bytes = 0; uint64_t data_bytes = 0;
double start_t, last_draw; double start_t, last_draw;
@ -671,6 +1061,11 @@ int main(int argc, char **argv)
return 2; return 2;
} }
device = argv[i]; 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) { } else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage(); usage();
return 0; return 0;
@ -694,23 +1089,20 @@ int main(int argc, char **argv)
strcpy(stamp, "00000000_000000"); strcpy(stamp, "00000000_000000");
{ {
int need = snprintf(path, sizeof path, "%s/%s__%s.wav", int need = snprintf(path, sizeof path, "%s/%s__%s.%s",
output_dir, prefix, stamp); output_dir, prefix, stamp, format_extension(format));
if (need < 0 || need >= (int)sizeof path) { if (need < 0 || need >= (int)sizeof path) {
fputs("Error: output path too long\n", stderr); fputs("Error: output path too long\n", stderr);
return 1; return 1;
} }
} }
/* ---- audio device and output file -------------------------------- */ /* ---- audio device and output encoder ----------------------------- */
pcm = open_capture(device, &rate, &channels, &period); pcm = open_capture(device, &rate, &channels, &period);
if (pcm == NULL) if (pcm == NULL)
return 1; return 1;
fd = wav_open(path, rate, channels, DEFAULT_BITS); if (encoder_open(&enc, format, path, rate, channels, DEFAULT_BITS) != 0) {
if (fd < 0) {
fprintf(stderr, "Error: cannot open output file: %s: %s\n",
path, strerror(errno));
snd_pcm_close(pcm); snd_pcm_close(pcm);
return 1; return 1;
} }
@ -725,13 +1117,13 @@ int main(int argc, char **argv)
use_stdin = isatty(STDIN_FILENO); use_stdin = isatty(STDIN_FILENO);
logmsg("Recording to %s", path); logmsg("Recording to %s", path);
logmsg("Using ALSA device '%s' at %u Hz, %u channel(s), %d-bit", logmsg("Using ALSA device '%s' at %u Hz, %u channel(s), %d-bit, %s format",
device, rate, channels, DEFAULT_BITS); device, rate, channels, DEFAULT_BITS, format_extension(format));
logmsg("Press q or Ctrl+C to stop."); logmsg("Press q or Ctrl+C to stop.");
if (snd_pcm_start(pcm) < 0) { if (snd_pcm_start(pcm) < 0) {
logmsg("Error: could not start capture on '%s'", device); logmsg("Error: could not start capture on '%s'", device);
close(fd); encoder_close(&enc, data_bytes);
snd_pcm_close(pcm); snd_pcm_close(pcm);
return 1; return 1;
} }
@ -752,7 +1144,7 @@ int main(int argc, char **argv)
if (samples == NULL) { if (samples == NULL) {
logmsg("Error: out of memory"); logmsg("Error: out of memory");
restore_terminal(); restore_terminal();
wav_finalize(fd, data_bytes); encoder_close(&enc, data_bytes);
snd_pcm_close(pcm); snd_pcm_close(pcm);
return 1; return 1;
} }
@ -765,7 +1157,7 @@ int main(int argc, char **argv)
logmsg("Error: out of memory"); logmsg("Error: out of memory");
free(samples); free(samples);
restore_terminal(); restore_terminal();
wav_finalize(fd, data_bytes); encoder_close(&enc, data_bytes);
snd_pcm_close(pcm); snd_pcm_close(pcm);
return 1; return 1;
} }
@ -849,13 +1241,11 @@ int main(int argc, char **argv)
size_t bytes = (size_t)got * channels * sizeof(int16_t); size_t bytes = (size_t)got * channels * sizeof(int16_t);
double meter_now; double meter_now;
if (data_bytes + bytes > WAV_DATA_MAX) { if (data_bytes + bytes > encoder_max_data_bytes(&enc)) {
logmsg("Warning: 4 GiB WAV limit reached, stopping."); logmsg("Warning: 4 GiB WAV limit reached, stopping.");
break; break;
} }
if (write_all(fd, samples, bytes) != 0) { if (encoder_write(&enc, samples, (size_t)got) != 0) {
logmsg("Error: failed to write audio: %s",
strerror(errno));
result = 1; result = 1;
break; break;
} }
@ -881,8 +1271,8 @@ int main(int argc, char **argv)
restore_terminal(); restore_terminal();
logmsg("\nRecording ended."); logmsg("\nRecording ended.");
if (wav_finalize(fd, data_bytes) != 0) { if (encoder_close(&enc, data_bytes) != 0) {
logmsg("Error: could not finalize WAV file %s", path); logmsg("Error: could not finalize output file %s", path);
result = 1; result = 1;
} else if (result == 0) { } else if (result == 0) {
logmsg("Saved %s", path); logmsg("Saved %s", path);