use crate::acb::utf::{get_bytes_field, get_int_field, get_string_field, UtfTable, Value};
use std::collections::HashMap;
use std::io::Cursor;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TrackError {
#[error("UTF error: {0}")]
Utf(#[from] crate::acb::utf::UtfError),
#[error("No rows in UTF table")]
NoRows,
#[error("Missing required table: {0}")]
MissingTable(String),
#[error("ReferenceType {0} not implemented")]
UnsupportedRefType(i32),
}
#[derive(Debug, Clone)]
pub struct Track {
pub cue_id: i32,
pub name: String,
pub wav_id: i32,
pub enc_type: i32,
pub is_stream: bool,
pub stream_awb_id: i32,
}
#[derive(Debug, Clone)]
pub struct TrackList {
pub tracks: Vec<Track>,
}
struct AcbTables {
cues: UtfTable,
nams: UtfTable,
wavs: Option<UtfTable>,
syns: Option<UtfTable>,
tras: UtfTable,
tevs: UtfTable,
seqs: Option<UtfTable>,
}
impl TrackList {
pub fn new(utf: &UtfTable) -> Result<Self, TrackError> {
if utf.rows.is_empty() {
return Err(TrackError::NoRows);
}
let tables = parse_acb_tables(&utf.rows[0])?;
let name_map = build_name_map(&tables.nams);
let mut tl = TrackList { tracks: Vec::new() };
extract_tracks_from_tables(&tables, &name_map, &mut tl)?;
Ok(tl)
}
}
fn parse_acb_tables(row: &HashMap<String, Value>) -> Result<AcbTables, TrackError> {
let table_bytes = extract_table_bytes(row)?;
parse_utf_tables(&table_bytes)
}
fn extract_table_bytes(row: &HashMap<String, Value>) -> Result<HashMap<&str, Vec<u8>>, TrackError> {
let mut tables = HashMap::new();
for name in &["CueTable", "CueNameTable", "TrackTable"] {
let data =
get_bytes_field(row, name).ok_or_else(|| TrackError::MissingTable(name.to_string()))?;
tables.insert(*name, data.to_vec());
}
for name in &["WaveformTable", "SynthTable"] {
if let Some(data) = get_bytes_field(row, name) {
if !data.is_empty() {
tables.insert(*name, data.to_vec());
}
}
}
let tev_data = get_bytes_field(row, "TrackEventTable")
.or_else(|| get_bytes_field(row, "CommandTable"))
.ok_or_else(|| TrackError::MissingTable("TrackEventTable/CommandTable".to_string()))?;
tables.insert("TrackEventTable", tev_data.to_vec());
if let Some(data) = get_bytes_field(row, "SequenceTable") {
if !data.is_empty() {
tables.insert("SequenceTable", data.to_vec());
}
}
Ok(tables)
}
fn parse_utf_tables(table_bytes: &HashMap<&str, Vec<u8>>) -> Result<AcbTables, TrackError> {
let cues = UtfTable::new(Cursor::new(&table_bytes["CueTable"]))?;
let nams = UtfTable::new(Cursor::new(&table_bytes["CueNameTable"]))?;
let wavs = table_bytes
.get("WaveformTable")
.filter(|d| !d.is_empty())
.map(|d| UtfTable::new(Cursor::new(d)))
.transpose()?;
let syns = table_bytes
.get("SynthTable")
.filter(|d| !d.is_empty())
.map(|d| UtfTable::new(Cursor::new(d)))
.transpose()?;
let tras = UtfTable::new(Cursor::new(&table_bytes["TrackTable"]))?;
let tevs = UtfTable::new(Cursor::new(&table_bytes["TrackEventTable"]))?;
let seqs = table_bytes
.get("SequenceTable")
.filter(|d| !d.is_empty())
.map(|d| UtfTable::new(Cursor::new(d)))
.transpose()?;
Ok(AcbTables {
cues,
nams,
wavs,
syns,
tras,
tevs,
seqs,
})
}
fn build_name_map(nams: &UtfTable) -> HashMap<i32, String> {
let mut name_map = HashMap::new();
for row in &nams.rows {
let idx = get_int_field(row, "CueIndex") as i32;
let name = get_string_field(row, "CueName").unwrap_or("").to_string();
name_map.insert(idx, name);
}
name_map
}
fn extract_tracks_from_tables(
tables: &AcbTables,
name_map: &HashMap<i32, String>,
tl: &mut TrackList,
) -> Result<(), TrackError> {
for cue_row in &tables.cues.rows {
let ref_type = get_int_field(cue_row, "ReferenceType") as i32;
if ref_type != 3 && ref_type != 8 {
return Err(TrackError::UnsupportedRefType(ref_type));
}
let ref_index = get_int_field(cue_row, "ReferenceIndex") as usize;
if let Some(seqs) = &tables.seqs {
if ref_index < seqs.rows.len() {
extract_sequence_tracks(tables, name_map, ref_index, tl);
continue;
}
}
extract_direct_tracks(tables, name_map, ref_index, tl);
}
Ok(())
}
fn extract_sequence_tracks(
tables: &AcbTables,
name_map: &HashMap<i32, String>,
ref_index: usize,
tl: &mut TrackList,
) {
if let Some(seqs) = &tables.seqs {
let seq = &seqs.rows[ref_index];
let num_tracks = get_int_field(seq, "NumTracks") as usize;
if let Some(track_index_data) = get_bytes_field(seq, "TrackIndex") {
for i in 0..num_tracks {
if i * 2 + 1 >= track_index_data.len() {
break;
}
let idx = u16::from_be_bytes([track_index_data[i * 2], track_index_data[i * 2 + 1]])
as usize;
if idx < tables.tras.rows.len() {
extract_track_from_track_row(tables, name_map, ref_index as i32, idx, tl);
}
}
}
}
}
fn extract_direct_tracks(
tables: &AcbTables,
name_map: &HashMap<i32, String>,
ref_index: usize,
tl: &mut TrackList,
) {
for idx in 0..tables.tras.rows.len() {
extract_track_from_track_row(tables, name_map, ref_index as i32, idx, tl);
}
}
fn extract_track_from_track_row(
tables: &AcbTables,
name_map: &HashMap<i32, String>,
ref_index: i32,
track_idx: usize,
tl: &mut TrackList,
) {
let track = &tables.tras.rows[track_idx];
let event_idx = get_int_field(track, "EventIndex") as usize;
if event_idx == 0xFFFF || event_idx >= tables.tevs.rows.len() {
return;
}
let tracks = extract_tracks_from_event(
&tables.tevs.rows[event_idx],
tables.syns.as_ref(),
tables.wavs.as_ref(),
name_map,
ref_index,
&tl.tracks,
);
tl.tracks.extend(tracks);
}
fn extract_tracks_from_event(
track_event: &HashMap<String, Value>,
syns: Option<&UtfTable>,
wavs: Option<&UtfTable>,
name_map: &HashMap<i32, String>,
ref_index: i32,
existing_tracks: &[Track],
) -> Vec<Track> {
let mut tracks = Vec::new();
let command = match get_bytes_field(track_event, "Command") {
Some(c) => c,
None => return tracks,
};
let mut k = 0;
while k < command.len() {
if k + 3 > command.len() {
break;
}
let cmd = u16::from_be_bytes([command[k], command[k + 1]]);
let cmd_len = command[k + 2] as usize;
k += 3;
if k + cmd_len > command.len() {
break;
}
let param_bytes = &command[k..k + cmd_len];
k += cmd_len;
if cmd == 0 {
break;
}
if cmd == 0x07d0 {
if let Some(track) = extract_track_from_command(
param_bytes,
syns,
wavs,
name_map,
ref_index,
existing_tracks,
&tracks,
) {
tracks.push(track);
}
}
}
tracks
}
fn extract_track_from_command(
param_bytes: &[u8],
syns: Option<&UtfTable>,
wavs: Option<&UtfTable>,
name_map: &HashMap<i32, String>,
ref_index: i32,
existing_tracks: &[Track],
current_tracks: &[Track],
) -> Option<Track> {
if param_bytes.len() < 4 {
return None;
}
let u1 = u16::from_be_bytes([param_bytes[0], param_bytes[1]]);
if u1 != 2 {
return None;
}
let syn_idx = u16::from_be_bytes([param_bytes[2], param_bytes[3]]) as usize;
let syns = syns?;
if syn_idx >= syns.rows.len() {
return None;
}
let r_data = get_bytes_field(&syns.rows[syn_idx], "ReferenceItems")?;
if r_data.len() < 4 {
return None;
}
let a = u16::from_be_bytes([r_data[0], r_data[1]]);
let wav_idx = u16::from_be_bytes([r_data[2], r_data[3]]) as usize;
if a != 1 {
return None;
}
let wavs = wavs?;
if wav_idx >= wavs.rows.len() {
return None;
}
let wav_row = &wavs.rows[wav_idx];
let is_stream = get_int_field(wav_row, "Streaming") != 0;
let enc_type = get_int_field(wav_row, "EncodeType") as i32;
let wav_id = if is_stream {
get_int_field(wav_row, "StreamAwbId") as i32
} else {
get_int_field(wav_row, "MemoryAwbId") as i32
};
let stream_awb_id = if is_stream {
get_int_field(wav_row, "StreamAwbPortNo") as i32
} else {
-1
};
let base_name = name_map.get(&ref_index).cloned().unwrap_or_default();
let name = generate_unique_name(
&base_name,
ref_index,
wav_id,
existing_tracks,
current_tracks,
);
Some(Track {
cue_id: ref_index,
name,
wav_id,
enc_type,
is_stream,
stream_awb_id,
})
}
fn generate_unique_name(
base_name: &str,
ref_index: i32,
wav_id: i32,
existing_tracks: &[Track],
current_tracks: &[Track],
) -> String {
let name = if base_name.is_empty() {
format!("UNKNOWN-{}", ref_index)
} else {
base_name.to_string()
};
let is_duplicate = existing_tracks.iter().any(|t| t.name == name)
|| current_tracks.iter().any(|t| t.name == name);
if is_duplicate {
format!("{}-{}", name, wav_id)
} else {
name
}
}