Holding Shift with the section-navigation keys jumps straight to the extreme in that direction instead of stepping one at a time: - Shift+J / Shift+K -> first / last section in time order - Shift+U / Shift+I -> loudest / quietest section Works in both clip-queue navigation (stepClip gains a jump flag) and in-player full-file navigation. Visible key-hint note and README updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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, play, cut, and download recordings — including files still being written
- Loud-event detection — adaptive: events are flagged by how far they rise above the rolling background noise, not by absolute level, and ranked by score
- Clip review — detected sections play as instant server-rendered clips with auto-advance, so a whole day can be reviewed like a highlights reel
Quick start — bare-metal
pip install requests # stream recording
pip install numpy soundfile # FLAC output + web analysis/clips for FLAC (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
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>:8050
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:
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). |
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
[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
[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 |
<partial name> |
Case-insensitive substring match against device name |
hw:X,Y |
Exact ALSA hardware ID |
<pcm name> |
Any ALSA PCM defined in asound.conf (e.g. the shared_mic dsnoop device), even if it doesn't appear in arecord -l |
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:
[general]
output_directory = recordings
split_minutes = 60
[radio1]
type = stream
url = http://radio.example.com:8000/stream1
[system_audio]
type = soundcard
device = hw:0,0
Filenames
Recordings are named <YYYYMMDD>_<HHMMSS>.<ext> from the time the file is opened — its start time — e.g. 20241225_143000.flac. This format is fixed and not configurable: the web UI parses the start time back out of the filename to show the recording date and to name cut clips with real wall-clock times (see below).
Web UI (web.py)
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 --margin 15 # dB above background noise for a section to count as loud (default 12)
python web.py --min-gap 15 # grace period in seconds for merging loud sections (default 2)
python web.py --min-duration 1 # discard loud sections shorter than this many seconds (default 0.5)
python web.py --analyses-dir /path/to/dir # where to store analysis cache files (default: <recordings>/analyses)
The browser UI (HTML/CSS/JS) lives in webui.html, which web.py loads at startup — keep the two files together.
Shows recordings grouped by day with collapsible sections. Features:
- Day groups — recordings are grouped under a collapsible day heading showing date, file count, total duration, and total size. The most recent day is expanded by default; older days start collapsed. Expanded state is preserved across filter changes.
- Day highlights — click Highlights on any day heading to run loudness analysis across all WAV/FLAC files in that day. The button is a toggle: clicking again collapses the panel, and re-expanding it reuses the already-computed results (they are only recomputed when the analysis parameters change). A · analysed suffix on the button marks days where every file already has a cached analysis for the current parameters, i.e. highlights open instantly. The panel is plain text and buttons in linear reading order (screen-reader friendly): files-analysed/section totals, a key hint, then the section chips. When a day has more sections than fit as chips, the chips are the top 50 by score (loudest-above-background first) so the most promising events are reviewed first; long chip lists are collapsed behind a toggle button so the panel stays compact. J/K still steps through all sections in time order, and U/I steps through them by loudness. Holding Shift jumps straight to an extreme: Shift+J/Shift+K to the first/last section in time, Shift+U/Shift+I to the loudest/quietest.
- Inline playback — collapsible
Playbutton per row; audio loads lazily via a seekable/stream/endpoint with HTTP Range support. Metadata is fetched immediately so the duration is visible without pressing play. - Loudness analysis — on demand per file; computes RMS per 100 ms window and lists sections that stand out above the background as clickable chips (no visual waveform — the UI is built for screen-reader use). Detection is adaptive: a rolling noise floor (20th percentile per 30 s block) is estimated across the file, and a section is flagged when the level rises at least margin dB (default 12) above that floor. Slow ambience changes — rain setting in, day/night traffic hum — move the floor instead of producing false positives. Each section gets a score used to rank it: its peak dB above the floor, capped by the sharpest rise within 0.5 s. Abrupt events — voices, impacts, barks — rise fast, so their score is their full prominence; a gradual swell (a gust, a distant approaching car) that drifts up faster than the floor can track still gets flagged, but scores near zero and sinks to the bottom of the highlight ranking. Supported for WAV and FLAC (FLAC requires
numpy+soundfile). Pure-Python fallback for WAV when numpy is absent. Results are cached inrecordings/analyses/<filename>.analysis.json; subsequent requests at the same margin, min-gap, and min-duration settings return instantly without re-reading the audio. The cache file is deleted automatically when the audio file is deleted. Orphaned cache files (audio deleted outside the UI) are pruned on startup. - Grace period — configurable in the controls bar (default 2 s). Loud sections separated by less than this gap are merged into one. Raise this (e.g. to 15–30 s) when a single event generates many timestamps due to brief quiet gaps within it.
- Min duration — configurable in the controls bar (default 0.5 s). Loud sections shorter than this (after grace-period merging) are discarded, so isolated sub-second pops — a click, a single raindrop — don't flood a day with thousands of near-zero-length sections. Set to 0 to disable.
- Clip playback — clicking a loud-section chip plays a short server-rendered WAV clip (
/api/clip, pre-roll included) in a player bar at the bottom of the page. Playback starts instantly even for sections deep inside multi-hundred-MB FLACs, because the browser never has to seek the full file. The player bar labels each clip with the wall-clock time it occurred (derived from the recording's filename) and its position in the queue, e.g.03:46:20 to 03:46:22 (73 / 187); the filename and score are in the label's hover tooltip. J / K (or the Prev / Next buttons) always step through the queued sections in time order — one file's, or a whole day's after Highlights. U / I step through the same queue by loudness instead: I plays the next-loudest section, U goes back up the ranking, so a day with thousands of detections is reviewed loudest-first for as long as it stays interesting — there is no top-N cutoff, just stop when it gets boring. The auto-advance radio in the player bar picks what happens when a clip ends: Don't auto-advance, Auto-advance (next section in time), or Auto-advance highlights only (next section by loudness) — it never changes what J/K do. Holding Shift with any of these jumps to the extreme in that direction — Shift+J/Shift+K to the first/last section in time, Shift+U/Shift+I to the loudest/quietest. The same keys work during full-file playback, seeking the open recording to the next section in time (J/K) or by loudness (U/I). Open in file (or the O key) switches to the full recording at the same position for context; each chip click also pre-fills the cut panel. - Cut & download —
Cutbutton opens the player row and reveals a cut panel. Enter start and end times inm:ssorh:mm:ssformat and click Download cut to receive an ffmpeg-trimmed copy without re-encoding. Requires ffmpeg (included in the Docker image). The cut is named with the real wall-clock span it covers —<YYYYMMDD>_<HH-MM-SS>_<HH-MM-SS>.<ext>, e.g. a 22:31:30→22:32:30 slice of a recording started at 22:00:00 becomes20260523_22-31-30_22-32-30.flac. - Filters — live filename search and from/to date pickers above the table; applied client-side with no additional requests. Shows
N of M shownwhen a filter is active. - Delete —
Deletebutton per row with confirmation prompt; disabled for files currently being recorded; sendsDELETE /api/files/<name>and re-renders the table. - Live REC badge — files currently being written by
isr.pyshow an animated REC indicator, polled every 5 seconds via/api/status. Duration for in-progress files shows—in the table (header is unfinalized until recording stops). The file list refreshes automatically when a recording starts, stops, or rolls over to a new split file (unless audio is playing). - Listen while recording — in-progress files are playable and seekable. For WAV and FLAC the server patches the (still unfinalized) header on the fly so the browser sees the real duration-so-far — for FLAC the exact sample count is parsed from the last frame header in the file tail. Reopening the player reloads the source to pick up newly recorded audio. Live responses are sent with
Cache-Control: no-store. - Fast loading — analysis results are cached server-side on disk and client-side per session; cached analyses load only for expanded day groups, and collapsed days fetch nothing until opened.
- WCAG-compliant — skip link,
aria-expanded/aria-controlson the player toggle,aria-livestatus, focus management; all information is plain text and buttons in linear reading order, with no visual-only elements.
Too many sections per day?
The detector is purely energy-based: anything that rises margin dB above the rolling background floor for at least min duration seconds is flagged. In a quiet environment the floor sits very low, so even modest sounds — a gust, a distant car, rustling — clear the margin, and an outdoor day can easily produce over a thousand sections. What to adjust, in rough order of preference:
- Review by rank instead of thinning the list — use U/I (or Auto-advance highlights only) to step through the sections loudest-first. Scores favour sharp onsets, so slow ambience swells sort themselves to the bottom automatically. Nothing is discarded and there is no cutoff — just stop when the events stop being interesting.
- Raise the grace period (2 → 15–30 s) — merges clusters of related noise (a rain burst, one long conversation) into a single section. Cuts the count heavily without dropping any audio from review.
- Raise the margin (12 → 15–18 dB) — demands more prominence above the background. The quietest events disappear first, so move in small steps.
- Raise min duration (0.5 → 1–2 s) — drops short rustles and pops, but beware: single bangs or knocks are themselves short.
Changing any of these re-runs the analysis (results are cached per parameter combination), so experimenting on one day before re-analysing everything is cheapest.
HTTP API
Everything the UI does goes through these endpoints, so they can also be scripted:
| Endpoint | Description |
|---|---|
GET /api/files |
File listing with size, mtime, duration, recording state, cached-analysis params |
GET /api/analyze?file=&margin=&min_gap=&min_duration= |
Loud-section analysis: scored sections, duration |
GET /api/clip?file=&start=&end= |
Section of a WAV/FLAC decoded server-side, returned as a standalone WAV (max 600 s) |
GET /api/cut?file=&start=&end= |
ffmpeg-trimmed copy of the file as a download |
GET /stream/<name> |
Inline playback with HTTP Range support; live files get an on-the-fly patched header |
GET /download/<name> |
Raw file download |
GET /api/status |
Currently recording files (status.json passthrough) |
GET /api/storage |
Disk free/total |
GET /api/config |
Server-side defaults for margin, min-gap, and min-duration (seeds the UI controls) |
DELETE /api/files/<name> |
Delete a recording and its analysis cache |
Analysis, clips, cut, and delete return 409 for files that are still being recorded.
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:
arecord -l # list capture hardware
ls -la /dev/snd # check device nodes
Sharing the soundcard with another app (e.g. darkice): ALSA hw: devices are exclusive — only one process can hold them at a time. asound.conf in this repo defines a dsnoop virtual device (shared_mic) that lets multiple processes capture simultaneously:
# 1. Deploy the ALSA config to the host (once)
sudo cp asound.conf /etc/asound.conf
# 2. Change darkice (or any other app) to use device "shared_mic" instead of hw:0,0
# 3. In config.ini set: device = shared_mic
# docker-compose.yml already mounts asound.conf and sets ipc: host
# (ipc: host is required so the container shares the host IPC namespace for dsnoop shared memory)
# 4. Restart everything
sudo systemctl restart darkice
docker compose down && docker compose up -d --build
Stream-only deployments: If you don't use soundcard recording, remove the devices block and the asound.conf volume mount and ipc: host line from docker-compose.yml — the image works fine without them.
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).
Analysis cache in Docker: The web container mounts ./recordings read-only; a separate read-write bind mount of ./recordings/analyses is layered on top so analysis cache files can still be written. The directory is created automatically by Docker Compose on first run. Cache files are stored as recordings/analyses/<filename>.analysis.json on the host.
File retention: Individual recordings can be deleted from the web UI. For bulk / automated cleanup, add a cron job on the host:
# Delete recordings older than 30 days
find recordings/ -type f -mtime +30 -delete
Licence
MIT