Initial commit.
This commit is contained in:
commit
efee1b8dbb
5 changed files with 1068 additions and 0 deletions
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Built binary
|
||||||
|
tuxrecord
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
recordings/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# IDE ignores
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.wav
|
||||||
13
LICENSE
Normal file
13
LICENSE
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright 2025 Johannes Findeisen
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
33
Makefile
Normal file
33
Makefile
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c99 -O2 -Wall -Wextra
|
||||||
|
PKGCONF = pkg-config
|
||||||
|
ALSA_CFLAGS := $(shell $(PKGCONF) --cflags alsa 2>/dev/null)
|
||||||
|
ALSA_LIBS := $(shell $(PKGCONF) --libs alsa 2>/dev/null)
|
||||||
|
ifeq ($(strip $(ALSA_LIBS)),)
|
||||||
|
ALSA_LIBS := -lasound
|
||||||
|
endif
|
||||||
|
LDLIBS = $(ALSA_LIBS) -lm
|
||||||
|
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
BINDIR = $(PREFIX)/bin
|
||||||
|
|
||||||
|
BINARY = tuxrecord
|
||||||
|
SRC = tuxrecord.c
|
||||||
|
|
||||||
|
all: $(BINARY)
|
||||||
|
|
||||||
|
$(BINARY): $(SRC)
|
||||||
|
$(CC) $(CFLAGS) $(ALSA_CFLAGS) -o $@ $< $(LDLIBS)
|
||||||
|
|
||||||
|
install: $(BINARY)
|
||||||
|
install -d $(DESTDIR)$(BINDIR)
|
||||||
|
install -m 755 $(BINARY) $(DESTDIR)$(BINDIR)/$(BINARY)
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(DESTDIR)$(BINDIR)/$(BINARY)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY)
|
||||||
|
|
||||||
|
.PHONY: all clean install uninstall
|
||||||
|
|
||||||
115
README.md
Normal file
115
README.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# tuxrecord
|
||||||
|
|
||||||
|
Record audio to a WAV file on **Linux** using **ALSA** as the recording source.
|
||||||
|
|
||||||
|
`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
|
||||||
|
capture API (`libasound`); it builds with **GCC**.
|
||||||
|
|
||||||
|
The port is derived from [macrecord](https://codeberg.org/armin/macrecord)
|
||||||
|
which was created by my good friend armin.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- GCC and GNU make
|
||||||
|
- ALSA development headers (`libasound2-dev` on Debian/Ubuntu,
|
||||||
|
`alsa-lib-devel` on Fedora, `alsa-lib` on Arch)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Record until q or Ctrl+C
|
||||||
|
tuxrecord
|
||||||
|
|
||||||
|
# Record for N seconds
|
||||||
|
tuxrecord -d 30
|
||||||
|
|
||||||
|
# Specify output directory
|
||||||
|
tuxrecord -o /path/to/dir
|
||||||
|
|
||||||
|
# Use a custom filename prefix
|
||||||
|
tuxrecord -p myrec
|
||||||
|
|
||||||
|
# Use a specific ALSA capture device
|
||||||
|
tuxrecord -D hw:1,0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -------------- | -------------------------------------------------------- |
|
||||||
|
| `-o <dir>` | Output directory (default: current directory) |
|
||||||
|
| `-d <secs>` | Stop automatically after `<secs>` seconds |
|
||||||
|
| `-p <prefix>` | Filename prefix instead of `tuxrecord` |
|
||||||
|
| `-D <device>` | ALSA capture device (default: `default`) |
|
||||||
|
| `-h, --help` | Show help |
|
||||||
|
|
||||||
|
Run `arecord -l` to list available ALSA capture devices.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **q** - Stop recording and save the file
|
||||||
|
- **Ctrl+C** - Stop recording and save the file
|
||||||
|
|
||||||
|
The file is written as `<prefix>__<timestamp>.wav` (default prefix
|
||||||
|
`tuxrecord`). The VU meter shows the input level of the left and right channels
|
||||||
|
in real time when stderr is a terminal.
|
||||||
|
|
||||||
|
## Recording system audio
|
||||||
|
|
||||||
|
The default ALSA capture device (`default`) is usually the microphone. To
|
||||||
|
record **system audio** (everything playing on the machine), route playback
|
||||||
|
through the ALSA loopback driver `snd-aloop` and capture from the other side
|
||||||
|
of the loop.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Load the loopback module (once per boot)
|
||||||
|
sudo modprobe snd-aloop
|
||||||
|
|
||||||
|
# 2. Play audio on the "playback" side of the loop
|
||||||
|
aplay -D hw:Loopback,0 song.wav
|
||||||
|
|
||||||
|
# 3. In another terminal, record from the paired "capture" side
|
||||||
|
tuxrecord -D hw:Loopback,1
|
||||||
|
```
|
||||||
|
|
||||||
|
Audio written to `hw:Loopback,0` (playback) is heard on `hw:Loopback,1`
|
||||||
|
(capture): the two subdevices form a loop and the samples pass straight
|
||||||
|
through. Point your players at `hw:Loopback,0` (default sink) and record from
|
||||||
|
`hw:Loopback,1` to capture the system mix. Subdevice pairing depends on the
|
||||||
|
kernel version; if one direction is silent, try recording from
|
||||||
|
`hw:Loopback,0` while playing to `hw:Loopback,1`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
# or directly:
|
||||||
|
# gcc -std=c99 -O2 -Wall -Wextra tuxrecord.c -o tuxrecord $(pkg-config --libs alsa) -lm
|
||||||
|
```
|
||||||
|
|
||||||
|
Install (installs to `$(PREFIX)/bin`, default `/usr/local/bin`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo make install
|
||||||
|
# or: sudo cp tuxrecord /usr/local/bin/tuxrecord
|
||||||
|
```
|
||||||
|
|
||||||
|
Install to a custom prefix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install PREFIX=~/.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Uninstall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo make uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License 2.0 — see the LICENSE file for details.
|
||||||
|
|
||||||
|
Copyright 2026 Johannes Findeisen
|
||||||
|
|
||||||
889
tuxrecord.c
Normal file
889
tuxrecord.c
Normal file
|
|
@ -0,0 +1,889 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* The port is derived from [macrecord](https://codeberg.org/armin/macrecord)
|
||||||
|
* which was created by my good friend armin.
|
||||||
|
*
|
||||||
|
* 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",
|
||||||
|
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, <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.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;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue