hw:0,0 is an exclusive ALSA device — darkice holding it caused arecord to fail silently (stderr was /dev/null), leaving all recordings at 0 bytes with no errors in the log. asound.conf: defines a dsnoop virtual device 'shared_mic' that opens hw:0,0 once and lets multiple processes capture simultaneously. docker-compose.yml: mount asound.conf into the container as /etc/asound.conf; add ipc: host so the container shares the host IPC namespace (dsnoop uses System V shared memory which does not cross the container IPC boundary without this). config.example.ini: document the dsnoop setup and shared-device pattern. README, CLAUDE.md: document the full setup procedure.
5.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Rules
- Always update
README.mdwhenever user-facing behaviour changes (new flags, new endpoints, changed Docker setup, new features). The README is the primary external reference; CLAUDE.md documents internals. - Always commit
README.mdin the same commit as the code changes it documents — never let the README fall behind.
Project Overview
ISR is a Python audio recording application that captures from multiple simultaneous sources (Icecast/HTTP streams and ALSA soundcard devices) with time-based file splitting. All application code is in two files: isr.py (recorder) and web.py (archive browser UI).
Commands
# Run the recorder
python isr.py # uses config.ini
python isr.py myconfig.ini # custom config file
python isr.py --list-devices # list available ALSA devices
# Run the web UI
python web.py # http://localhost:8080
python web.py --dir recordings # custom recordings directory
python web.py --port 8888 # custom port
python web.py --threshold 0.03 # loudness threshold (0-1, default 0.05)
# Stop: Ctrl+C (or docker compose down)
# Install dependencies
pip install requests # for stream recording
pip install numpy soundfile # for FLAC output and web waveform analysis (optional)
# Docker
docker compose up -d
docker compose logs -f
docker compose down
Architecture
Audio Backend System
- AudioDevice — Dataclass: id, name, channels, sample_rate, backend type
- AudioBackend (ABC) — Abstract base for audio capture backends
- ALSABackend — Native ALSA support via
arecordsubprocess (the only backend)
- ALSABackend — Native ALSA support via
- ALSAStream — Context manager that wraps an
arecordsubprocess and reads PCM in a thread - AudioSystem — Discovers available backends, lists devices, resolves device specs
Recorder Classes
- BaseRecorder (ABC) — Common settings,
get_next_split_time(),generate_filename(),record()interface - StreamRecorder(BaseRecorder) — Records HTTP/Icecast streams with format auto-detection and OGG/FLAC header injection
- SoundcardRecorder(BaseRecorder) — Records from ALSA devices; outputs WAV or FLAC via
_AudioFileWriter - _AudioFileWriter — Unified write/close interface for wave (WAV) and soundfile (FLAC)
- RecorderManager — Loads config, creates recorders, manages threads, handles shutdown
Key Implementation Details
- ALSA backend spawns
arecordas a subprocess; raw PCM is read in 100 ms chunks via a reader thread - Device selection:
default,monitor(loopback), partial name match, or exacthw:X,YID - Thread-safe audio buffering with
threading.Lock() - OGG/Opus/FLAC headers captured from first ~16 KB of stream and prepended to each split file
- File splits aligned to time period boundaries (
get_next_split_time()) - SIGTERM handled in
main()so Dockerdocker compose downshuts down cleanly RecorderManager._write_status()atomically writesrecordings/status.jsonevery 2 s while running; deleted on clean shutdown so the web UI shows no stale active-recording badges
Web UI (web.py)
GET /— Single-page archive table; lists all recordings sorted newest firstGET /api/files— JSON list of file metadata (name, size, date, duration, ext, recording flag)GET /api/analyze?file=<path>— RMS loudness analysis for WAV and FLAC files; returns waveform data, loud sections, and duration. Requiresnumpyandsoundfilefor FLAC.GET /api/status— Returns{"active": [...]}fromstatus.json; used by the UI to animate the REC badge on in-progress files (polled every 5 s)GET /stream/<path>— Serves audio for inline<audio>playback with full HTTP Range support (seekable). Responds 206 Partial Content for range requests. Files are served withContent-Disposition: inline.GET /download/<path>— Serves audio as a file download (Content-Disposition: attachment)- All paths are validated against the recordings directory to prevent path traversal.
Configuration
Copy config.example.ini to config.ini. Each section defines a recording source:
type = stream— HTTP/Icecast stream recordingtype = soundcard— ALSA device recording
The output_directory value is used as-is: a relative path like recordings resolves to recordings/ next to isr.py. No Docker-specific config change is needed — the docker-compose.yml mounts ./recordings at /app/recordings to match this default.
Docker
Two services share a ./recordings bind mount:
recorder— runsisr.py; volume at/app/recordings; mountsasound.confas/etc/asound.conf; maps/dev/snd;ipc: hostfor dsnoop shared memory;stop_grace_period: 30sweb— runsweb.py; same./recordingsread-only at/recordings; exposes port 8080
Sharing the soundcard with darkice (or any other ALSA app):
ALSA hw: devices are exclusive. asound.conf defines a dsnoop virtual device shared_mic that both processes use instead:
sudo cp asound.conf /etc/asound.confon the host- Change darkice config to
device = shared_mic - Set
device = shared_micinconfig.ini ipc: hostindocker-compose.ymlis already set — required for dsnoop shared memory to cross the container boundary