Files
admin e67e27f047 fix: share soundcard between darkice and ISR via ALSA dsnoop
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.
2026-04-26 14:21:31 +02:00

94 lines
5.4 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Rules
- **Always update `README.md`** whenever 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.md`** in 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
```bash
# 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 `arecord` subprocess (the only backend)
- **ALSAStream** — Context manager that wraps an `arecord` subprocess 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 `arecord` as a subprocess; raw PCM is read in 100 ms chunks via a reader thread
- Device selection: `default`, `monitor` (loopback), partial name match, or exact `hw:X,Y` ID
- 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 Docker `docker compose down` shuts down cleanly
- `RecorderManager._write_status()` atomically writes `recordings/status.json` every 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 first
- **`GET /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. Requires `numpy` and `soundfile` for FLAC.
- **`GET /api/status`** — Returns `{"active": [...]}` from `status.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 with `Content-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 recording
- `type = 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` — runs `isr.py`; volume at `/app/recordings`; mounts `asound.conf` as `/etc/asound.conf`; maps `/dev/snd`; `ipc: host` for dsnoop shared memory; `stop_grace_period: 30s`
- `web` — runs `web.py`; same `./recordings` read-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:
1. `sudo cp asound.conf /etc/asound.conf` on the host
2. Change darkice config to `device = shared_mic`
3. Set `device = shared_mic` in `config.ini`
4. `ipc: host` in `docker-compose.yml` is already set — required for dsnoop shared memory to cross the container boundary