f84b36687b
- Remove stale instruction to set output_directory = /recordings in Docker - Document that docker compose down + up --build is required when updating - Fix output_directory and log_file table descriptions - Expand Web UI section: inline playback, FLAC waveform, live REC badge, WCAG - Fix Docker notes: log_file path and log_file in Docker guidance - Add rule to CLAUDE.md: always update and commit README.md with code changes
200 lines
7.6 KiB
Markdown
200 lines
7.6 KiB
Markdown
# ISR — Audio Recorder
|
||
|
||
> AI-generated code. Run at your own risk. MIT licence.
|
||
|
||
Records from multiple simultaneous sources — Icecast/HTTP streams and ALSA soundcards — with time-based file splitting.
|
||
|
||
## Features
|
||
|
||
- Multiple sources recorded in parallel (each in its own thread)
|
||
- **Stream recording** — HTTP/Icecast, auto-detects MP3 / OGG / AAC / FLAC / Opus from `Content-Type`
|
||
- **Soundcard recording** — ALSA (`arecord`), works on any Linux / Raspberry Pi
|
||
- Time-aligned file splits (e.g. every hour, on the hour)
|
||
- OGG / Opus / FLAC header injection so every split file is independently playable
|
||
- Auto-reconnect on stream drops or device errors
|
||
- WAV and FLAC output for soundcard sources
|
||
- Web UI to browse, download, and analyse recordings
|
||
|
||
---
|
||
|
||
## Quick start — bare-metal
|
||
|
||
```bash
|
||
pip install requests # stream recording
|
||
pip install numpy soundfile # FLAC output + web waveform analysis (optional)
|
||
|
||
cp config.example.ini config.ini
|
||
# edit config.ini to add your sources
|
||
python isr.py # start recorder (Ctrl+C to stop)
|
||
python web.py # start web UI at http://localhost:8080
|
||
```
|
||
|
||
## Quick start — Docker
|
||
|
||
```bash
|
||
cp config.example.ini config.ini
|
||
# edit config.ini to add your sources (no path changes needed for Docker)
|
||
|
||
docker compose up -d
|
||
# recorder starts immediately; web UI at http://<host>:8080
|
||
docker compose logs -f # tail logs from both services
|
||
docker compose down # graceful stop (waits up to 30 s for files to close)
|
||
```
|
||
|
||
Recordings land in `./recordings/` on the host (bind-mounted into both containers at different internal paths — no config changes required vs. bare-metal).
|
||
|
||
If you only record streams (no soundcard), comment out the `devices` block in `docker-compose.yml`.
|
||
|
||
**Updating a running deployment:**
|
||
```bash
|
||
git pull
|
||
docker compose down
|
||
docker compose up -d --build
|
||
```
|
||
`docker compose down` is required — Docker won't apply changed volume mounts to existing containers without recreating them.
|
||
|
||
---
|
||
|
||
## Configuration
|
||
|
||
`config.ini` uses standard INI format. `[general]` provides defaults; every other section is a recording source. Source sections inherit all general settings and can override any of them.
|
||
|
||
### `[general]`
|
||
|
||
| Key | Default | Description |
|
||
|-----|---------|-------------|
|
||
| `output_directory` | `recordings` | Output path relative to the working directory (or absolute). The Docker setup mounts `./recordings` at `/app/recordings` so this default works unchanged. |
|
||
| `split_minutes` | `60` | Split into a new file every N minutes, aligned to clock boundaries (e.g. 60 → files start at :00, 30 → at :00 and :30). |
|
||
| `filename_pattern` | `%Y%m%d_%H%M%S` | strftime pattern; file extension is appended automatically. |
|
||
| `max_retries` | `10` | Give up after this many consecutive failures per source. |
|
||
| `retry_delay_seconds` | `5` | Wait between retries. |
|
||
| `log_level` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL` |
|
||
| `log_file` | `recorder.log` | Log file path. In Docker, logs also go to stdout and are visible via `docker compose logs`. |
|
||
|
||
### `type = stream`
|
||
|
||
```ini
|
||
[my_stream]
|
||
type = stream
|
||
url = http://icecast.example.com:8000/live
|
||
username = # leave blank for public streams
|
||
password =
|
||
format = auto # auto | mp3 | ogg | aac | flac | opus
|
||
```
|
||
|
||
`format = auto` detects from the `Content-Type` response header. For OGG/Opus/FLAC the first ~16 KB of each connection is buffered to extract codec headers, which are then prepended to every split file — all files are independently playable.
|
||
|
||
A new file is always opened on (re)connect so gaps between connections are never silently merged.
|
||
|
||
### `type = soundcard`
|
||
|
||
```ini
|
||
[mic_in]
|
||
type = soundcard
|
||
device = default # see device selection below
|
||
sample_rate = 44100
|
||
channels = 2
|
||
format = wav # wav | flac
|
||
```
|
||
|
||
**Device selection:**
|
||
|
||
| Value | Behaviour |
|
||
|-------|-----------|
|
||
| `default` | System default input |
|
||
| `monitor` | First loopback/monitor source (capture system audio) |
|
||
| `<partial name>` | Case-insensitive substring match against device name |
|
||
| `hw:X,Y` | Exact ALSA hardware ID |
|
||
|
||
Run `python isr.py --list-devices` (or `arecord -l`) to see available devices and their IDs.
|
||
|
||
FLAC output requires `pip install soundfile numpy`.
|
||
|
||
### Multiple sources
|
||
|
||
Every section except `[general]` is a source — they all record simultaneously:
|
||
|
||
```ini
|
||
[general]
|
||
output_directory = recordings
|
||
split_minutes = 60
|
||
|
||
[radio1]
|
||
type = stream
|
||
url = http://radio.example.com:8000/stream1
|
||
filename_pattern = radio1_%Y%m%d_%H%M%S
|
||
|
||
[system_audio]
|
||
type = soundcard
|
||
device = hw:0,0
|
||
filename_pattern = system_%Y%m%d_%H%M%S
|
||
```
|
||
|
||
---
|
||
|
||
## Filename patterns
|
||
|
||
strftime codes are substituted at split time. The file extension is added automatically.
|
||
|
||
| Pattern | Example |
|
||
|---------|---------|
|
||
| `%Y%m%d_%H%M%S` | `20241225_143000.mp3` |
|
||
| `radio_%Y-%m-%d_%H%M` | `radio_2024-12-25_1430.mp3` |
|
||
| `%Y/%m/%d/rec_%H%M%S` | `2024/12/25/rec_143000.mp3` *(subdirs created automatically)* |
|
||
|
||
---
|
||
|
||
## Web UI (`web.py`)
|
||
|
||
```bash
|
||
python web.py # serves ./recordings on port 8080
|
||
python web.py --dir /path/to/audio # custom recordings directory
|
||
python web.py --port 8888 # custom port
|
||
python web.py --threshold 0.03 # loudness threshold 0–1 (default 0.05)
|
||
```
|
||
|
||
Shows a table of all recordings sorted newest-first. Features:
|
||
|
||
- **Inline playback** — collapsible `▶ Play` button per row; audio loads lazily via a seekable `/stream/` endpoint with HTTP Range support.
|
||
- **Waveform analysis** — on demand per file; computes RMS per 100 ms window and highlights loud sections. Supported for WAV and FLAC (FLAC requires `numpy` + `soundfile`). Pure-Python fallback for WAV when numpy is absent.
|
||
- **Live REC badge** — files currently being written by `isr.py` show an animated REC indicator, polled every 5 seconds via `/api/status`.
|
||
- **WCAG-compliant** — skip link, `aria-expanded`/`aria-controls` on the player toggle, `aria-live` status, focus management, `role=img` on SVG waveforms.
|
||
|
||
---
|
||
|
||
## How it works
|
||
|
||
**Streams:** Connect via HTTP → detect format from `Content-Type` → buffer first ~16 KB to extract OGG/FLAC codec headers → stream raw bytes to disk → at each split boundary open a new file and prepend the saved headers. No transcoding, no decoding — raw bytes in, raw bytes out.
|
||
|
||
**Soundcard:** Spawn `arecord` as a subprocess (raw PCM output) → read 100 ms chunks via a thread → write 16-bit PCM to WAV or FLAC → split at configured boundaries.
|
||
|
||
Both recorder types run in separate threads and retry independently up to `max_retries`.
|
||
|
||
---
|
||
|
||
## Docker notes
|
||
|
||
**ALSA device access:** The `recorder` container needs `/dev/snd` mapped. The container runs as root, so no group configuration is needed — the device mapping alone is sufficient.
|
||
|
||
If ALSA still fails to find the device inside the container, verify the device exists on the host:
|
||
```bash
|
||
arecord -l # list capture hardware
|
||
ls -la /dev/snd # check device nodes
|
||
```
|
||
|
||
**Stream-only deployments:** If you don't use soundcard recording, remove the `devices` block from `docker-compose.yml` — the image works fine without it.
|
||
|
||
**Log file in Docker:** The recorder always logs to stdout, so `docker compose logs -f` shows live output. To persist logs on the host, set `log_file = /app/recordings/recorder.log` in `config.ini` (the `recordings` directory is the bind mount).
|
||
|
||
**File retention:** ISR never deletes recordings. Add a cron job on the host if needed:
|
||
```bash
|
||
# Delete recordings older than 30 days
|
||
find recordings/ -type f -mtime +30 -delete
|
||
```
|
||
|
||
---
|
||
|
||
## Licence
|
||
|
||
MIT
|