Skip to content

Commit 653edc9

Browse files
committed
fix(audio): support ADPCM WAV, validate format on selection, remove unsupported FLAC filter
Three related fixes for issue #389, where custom notification sounds silently fell back to the built-in default on Windows. Root cause: Rodio 0.22 uses Symphonia for decoding. The compiled Symphonia crates only included PCM WAV support. ADPCM-encoded WAV (the default output of Windows Sound Recorder) failed to decode silently, and FLAC — offered in the file picker — was never decodable. Failures were never surfaced to the user; they would see their filename in settings but hear the wrong sound. - Add symphonia-codec-adpcm as a direct dependency so ADPCM WAV files decode correctly via the existing Decoder::new() path. - Add audio::probe_audio_file() which attempts to construct a Decoder from a copied file before committing it. On failure, the orphan is deleted and the in-memory custom path is cleared so playback falls back to the embedded default cleanly. - Call probe_audio_file() in audio_set_custom after the file is copied; return an Err to the frontend if the format is unsupported. - Add per-cue inline error state to NotificationsSection.svelte so the error message is displayed below the failing row. Error clears on a successful selection or when the user restores the default. - Remove 'flac' from the openAudioFilePicker filter; FLAC decoding is not available and the option was misleading.
1 parent 98d9ee5 commit 653edc9

6 files changed

Lines changed: 65 additions & 1 deletion

File tree

src-tauri/Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ axum = { version = "0.8", features = ["ws"] }
3737
futures-util = "0.3"
3838

3939
rodio = { version = "0.22", default-features = false, features = ["playback", "wav", "mp3", "vorbis"] }
40+
symphonia-codec-adpcm = "0.5.5"
4041
tiny-skia = "0.12"
4142
notify = "8"
4243

src-tauri/src/audio/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,18 @@ pub fn find_custom_files(audio_dir: &Path) -> CustomAudioPaths {
209209
}
210210
}
211211

212+
/// Probe `path` by attempting to construct a `rodio::Decoder`.
213+
/// Returns `Ok(())` if the format and codec are recognised, or an error
214+
/// string suitable for returning to the frontend.
215+
pub fn probe_audio_file(path: &Path) -> Result<(), String> {
216+
let reader = std::fs::File::open(path)
217+
.map(std::io::BufReader::new)
218+
.map_err(|e| format!("cannot open file: {e}"))?;
219+
Decoder::new(reader)
220+
.map(|_| ())
221+
.map_err(|e| format!("audio format not supported: {e}"))
222+
}
223+
212224
// ---------------------------------------------------------------------------
213225
// Audio thread
214226
// ---------------------------------------------------------------------------

src-tauri/src/commands.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,15 @@ pub fn audio_set_custom(
451451
let dest = audio_dir.join(format!("{stem}.{ext}"));
452452
std::fs::copy(src, &dest).map_err(|e| e.to_string())?;
453453

454+
// Verify the copied file is decodable before committing. If it fails,
455+
// clean up the orphan and sync in-memory state to default (the old file
456+
// was already deleted above).
457+
if let Err(e) = audio::probe_audio_file(&dest) {
458+
let _ = std::fs::remove_file(&dest);
459+
audio_state.clear_custom_path(&cue);
460+
return Err(e);
461+
}
462+
454463
audio_state.set_custom_path(&cue, dest);
455464

456465
let display_name = src

src/lib/components/settings/sections/NotificationsSection.svelte

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@
4343
else longBreakAlert = val;
4444
}
4545
46+
let workAlertError = $state<string | null>(null);
47+
let shortBreakAlertError = $state<string | null>(null);
48+
let longBreakAlertError = $state<string | null>(null);
49+
50+
function getError(id: CueKey): string | null {
51+
if (id === 'work_alert') return workAlertError;
52+
if (id === 'short_break_alert') return shortBreakAlertError;
53+
return longBreakAlertError;
54+
}
55+
56+
function setError(id: CueKey, val: string | null) {
57+
if (id === 'work_alert') workAlertError = val;
58+
else if (id === 'short_break_alert') shortBreakAlertError = val;
59+
else longBreakAlertError = val;
60+
}
61+
4662
async function refreshAudioInfo() {
4763
try {
4864
const info: CustomAudioInfo = await getCustomAudioInfo();
@@ -76,6 +92,7 @@
7692
}
7793
7894
async function pickAudio(id: CueKey) {
95+
setError(id, null);
7996
let path: string | null = null;
8097
try {
8198
path = await openAudioFilePicker();
@@ -87,15 +104,18 @@
87104
try {
88105
const displayName = await setCustomAudio(id, path);
89106
setFileName(id, displayName);
107+
setError(id, null);
90108
} catch (err) {
91109
await logError(`[audio] setCustomAudio failed: ${err}`);
110+
setError(id, String(err));
92111
}
93112
}
94113
95114
async function restoreAudio(id: CueKey) {
96115
try {
97116
await clearCustomAudio(id);
98117
setFileName(id, null);
118+
setError(id, null);
99119
} catch (err) {
100120
await logError(`[audio] clearCustomAudio failed: ${err}`);
101121
}
@@ -122,6 +142,9 @@
122142
<button class="btn-choose" onclick={() => pickAudio(id)}>{m.notif_btn_choose()}</button>
123143
</div>
124144
</div>
145+
{#if getError(id)}
146+
<p class="audio-error">{getError(id)}</p>
147+
{/if}
125148
{/each}
126149

127150
<div class="group-heading">{m.notif_group_desktop()}</div>
@@ -324,4 +347,12 @@
324347
background: color-mix(in oklch, var(--color-foreground) 14%, transparent);
325348
color: var(--color-foreground);
326349
}
350+
351+
.audio-error {
352+
margin: 0;
353+
padding: 2px 20px 8px;
354+
font-size: 0.72rem;
355+
color: var(--color-danger, #e05252);
356+
font-family: monospace;
357+
}
327358
</style>

src/lib/ipc/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const clearCustomAudio = (cue: string) => invoke<void>('audio_clear_custo
6060
export const openAudioFilePicker = (): Promise<string | null> =>
6161
dialogOpen({
6262
multiple: false,
63-
filters: [{ name: 'Audio', extensions: ['mp3', 'wav', 'ogg', 'flac'] }],
63+
filters: [{ name: 'Audio', extensions: ['mp3', 'wav', 'ogg'] }],
6464
}) as Promise<string | null>;
6565

6666
// --- Diagnostic log commands ---

0 commit comments

Comments
 (0)