[WIP] Audio codec probe/detection + CLI audio-info + viewer audio panel

Snapshot of local WIP before rebasing onto the movie/voice remote work.
- audio.rs: AudioCodec enum, AudioInfo::probe (RIFF/WAVE parse, raw-XMA2
  Shannon-entropy heuristic looks_like_raw_xma2), from_wav, riff_data_span.
- cli/main.rs: `audio-info` subcommand (cmd_audio_info).
- viewer ui.rs/iso_loader.rs: draw_audio_detail panel wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-20 20:42:48 +02:00
parent 3e1040b472
commit 8ed8c85f18
5 changed files with 496 additions and 62 deletions

View File

@@ -165,6 +165,8 @@ pub enum PakContent {
Ratc(Vec<RatcEntry>),
/// Plain-text / XML payload, with its encoding label.
Text { text: String, encoding: String },
/// An audio stream (WAV / XMA / raw XMA2) — metadata only; XMA isn't decoded.
Audio(sylpheed_formats::AudioInfo),
}
/// One child in a presented RATC bundle.
@@ -978,6 +980,12 @@ fn classify_content(payload: &[u8]) -> PakContent {
encoding: encoding.to_string(),
};
}
// Last resort: a self-describing WAV/XMA header, or a header-less high-
// entropy blob (the game's raw XMA2 sound-bank streams).
let audio = sylpheed_formats::AudioInfo::probe(payload);
if audio.codec != sylpheed_formats::AudioCodec::Unknown {
return PakContent::Audio(audio);
}
PakContent::None
}

View File

@@ -506,6 +506,7 @@ fn draw_pak_browser(ui: &mut egui::Ui, pak: &mut PakView) {
PakContent::Text { text, encoding } => {
draw_text_detail(ui, row, text, encoding)
}
PakContent::Audio(info) => draw_audio_detail(ui, row, info),
PakContent::None => draw_idxd_detail(ui, row),
},
});
@@ -531,6 +532,10 @@ fn row_kind(row: &crate::iso_loader::PakRow) -> String {
PakContent::Lsta(f) => format!("LSTA · {} sprite(s)", f.len()),
PakContent::Ratc(c) => format!("RATC · {} item(s)", c.len()),
PakContent::Text { .. } => "text".into(),
PakContent::Audio(a) => match a.xma_packets {
Some(p) => format!("audio · {} · {p} pkts", a.codec.label()),
None => format!("audio · {}", a.codec.label()),
},
PakContent::None => row.identity.clone(),
}
}
@@ -836,6 +841,59 @@ fn draw_text_detail(
);
}
/// An audio stream entry: codec + whatever metadata is derivable. The game's
/// `sound.pak` streams are raw XMA2 (no embedded channels/rate), so most fields
/// read "—" until the sound-bank descriptor + an XMA2 decoder are added.
fn draw_audio_detail(
ui: &mut egui::Ui,
row: &crate::iso_loader::PakRow,
info: &sylpheed_formats::AudioInfo,
) {
ui.horizontal(|ui| {
ui.heading("Audio stream");
ui.separator();
ui.label(info.codec.label());
ui.separator();
ui.label(format!("hash {:08x}", row.hash));
});
ui.separator();
let dash = "".to_string();
egui::Grid::new("audio_meta").striped(true).num_columns(2).show(ui, |ui| {
ui.strong("Codec");
ui.label(info.codec.label());
ui.end_row();
ui.strong("Channels");
ui.label(info.channels.map(|c| c.to_string()).unwrap_or_else(|| dash.clone()));
ui.end_row();
ui.strong("Sample rate");
ui.label(info.sample_rate.map(|r| format!("{r} Hz")).unwrap_or_else(|| dash.clone()));
ui.end_row();
if let Some(d) = info.duration_secs {
ui.strong("Duration");
ui.label(format!("{d:.2} s"));
ui.end_row();
}
if let Some(p) = info.xma_packets {
ui.strong("XMA packets");
ui.label(format!("{p} (2048 B each)"));
ui.end_row();
}
ui.strong("Size");
ui.label(format!("{} bytes", info.size_bytes));
ui.end_row();
});
if info.codec.needs_decoder() {
ui.separator();
ui.colored_label(
egui::Color32::from_gray(150),
"Raw XMA2 stream — playback needs an XMA2 decoder and the (un-RE'd) \
sound-bank descriptor for per-stream channels/sample-rate.",
);
}
}
// ── Video player (WMV cutscenes) ──────────────────────────────────────────────
/// Format seconds as `mm:ss`.