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>
5.5 KiB
name, type, slice, last_verified
| name | type | slice | last_verified |
|---|---|---|---|
| Audio file decoding (symphonia) | architecture-map-page | 03-audio-transcription | 2026/05/09 |
Audio file decoding (symphonia)
Where you are: Architecture map → Audio + Transcription → Audio file decoding
Plain English summary. When the user imports an audio file, decode_audio_file reads it through symphonia, downmixes to mono, and returns f32 PCM at the file's native rate. decode_and_resample chains this with resample_to_16khz on a blocking task so the Tauri command stays async. A short probe_audio_duration_secs helper reads the duration without decoding the body, used for "is this longer than the import limit?" preflight.
At a glance
- Crate:
lumotia-audio - Paths:
crates/audio/src/decode.rs(283 LOC),crates/audio/src/concurrency.rs(19 LOC). - External deps:
symphonia 0.5with featuresmp3, aac, flac, pcm, vorbis, wav, ogg, isomp4. - Internal callers (best effort, slice 2 reconciles): the import-file Tauri command and the file-mode transcription command call
decode_and_resample. Standalonedecode_audio_fileis exposed for tests and any path that wants the native rate.
Public surface:
pub fn decode_audio_file(path: &Path) -> Result<AudioSamples>—crates/audio/src/decode.rs:23.pub fn decode_audio_file_limited(path: &Path, max_duration_secs: Option<f64>) -> Result<AudioSamples>—crates/audio/src/decode.rs:27.pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>>—crates/audio/src/decode.rs:43.pub async fn decode_and_resample(path: &Path) -> Result<AudioSamples>—crates/audio/src/concurrency.rs:11.
What's in here
decode_audio_file and friends
decode_audio_file (decode.rs:23) delegates to decode_audio_file_limited(path, None). The _limited variant accepts a hard cap in seconds; once the per-track sample count crosses (max_duration_secs * sample_rate).ceil() it returns MagnotiaError::AudioDecodeFailed("Audio is longer than the {:.0} minute import limit", ...). Used by the Tauri import command to enforce the configured maximum.
Both call into decode_media_stream (decode.rs:77), which is the actual decode loop and the seam tests inject a MediaSource into. Steps:
- Probe the format (
symphonia::default::get_probe()). - Pick the default track; reject if there is none or the codec sample rate is
0. - Build a decoder via
symphonia::default::get_codecs(). - Loop:
format.next_packet()—UnexpectedEofis the natural EOF marker; any other error is propagated asAudioDecodeFailed.ResetRequiredis treated as a discontinuity error.- Skip packets that don't belong to the chosen track.
- Decode into a
SampleBuffer<f32>. - Average channels into mono if
channels > 1; otherwise extend the output verbatim. - Apply the duration cap if one was provided.
- Reject empty output (
No audio data decoded) and returnAudioSamples::new(samples, sample_rate, 1).
probe_audio_duration_secs
decode.rs:43. Re-runs the probe step only, reads track.codec_params.n_frames and sample_rate, returns Some(seconds) if both are available. Cheaper than a full decode for "is this file too long?" gating.
decode_and_resample
concurrency.rs:11. Wraps decode_audio_file + resample_to_16khz in tokio::task::spawn_blocking. Used by every async caller that needs ready-to-transcribe samples. Maps JoinError to MagnotiaError::AudioDecodeFailed("Task join error: {e}").
Data flow
Path
└─ File::open
└─ MediaSourceStream
└─ Hint(extension)
└─ probe → format + codec_params
└─ Decoder
└─ next_packet loop
├─ packet → decoded → SampleBuffer<f32>
└─ if channels > 1: mean across channels (mono mixdown)
→ samples (f32, native rate)
decode_and_resample chains the above with resample_to_16khz so the caller gets AudioSamples already at 16 kHz.
Watch-outs
- RB-09 regression (2026-04-22 review). The previous loop did
Err(_) => breakand silently returned partial audio on a mid-stream I/O error. Current code distinguishes EOF,ResetRequired, and other errors. Test atdecode.rs:262injects a flakyMediaSourceto keep this honest. - AAC and ISO MP4 are pulled in unconditionally by the symphonia feature set. Patent-licence implications for AAC are flagged in the brief; this is not a per-build decision today.
- Channels are averaged, not mixed psychoacoustically. Stereo to mono goes via mean of all channels per sample. Fine for ASR; lossy for any future task that needs spatial cues.
- Whole-file decode loads everything into memory. The duration cap is the only guard. Long imports exceed RAM long before they exceed disk.
Hintonly carries extension. A.mp3named.binwill fail to probe even if the bytes are valid MP3. Caller responsibility to keep extensions truthful.decode_and_resamplereturns the resampled output. If a downstream caller wanted the original rate (for a non-ASR purpose), it must calldecode_audio_filedirectly.
See also
- Audio resampling —
resample_to_16khzis the second half ofdecode_and_resample. - Audio WAV I/O —
read_wavis the WAV-only fast path that useshoundinstead of symphonia. lumotia_core::types::AudioSamples(slice 5) — the value type returned.- Tests at
decode.rs:175(RB-09 regression among others).