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.6 KiB

name, type, slice, last_verified
name type slice last_verified
Audio resampling (rubato) architecture-map-page 03-audio-transcription 2026/05/09

Audio resampling (rubato)

Where you are: Architecture mapAudio + Transcription → Audio resampling

Plain English summary. Whisper and Parakeet want 16 kHz mono f32. Microphones and audio files almost never deliver that natively. Two rubato-backed resamplers convert: resample_to_16khz for one-shot file conversion, StreamingResampler for the live session where samples arrive in arbitrary chunks and a flush at the end drains the resampler's tail.

At a glance

  • Crate: lumotia-audio
  • Paths: crates/audio/src/resample.rs (100 LOC), crates/audio/src/streaming_resample.rs (211 LOC).
  • External deps: rubato 0.15 (sinc interpolation).
  • Internal callers (best effort, slice 2 reconciles): concurrency::decode_and_resample (file path used by audio.rs and transcription.rs Tauri commands), live.rs Tauri command (live path).

Public surface:

  • pub fn resample_to_16khz(audio: &AudioSamples) -> Result<AudioSamples>crates/audio/src/resample.rs:11.
  • pub enum StreamingResamplercrates/audio/src/streaming_resample.rs:37. Variants: Passthrough, Sinc { resampler, residual, ratio }.
  • pub fn StreamingResampler::new(from_rate: u32) -> Result<Self>crates/audio/src/streaming_resample.rs:52.
  • pub fn StreamingResampler::push_samples(&mut self, mono: &[f32]) -> Result<Vec<f32>>crates/audio/src/streaming_resample.rs:93.
  • pub fn StreamingResampler::flush(&mut self) -> Result<Vec<f32>>crates/audio/src/streaming_resample.rs:127.

What's in here

Shared sinc parameters

Both files use the same SincInterpolationParameters:

sinc_len: 256,
f_cutoff: 0.95,
oversampling_factor: 128,
interpolation: SincInterpolationType::Cubic,
window: WindowFunction::Blackman,

max_relative_jitter = 1.1, chunks_in = 1024, channels = 1.

resample_to_16khz (file)

crates/audio/src/resample.rs:11. Pure function over an immutable AudioSamples. Short-circuits the passthrough case (already 16 kHz). Iterates the input in 1024-sample chunks; pads the trailing partial chunk to chunk_size zeros; truncates the output to (input_len * ratio) as usize to discard the padding's residue.

Errors map to MagnotiaError::AudioDecodeFailed (one variant for ratio init failure, one for per-chunk process failure).

StreamingResampler (live)

crates/audio/src/streaming_resample.rs. Two states:

  • Passthrough — emitted when from_rate == WHISPER_SAMPLE_RATE. push_samples returns input verbatim; flush returns empty.
  • Sinc { resampler, residual, ratio } — wraps a SincFixedIn::<f32>. residual buffers samples that don't yet form a complete INPUT_CHUNK = 1024. Each push_samples call drains as many full chunks as it can.

flush() is the load-bearing tail handler:

  1. If residual is empty, return empty (no flush work pending).
  2. Pad residual up to INPUT_CHUNK zeros.
  3. Run resampler.process() once over the padded chunk.
  4. Truncate the output to (leftover_real_samples * ratio).round() as usize so the silence introduced by padding does not leak into the saved WAV.

The flush correctness math is verified by the streaming_48k_to_16k_preserves_duration test (streaming_resample.rs:183).

Data flow

File path:

AudioSamples (any rate, mono)
   └─ resample_to_16khz()
       ├─ if rate == 16k → AudioSamples::mono_16khz(clone)
       └─ else: rubato SincFixedIn loop, padded final chunk, truncate to expected_len
   → AudioSamples (16k, mono)

Live path:

cpal AudioChunk (native rate, possibly stereo, downmixed by caller)
   └─ StreamingResampler::push_samples(&mono_f32)
       ├─ residual.extend(mono)
       ├─ while residual.len() >= 1024: resampler.process(chunk) → out
       └─ return out (zero or more 16 kHz samples)
   ...session ends...
   └─ StreamingResampler::flush()
       └─ pad residual to 1024, process, trim padding-induced output
   → 16 kHz mono Vec<f32>

Watch-outs

  • Two implementations, one parameter set. Drift between resample.rs and streaming_resample.rs is currently held by convention only. Any change to SincInterpolationParameters must touch both files.
  • Caller is responsible for downmixing. Both functions assume mono input. decode.rs averages channels for files; the live session must do the same before pushing to StreamingResampler.
  • from_rate = 0 is rejected in both functions (matches a corrupt WAV header path).
  • Truncation policy differs slightly. resample_to_16khz uses (samples.len() as f64 * ratio) as usize (truncating cast), StreamingResampler::flush uses .round() as usize. Off by one sample at most; matters for WAV file length parity if you ever compare them.
  • No streaming for file path. resample_to_16khz materialises the whole resampled vec in memory. A 90-minute meeting at 48 kHz pre-resample is ~250 MB of f32; the post-resample buffer is another 350 MB. Acceptable today, brittle for very long imports.
  • Passthrough still calls to_vec() on the input samples (streaming_resample.rs:95). Cheap but not free; if push frequency is high consider returning Cow.

See also

  • Audio capture pipeline — produces the AudioChunks that feed StreamingResampler.
  • Audio file decodingdecode_and_resample chains decode + resample_to_16khz.
  • lumotia_core::constants::WHISPER_SAMPLE_RATE (slice 5) — single source of truth for the target rate.