Files
Jake 26c7307607
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — docs, scripts, root config, residuals
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00

5.3 KiB

name, type, slice, last_verified
name type slice last_verified
Audio WAV I/O (hound) architecture-map-page 03-audio-transcription 2026/05/09

Audio WAV I/O (hound)

Where you are: Architecture mapAudio + Transcription → Audio WAV I/O

Plain English summary. Two helpers cover the simple round-trip case: write_wav writes f32 samples to a 16-bit PCM WAV; read_wav reads them back, converting either int or float WAV payloads to f32. A third type, WavWriter, is the crash-safe append writer used during long live captures: it flushes the WAV header every 8000 samples (≈500 ms at 16 kHz), so a process kill leaves a valid, playable file on disk up to the last flush.

At a glance

  • Crate: lumotia-audio
  • Path: crates/audio/src/wav.rs
  • LOC: 287
  • External deps: hound 3.5.
  • Internal callers (best effort, slice 2 reconciles): WavWriter is consumed by the live-session command in src-tauri/src/commands/live.rs (brief item #19). read_wav and write_wav are used by the file-import command and by the audio-decode tests.

Public surface:

  • pub struct WavWritercrates/audio/src/wav.rs:21.
  • pub fn WavWriter::create(path: &Path, sample_rate: u32, channels: u16) -> Result<Self>crates/audio/src/wav.rs:36.
  • pub fn WavWriter::append(&mut self, samples: &[f32]) -> Result<()>crates/audio/src/wav.rs:58.
  • pub fn WavWriter::flush(&mut self) -> Result<()>crates/audio/src/wav.rs:78.
  • pub fn WavWriter::finalize(self) -> Result<()>crates/audio/src/wav.rs:90.
  • pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()>crates/audio/src/wav.rs:99.
  • pub fn read_wav(path: &Path) -> Result<AudioSamples>crates/audio/src/wav.rs:132.

What's in here

WavWriter (crash-safe live capture)

Wraps a hound::WavWriter<BufWriter<File>> plus a samples_since_flush counter and a flush_every threshold (DEFAULT_FLUSH_EVERY_SAMPLES = 8_000, 500 ms at 16 kHz).

  • create constructs a 16-bit PCM WavSpec from the supplied sample_rate / channels. Sample rate and channel count come from LocalEngine::capabilities() rather than being hardcoded — this matters once non-Whisper engines (Parakeet, future Moonshine) declare different rates.
  • append clamps each f32 to [-1.0, 1.0], scales to i16, writes the sample, and flushes the header every flush_every samples.
  • flush calls hound::WavWriter::flush (which rewrites the RIFF and data chunk sizes in the header) and resets the counter.
  • finalize writes the terminal header and consumes the writer. A dropped-without-finalize writer leaves a valid file up to the last flush; callers that care about the unflushed tail should always call finalize.

write_wav and read_wav

write_wav is the simple one-shot writer over AudioSamples. Same clamp + i16 scale.

read_wav reads either int or float WAV payloads and returns f32 in [-1, 1]. For int payloads it scales by 1 << (bits_per_sample - 1). Unlike a previous version, per-sample errors are propagated rather than silently dropped (see watch-out below).

Data flow

Live capture path (production use):

StreamingResampler.push_samples → Vec<f32> 16 kHz mono
    └─ WavWriter::append(chunk)
        └─ for each f32:
              clamp [-1,1] → (i16) write_sample
        └─ if samples_since_flush >= 8000:
              hound flush (rewrites size headers)
…session ends…
    └─ WavWriter::finalize()
        └─ terminal header write + close

File round-trip (used in tests + simple imports):

AudioSamples → write_wav → 16-bit PCM WAV → read_wav → AudioSamples

Watch-outs

  • Crash-safety guarantee is "up to the last flush", not "every sample". Worst-case loss is flush_every / sample_rate seconds (default 500 ms at 16 kHz). The wav_writer_survives_crash test (wav.rs:170) uses std::mem::forget to skip Drop and proves the flushed prefix is recoverable.
  • finalize is consuming. Once called you cannot append further. The live-session command must hold the writer until session end and only finalize at the close path.
  • i16 quantisation noise. Both writers downcast f32 → i16. WAV round-trip introduces ~1 / 32768 absolute error per sample, validated by the wav_roundtrip test (wav.rs:265).
  • Truncated WAV bug fixed in tree (2026-04-22 review). Previous read_wav used filter_map(|s| s.ok()) and silently returned a short AudioSamples for corrupt input. Current code surfaces MagnotiaError::AudioDecodeFailed. Regression test at wav.rs:235.
  • Channels and sample rate are taken from AudioSamples directly in write_wav. WavWriter::create takes them as explicit args because it precedes the data; the caller (live session) reads LocalEngine::capabilities() to know what to pass.
  • No async wrapper. WavWriter operations are synchronous (Buffered file I/O). Live session must call them on a blocking task or accept the syscall cost on the audio thread.

See also

  • Transcription engines overviewLocalEngine::capabilities() is the source of sample_rate / channels for WavWriter::create.
  • Audio file decodingdecode_audio_file is the symphonia-based path used for non-WAV files.
  • Tests at crates/audio/src/wav.rs:165 (crash-safety, finalize round-trip, truncated-input regression).