Commit Graph
11 Commits
Author SHA1 Message Date
adminandClaude Opus 5 9bb1229aa9 fix: two error paths that made a failure worse than the thing that failed
/api/clip: _clip_wav/_clip_flac send audio headers and then stream frames.
A decode failure mid-stream unwound into _api_clip's handler, which called
_json_err(500) — writing a second status line and a JSON body into a
response that had already promised Content-Type: audio/wav and an exact
Content-Length. Under HTTP/1.1 keep-alive that desyncs the connection for
every subsequent request on it, so one bad clip corrupted unrelated
traffic. Track whether the headers went out; if they did, log and force
the connection closed instead, the same way _copy_to_response already
handles under-delivery.

config.ini: the int() coercion for split_minutes/max_retries/etc sat
outside the try that wraps recorder construction, so `split_minutes =
sixty` aborted the entire config load with an unhandled ValueError rather
than skipping that one section like every other config error. Moved
inside. split_duration is also clamped to >= 1 — 0 divided by zero in
get_next_split_time().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:29:54 +02:00
adminandClaude Opus 5 1541110ffb refactor: collapse the single-implementation backend abstraction, fix device resolution
isr.py carried a plugin architecture — AudioBackend ABC, a _backend_classes
registry, _discover_backends, available_backends/get_backend, a
preferred_backend config knob — for exactly one implementation. It was
also the source of three real bugs:

* find_device() returned None before reaching the ALSA PCM-name fallback
  whenever `arecord -l` enumerated nothing, so the documented
  `device = shared_mic` (dsnoop from asound.conf) case failed outright on
  any host with no hardware card. The fallback is the whole point: those
  PCMs never appear in `arecord -l`.

* `device = monitor` could never resolve. ALSA has no generic monitor
  source and list_devices hardcoded is_monitor=False, so the keyword —
  advertised in config.example.ini as the "EASY" way to capture system
  audio — only ever produced "device not found". Removed; it is a
  PulseAudio idea that outlived the PulseAudio backend.

* list_all_devices() deduplicated by lowercased name "in case a device
  appears in multiple backends". With one backend what it actually did
  was delete the second of two identical USB cards. Dedup dropped.

find_device() now always returns a device, so _resolve_device's
unreachable "Available devices:" error is gone; an unknown spec is
passed to ALSA verbatim and logged. AudioDevice loses backend/
is_monitor plus channels/sample_rate — the latter were hardcoded 2/44100
and only ever printed by --list-devices, i.e. numbers with no bearing on
what gets recorded.

--list-devices now says outright that it lists hardware cards only, which
is where the shared_mic confusion started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:28:30 +02:00
adminandClaude Opus 5 46af41b12d fix: soundcard capture detects a dead arecord instead of silently recording nothing
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>
2026-07-29 20:21:05 +02:00
adminandClaude Opus 4.8 5e7620627b feat: name cut clips by wall-clock time; fix recording filename format
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>
2026-06-11 14:30:30 +02:00
adminandClaude Fable 5 fa055fc80a perf: do not hold the audio buffer lock during disk writes
_flush_buffer_to_file wrote (and for FLAC, encoded) every chunk while
holding buffer_lock, blocking the capture callback for the duration of
each disk flush. Swap the buffer out under the lock and write outside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:29:21 +02:00
adminandClaude Fable 5 792f2b1fd5 refactor: remove dead code from isr.py
- Drop unused struct import, AudioDevice.extra/description fields
- Remove unused get_preferred_backend() and the backend priority
  machinery (only one backend exists)
- Deduplicate SoundcardRecorder.close_current_file via super()
- Remove duplicate config-exists check in main()
- Simplify --list-devices output: drop dead monitor grouping and the
  nonexistent pipewire backend example

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:41:37 +02:00
adminandClaude Sonnet 4.6 9ba084107b chore: remove dead test code and fix stale comment
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>
2026-06-02 23:32:34 +02:00
admin 9ac23e9f1d fix: allow virtual ALSA PCM names (dsnoop etc.); log arecord stderr
AudioSystem.find_device() now falls back to treating an unmatched spec as
a direct ALSA PCM name when the ALSA backend is available.  Virtual devices
defined in asound.conf (dsnoop, plug, etc.) never appear in 'arecord -l'
so they were always rejected as 'not found', even when valid.

ALSAStream now captures arecord stderr via a reader thread instead of
discarding it, so errors like 'Device or resource busy' are logged as
warnings and visible in docker compose logs.
2026-04-26 14:39:15 +02:00
admin da67523170 fix: Docker volume path, graceful shutdown deadline, inline stream headers
docker-compose.yml: mount ./recordings at /app/recordings (matches
output_directory = recordings in config.ini); previously the recorder
wrote to /app/recordings while the web container read from /recordings,
causing all files to appear missing — explaining the 9-byte Not-found
download from /stream/ and the 0-byte recordings in the UI.
Add stop_grace_period: 30s so Docker waits long enough for files to close.

isr.py: replace per-thread join(timeout=5) with a shared 25 s deadline;
with N recorders the old code could exceed Docker's SIGKILL window and
leave WAV/FLAC files unclosed (corrupt headers).

web.py: add Content-Disposition: inline to /stream/ responses so
browsers never treat the audio response as a file download.

CLAUDE.md: document web.py endpoints, status.json lifecycle, corrected
Docker volume layout, and web.py CLI flags.
2026-04-26 13:10:14 +02:00
admin 624f1f2664 feat: FLAC analysis, inline player, WCAG, live-recording status
web.py:
- Extend loudness analysis to FLAC files via soundfile (numpy required)
- Add /stream/ endpoint with HTTP Range support for seekable inline playback
- Add collapsible ▶ Play button per row (hidden by default); src loaded lazily
- Add /api/status endpoint returning active filenames from status.json
- Animated ● REC badge on in-progress files, polled every 5 s
- Full WCAG: skip link, aria-expanded/controls, aria-label, role=img on
  waveform SVG, role=list on loud-section chips, focus-visible outlines,
  aria-live on subtitle, focus moved to <audio> when player opens

isr.py:
- Write recordings/status.json atomically every 2 s while recording
- Delete status.json on clean shutdown so web UI shows no stale state
2026-04-26 12:53:01 +02:00
admin 8254ccde86 Add Docker support, fix stale docs, translate UI to English
- Dockerfile + docker-compose.yml: two services (recorder + web) sharing
  ./recordings bind mount; recorder maps /dev/snd for ALSA soundcard access
- requirements.txt: requests, numpy, soundfile
- .dockerignore, updated .gitignore (add __pycache__, .pytest_cache)
- isr.py: add SIGTERM handler for clean Docker shutdown; fix stale error
  message that referenced removed PulseAudio/PipeWire/PortAudio backends
- web.py: translate all German UI strings to English
- config.example.ini: remove PipeWire/PulseAudio/PortAudio backend refs,
  simplify soundcard tips to ALSA only
- README.md: full rewrite as user guide (quick start, config reference,
  Docker notes, how it works)
- CLAUDE.md: update architecture section to reflect ALSA-only backend
- Delete changelog.txt and guide.md (internal session notes)
2026-04-26 10:56:55 +02:00