diff --git a/CLAUDE.md b/CLAUDE.md index 38a8e4f..56e0ff2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,93 +1,40 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Guidance for Claude Code when working 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. +- **Always update `README.md`** when user-facing behaviour changes (flags, endpoints, Docker setup, features), and **commit it in the same commit** as the code change. README is the external reference; CLAUDE.md documents internals. +- Run `python -m pytest tests/` after changing `isr.py` (tests cover the recorder only). -## Project Overview +## Files -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). +| File | Purpose | +|------|---------| +| `isr.py` | Recorder: streams (Icecast/HTTP) + ALSA soundcards, time-aligned file splits | +| `web.py` | Archive browser: HTTP server, file listing, RMS loudness analysis, cut/delete | +| `webui.html` | Single-page UI (HTML/CSS/JS), loaded by `web.py` at startup — must sit next to `web.py` and be copied in the Dockerfile | +| `config.ini` | Recording sources; copy from `config.example.ini`. `[general]` gives defaults, every other section is a source (`type = stream` or `type = soundcard`) | +| `asound.conf` | dsnoop device `shared_mic` so ISR and other ALSA apps can share a soundcard | ## 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 +python isr.py [config.ini] # recorder; --list-devices to list ALSA inputs +python web.py # web UI on :8080 (--dir, --port, --threshold, --min-gap, --analyses-dir) +python -m pytest tests/ # test suite +docker compose up -d / down # web UI mapped to host port 8050 ``` -## Architecture +Dependencies: `requests` (streams), `numpy` + `soundfile` (FLAC output and FLAC/waveform analysis — both optional, code degrades gracefully). -### 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 +## Non-obvious internals -### 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=`** — 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/`** — Serves audio for inline `