feat(formats,viewer): standalone voice player covers all line categories

list_voice_clips now enumerates every spoken-line category so the standalone audio player covers them all — bound movie voices (\Movie\), in-mission radio (\Voice\, \etc\), and mission briefings (\Briefing\, whose BR<NN>_<MM> names lack "VOICE"); music (BGM_*) stays excluded.

Viewer voice-library fixes: invalidate the library when a new game source loads, and don't latch an empty result as "loaded" (sounds.tbl always has thousands of clips, so empty = a failed read) so re-opening the menu retries.

Voice decode reverted to the first sub-wave (base behaviour): a naive multi-sub-wave concat produced the wrong track for RT movies. The per-sub-wave splitter (to_xma_riffs) is kept + documented for the eventual real fix, which needs ground-truth playback instrumentation to resolve segments-vs-takes-vs-timecode placement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-21 22:49:09 +02:00
parent 95de29f290
commit eee1607e1b
2 changed files with 109 additions and 6 deletions

View File

@@ -1698,6 +1698,10 @@ fn poll_loader_channel(
browser.files = files;
browser.selected = None;
browser.filter.clear();
// A new game source is loaded — invalidate the voice library so the
// browser re-reads sounds.tbl from THIS source next time it opens
// (otherwise a stale/failed empty result from before load persists).
*voice_lib = VoiceLibrary::default();
info!("Loaded {} files", browser.files.len());
}
Ok(IsoLoaderMsg::FileLoaded { path, bytes }) => {
@@ -1773,9 +1777,17 @@ fn poll_loader_channel(
}
}
Ok(IsoLoaderMsg::VoiceLibraryLoaded { clips }) => {
voice_lib.clips = clips;
voice_lib.loaded = true;
voice_lib.loading = false;
// sounds.tbl always has thousands of clips, so an empty result means
// the read failed (e.g. no game source, or opened only a bare pak).
// Leave `loaded=false` so re-opening the menu retries instead of
// latching an empty list forever.
if clips.is_empty() {
voice_lib.loaded = false;
} else {
voice_lib.clips = clips;
voice_lib.loaded = true;
}
}
Ok(IsoLoaderMsg::Cancelled) => {
iso_state.loading = false;
@@ -3128,6 +3140,10 @@ fn decode_sound_clip(
use sylpheed_formats::{hash::name_hash, slb};
let key = name_hash(clip_name);
let bytes = read_sound_entry(source, key)?;
// Decode the first sub-wave (base behaviour). NOTE: multi-sub-wave assembly
// (segments vs alternate takes vs timecode-placed lines) is UNRESOLVED — a
// naive concat produced the wrong track for RT movies. Pending ground-truth
// voice-playback instrumentation (Canary APU/XMA) before a real fix.
let riff = slb::to_xma_riff(&bytes).ok_or("not a decodable .slb")?;
let dir = std::env::temp_dir();
@@ -3143,8 +3159,6 @@ fn decode_sound_clip(
let status = Command::new("ffmpeg")
.args(["-hide_banner", "-y", "-i"])
.arg(&xma_path)
// Downmix to mono using the left channel (holds the full signal for both
// left-only and dual-mono sources), then clamp to the media length.
.args(["-af", "pan=mono|c0=c0", "-t"])
.arg(format!("{dur}"))
.arg(&out_path)