ALSAStream._read_loop just returned when arecord died or hit EOF, and
__exit__ never checked the exit status. SoundcardRecorder.record's inner
loop only slept and flushed, so nothing ever noticed: the recorder stayed
"running", kept rolling over to a new split file on schedule, and wrote
zero bytes into every one of them. The max_retries/reconnect path below
it was unreachable. Worst kind of failure for an unattended recorder —
files keep appearing, at the right times, with the right names.
ALSAStream now tracks when audio last arrived and exposes check_alive(),
which raises if the process exited or nothing has been captured for
SILENCE_TIMEOUT (15 s). The record loop calls it every 0.5 s tick, so a
dead device unwinds into the existing retry handler.
retry_count resets only when the stream actually delivered audio —
resetting on every healthy tick would let a device that opens but never
captures retry forever, which is the same silent failure by another
route.
Also: join the stderr thread in __exit__, and break _read_loop on EOF
rather than looping until poll() catches up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut downloads were named by byte offsets (`..._cut_740s-750s.flac`). They are
now named by the actual recording time the slice covers, e.g.
`20260523_22-31-30_22-32-30.flac` for a 22:31:30->22:32:30 cut of a recording
started at 22:00:00.
To make this reliable, the recording filename is now a fixed
`%Y%m%d_%H%M%S` start-time format (`FILENAME_FORMAT`) shared by isr.py and
web.py, replacing the user-configurable `filename_pattern` (web.py never reads
config.ini, so a custom pattern could not be parsed back). web.py parses the
start time out of the filename via `_recording_start()` and builds cut names
with `_cut_filename()`. The DATE column now also comes from the filename
(falling back to mtime only for non-standard names), since mtime is the last
write, not the start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tests/test_isr.py:
- Remove unused imports: io, SimpleNamespace, call
- Remove test_mp3_chunks_written_to_file — it mocked connect_stream but
never called record(), making all the mock scaffolding dead; the actual
assertion (bytes written to a file) is already covered by
TestAudioFileWriter and TestGenerateFilename
isr.py:
- Update AudioDevice.backend comment: only the ALSA backend exists now
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>