feat: initialise workspace — Milestone 1 asset explorer
Three-crate Cargo workspace structured per PROJECT.md spec: - crates/sylpheed-formats — Xbox 360 format parsers (no Bevy) - crates/sylpheed-viewer — Bevy 0.15 asset viewer + egui UI - crates/sylpheed-cli — CLI tools (extract/list/sniff/texture) Milestone 1 features: - XISO disc image reading via xdvdfs 0.8 - XPR2 texture container parsing + Morton de-tiling - D3DFORMAT → wgpu TextureFormat mapping (DXT1/3/5, DXN, ARGB) - Custom Bevy AssetLoader for .xpr files - Orbit camera (LMB orbit, RMB pan, scroll zoom) - egui file browser + RE notes panel - CLI: extract / list / sniff / texture info / texture export - GitHub Actions CI (Linux, macOS, Windows, WASM) - Trunk WASM build config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
85
crates/sylpheed-formats/src/audio.rs
Normal file
85
crates/sylpheed-formats/src/audio.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! Audio format parsing — XMA and XWB handling.
|
||||
//!
|
||||
//! Xbox 360 games use **XMA** (Xbox Media Audio) as their primary audio codec.
|
||||
//! XMA is a proprietary Microsoft codec derived from WMA Pro.
|
||||
//!
|
||||
//! ## The Challenge
|
||||
//! XMA decoding requires Microsoft's proprietary XMA decoder, which is only
|
||||
//! available in the Windows DirectX runtime. There are two approaches:
|
||||
//!
|
||||
//! ### Option A: Convert on extraction (recommended for Milestone 1)
|
||||
//! Use `xma2encode.exe` (from Xbox 360 SDK) or `ffmpeg` (has partial XMA support)
|
||||
//! to pre-convert all audio to OGG/WAV during the extraction step.
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Convert a single XMA file to WAV using ffmpeg
|
||||
//! ffmpeg -i audio.xma output.wav
|
||||
//!
|
||||
//! # Or use VGMStream's test.exe for more accurate XMA decoding
|
||||
//! ```
|
||||
//!
|
||||
//! ### Option B: XMA2 software decoder
|
||||
//! The `xma2dec` project provides an open-source XMA2 decoder.
|
||||
//! GitHub: https://github.com/koolkdev/xmalib (research-quality)
|
||||
//!
|
||||
//! ### XWB Wave Bank format
|
||||
//! Xbox 360 games typically bundle audio into XWB (Xbox Wave Bank) files.
|
||||
//! These are containers that hold multiple XMA streams.
|
||||
//! Reference: https://github.com/microsoft/DirectXTK/tree/main/Audio
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AudioError {
|
||||
#[error("Unknown audio format magic: {0:?}")]
|
||||
UnknownMagic([u8; 4]),
|
||||
#[error("XMA decoding requires pre-conversion. See audio.rs for instructions.")]
|
||||
XmaNotSupported,
|
||||
#[error("Parse error: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
/// An audio clip decoded to raw PCM, ready for Bevy's audio system.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameAudio {
|
||||
/// Raw PCM samples (interleaved for stereo: L R L R ...)
|
||||
pub samples: Vec<f32>,
|
||||
pub channels: u16,
|
||||
pub sample_rate: u32,
|
||||
}
|
||||
|
||||
impl GameAudio {
|
||||
/// Parse audio from raw bytes.
|
||||
///
|
||||
/// Currently only WAV/PCM is supported. For XMA files, pre-convert using:
|
||||
/// `ffmpeg -i file.xma file.wav`
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, AudioError> {
|
||||
// Check for WAV magic
|
||||
if bytes.len() >= 4 && &bytes[..4] == b"RIFF" {
|
||||
return Self::from_wav(bytes);
|
||||
}
|
||||
|
||||
// XMA magic bytes
|
||||
if bytes.len() >= 4 && (
|
||||
&bytes[..4] == b"XWB\0" || // Wave bank
|
||||
&bytes[..4] == b"XMA2" // Raw XMA2
|
||||
) {
|
||||
return Err(AudioError::XmaNotSupported);
|
||||
}
|
||||
|
||||
Err(AudioError::UnknownMagic(
|
||||
bytes[..4.min(bytes.len())].try_into().unwrap_or([0u8; 4])
|
||||
))
|
||||
}
|
||||
|
||||
fn from_wav(bytes: &[u8]) -> Result<Self, AudioError> {
|
||||
// Minimal WAV parser for PCM files
|
||||
// A proper implementation should use the `hound` crate:
|
||||
// https://crates.io/crates/hound
|
||||
//
|
||||
// TODO: Add `hound = "3"` to Cargo.toml and implement properly
|
||||
Err(AudioError::Parse(
|
||||
"WAV parsing TODO: add `hound` crate and implement".to_string()
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user