cridecoder 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cridecoder-0.3.0 → cridecoder-0.3.1}/Cargo.lock +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.1}/Cargo.toml +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.1}/PKG-INFO +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.1}/cridecoder.pyi +26 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/pyproject.toml +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/extractor.rs +97 -10
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb.rs +4 -1
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/lib.rs +4 -1
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/python.rs +97 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/tests/integration_tests.rs +37 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/copilot-instructions.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/dependabot.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/workflows/ci.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/workflows/release-crate.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/workflows/release-python.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/.gitignore +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/AGENTS.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/CLAUDE.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/KNOWN_GAPS.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/LICENSE +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/README.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/debug_acb.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/profile_hca.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/test_acb.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/test_hca.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/test_usm.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/afs.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/builder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/consts.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/track.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/utf.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/ath.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/bitreader.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/cipher.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/decoder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/encoder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/hca_file.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/imdct.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/tables.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/reader.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm/builder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm/extractor.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm/metadata.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm.rs +0 -0
|
@@ -9,6 +9,8 @@ from typing import Optional
|
|
|
9
9
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"extract_acb",
|
|
12
|
+
"extract_acb_tracks",
|
|
13
|
+
"decode_acb_to_wav",
|
|
12
14
|
"build_acb",
|
|
13
15
|
"build_acb_bytes",
|
|
14
16
|
"build_music_acb_bytes",
|
|
@@ -31,6 +33,30 @@ def extract_acb(acb_path: str, output_dir: str) -> Optional[list[str]]:
|
|
|
31
33
|
"""
|
|
32
34
|
...
|
|
33
35
|
|
|
36
|
+
def extract_acb_tracks(
|
|
37
|
+
acb_path: str, output_dir: str
|
|
38
|
+
) -> Optional[list[dict[str, object]]]:
|
|
39
|
+
"""Extract audio tracks from an ACB, returning per-track metadata.
|
|
40
|
+
|
|
41
|
+
Like :func:`extract_acb`, but each entry is a dict with ``path`` (written
|
|
42
|
+
file), ``name`` (cue name), ``cue_id`` and ``subkey`` — the AFS2 subkey of
|
|
43
|
+
the originating AWB, needed (with the global keycode) to decode type-56
|
|
44
|
+
encrypted HCA. Returns ``None`` if the file is invalid.
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def decode_acb_to_wav(
|
|
49
|
+
acb_path: str, output_dir: str, key: Optional[int] = ...
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
"""Extract an ACB and decode its HCA tracks straight to WAV files.
|
|
52
|
+
|
|
53
|
+
The per-AWB AFS2 subkey is applied automatically, so encrypted (type-56)
|
|
54
|
+
ACBs only need the global ``key`` (omit/``None`` for unencrypted ACBs).
|
|
55
|
+
Non-HCA tracks are written verbatim with their original extension. Returns
|
|
56
|
+
the list of written file paths.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
34
60
|
def build_acb(tracks: list[tuple[str, int, bytes]], output_path: str) -> None:
|
|
35
61
|
"""Build an ACB file from ``(name, cue_id, hca_data)`` tuples, writing to disk."""
|
|
36
62
|
...
|
|
@@ -209,11 +209,78 @@ fn get_track_data(
|
|
|
209
209
|
Ok(None)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
///
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
/// A track extracted to disk together with the metadata needed to decode it.
|
|
213
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
214
|
+
pub struct ExtractedTrackFile {
|
|
215
|
+
/// Path of the written waveform file.
|
|
216
|
+
pub path: String,
|
|
217
|
+
/// Cue name of the track (also the file stem).
|
|
218
|
+
pub name: String,
|
|
219
|
+
/// Cue id of the track.
|
|
220
|
+
pub cue_id: i32,
|
|
221
|
+
/// AFS2 subkey of the originating AWB. Required (with the global keycode) to
|
|
222
|
+
/// decrypt type-56 encrypted HCA; 0 when the AWB is unencrypted.
|
|
223
|
+
pub subkey: u16,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Extract all audio tracks to `target_dir`, returning per-track metadata
|
|
227
|
+
/// (output path, cue name/id, and the originating AWB's AFS2 subkey).
|
|
228
|
+
pub fn extract_acb_tracks<R: Read + Seek>(
|
|
229
|
+
acb_file: R,
|
|
215
230
|
target_dir: &Path,
|
|
216
|
-
|
|
231
|
+
acb_file_path: Option<&Path>,
|
|
232
|
+
) -> Result<Vec<ExtractedTrackFile>, ExtractError> {
|
|
233
|
+
let utf = UtfTable::new(acb_file)?;
|
|
234
|
+
let track_list = TrackList::new(&utf)?;
|
|
235
|
+
let mut embedded_awb = load_embedded_awb(&utf.rows[0]);
|
|
236
|
+
let mut external_awbs = load_external_awbs(&utf.rows[0], acb_file_path);
|
|
237
|
+
|
|
238
|
+
fs::create_dir_all(target_dir)?;
|
|
239
|
+
|
|
240
|
+
let mut outputs = Vec::new();
|
|
241
|
+
for track in &track_list.tracks {
|
|
242
|
+
if let Some(info) =
|
|
243
|
+
extract_single_track_file(track, target_dir, &mut embedded_awb, &mut external_awbs)?
|
|
244
|
+
{
|
|
245
|
+
outputs.push(info);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
Ok(outputs)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn extract_single_track_file(
|
|
252
|
+
track: &Track,
|
|
253
|
+
target_dir: &Path,
|
|
254
|
+
embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
|
|
255
|
+
external_awbs: &mut [AfsArchive<Cursor<Vec<u8>>>],
|
|
256
|
+
) -> Result<Option<ExtractedTrackFile>, ExtractError> {
|
|
257
|
+
let ext = wave_type_extension(track.enc_type);
|
|
258
|
+
let ext = if ext.is_empty() {
|
|
259
|
+
format!(".{}", track.enc_type)
|
|
260
|
+
} else {
|
|
261
|
+
ext.to_string()
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
let filename = format!("{}{}", track.name, ext);
|
|
265
|
+
let output_path = target_dir.join(&filename);
|
|
266
|
+
|
|
267
|
+
let (data, subkey) = match get_track_data(track, embedded_awb, external_awbs)? {
|
|
268
|
+
Some(d) => d,
|
|
269
|
+
None => return Ok(None),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
fs::write(&output_path, data)?;
|
|
273
|
+
Ok(Some(ExtractedTrackFile {
|
|
274
|
+
path: output_path.to_string_lossy().into_owned(),
|
|
275
|
+
name: track.name.clone(),
|
|
276
|
+
cue_id: track.cue_id,
|
|
277
|
+
subkey,
|
|
278
|
+
}))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Open and validate an ACB file, returning the seekable handle positioned at
|
|
282
|
+
/// the start, or `None` if the path is missing or is not a valid ACB.
|
|
283
|
+
fn open_validated_acb(acb_path: &Path) -> Result<Option<File>, ExtractError> {
|
|
217
284
|
let info = match fs::metadata(acb_path) {
|
|
218
285
|
Ok(i) => i,
|
|
219
286
|
Err(_) => return Ok(None),
|
|
@@ -226,20 +293,40 @@ pub fn extract_acb_from_file(
|
|
|
226
293
|
|
|
227
294
|
let mut file = File::open(acb_path)?;
|
|
228
295
|
|
|
229
|
-
// Read and validate the
|
|
296
|
+
// Read and validate the @UTF magic (0x40 0x55 0x54 0x46)
|
|
230
297
|
let mut header = [0u8; 4];
|
|
231
|
-
use std::io::Read;
|
|
232
298
|
file.read_exact(&mut header)?;
|
|
233
|
-
|
|
234
|
-
// Check for @UTF magic (0x40 0x55 0x54 0x46)
|
|
235
299
|
if header != [0x40, 0x55, 0x54, 0x46] {
|
|
236
300
|
return Ok(None); // Not a valid ACB file
|
|
237
301
|
}
|
|
238
302
|
|
|
239
|
-
// Seek back to start
|
|
240
|
-
use std::io::Seek;
|
|
241
303
|
file.seek(std::io::SeekFrom::Start(0))?;
|
|
304
|
+
Ok(Some(file))
|
|
305
|
+
}
|
|
242
306
|
|
|
307
|
+
/// Convenience function to extract from a file path
|
|
308
|
+
pub fn extract_acb_from_file(
|
|
309
|
+
acb_path: &Path,
|
|
310
|
+
target_dir: &Path,
|
|
311
|
+
) -> Result<Option<Vec<String>>, ExtractError> {
|
|
312
|
+
let file = match open_validated_acb(acb_path)? {
|
|
313
|
+
Some(f) => f,
|
|
314
|
+
None => return Ok(None),
|
|
315
|
+
};
|
|
243
316
|
let outputs = extract_acb(file, target_dir, Some(acb_path))?;
|
|
244
317
|
Ok(Some(outputs))
|
|
245
318
|
}
|
|
319
|
+
|
|
320
|
+
/// Like [`extract_acb_from_file`], but returns per-track metadata (output path,
|
|
321
|
+
/// cue name/id, and AFS2 subkey) instead of just the written paths.
|
|
322
|
+
pub fn extract_acb_tracks_from_file(
|
|
323
|
+
acb_path: &Path,
|
|
324
|
+
target_dir: &Path,
|
|
325
|
+
) -> Result<Option<Vec<ExtractedTrackFile>>, ExtractError> {
|
|
326
|
+
let file = match open_validated_acb(acb_path)? {
|
|
327
|
+
Some(f) => f,
|
|
328
|
+
None => return Ok(None),
|
|
329
|
+
};
|
|
330
|
+
let outputs = extract_acb_tracks(file, target_dir, Some(acb_path))?;
|
|
331
|
+
Ok(Some(outputs))
|
|
332
|
+
}
|
|
@@ -12,6 +12,9 @@ pub use builder::{
|
|
|
12
12
|
AcbBuilder, AfsArchiveBuilder, BuilderError, ColumnDef, TrackInput, UtfTableBuilder,
|
|
13
13
|
};
|
|
14
14
|
pub use consts::*;
|
|
15
|
-
pub use extractor::{
|
|
15
|
+
pub use extractor::{
|
|
16
|
+
extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
|
|
17
|
+
extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
|
|
18
|
+
};
|
|
16
19
|
pub use track::{Track, TrackList};
|
|
17
20
|
pub use utf::{UtfHeader, UtfTable, Value};
|
|
@@ -14,7 +14,10 @@ pub mod usm;
|
|
|
14
14
|
mod python;
|
|
15
15
|
|
|
16
16
|
// ACB/AWB exports
|
|
17
|
-
pub use acb::{
|
|
17
|
+
pub use acb::{
|
|
18
|
+
extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
|
|
19
|
+
extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
|
|
20
|
+
};
|
|
18
21
|
pub use acb::{AcbBuilder, AfsArchiveBuilder, BuilderError, TrackInput, UtfTableBuilder};
|
|
19
22
|
|
|
20
23
|
// HCA exports
|
|
@@ -37,6 +37,101 @@ fn extract_acb(acb_path: &str, output_dir: &str) -> PyResult<Option<Vec<String>>
|
|
|
37
37
|
.map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/// Extract audio tracks from an ACB file, returning per-track metadata.
|
|
41
|
+
///
|
|
42
|
+
/// Unlike :func:`extract_acb`, this also surfaces each track's cue id and the
|
|
43
|
+
/// AFS2 subkey of the AWB it came from, which is required (together with the
|
|
44
|
+
/// global keycode) to decode type-56 encrypted HCA.
|
|
45
|
+
///
|
|
46
|
+
/// Args:
|
|
47
|
+
/// acb_path: Path to the ACB file
|
|
48
|
+
/// output_dir: Directory to write extracted files to
|
|
49
|
+
///
|
|
50
|
+
/// Returns:
|
|
51
|
+
/// List of dicts ``{"path", "name", "cue_id", "subkey"}``, or None if the
|
|
52
|
+
/// file is invalid.
|
|
53
|
+
#[pyfunction]
|
|
54
|
+
fn extract_acb_tracks<'py>(
|
|
55
|
+
py: Python<'py>,
|
|
56
|
+
acb_path: &str,
|
|
57
|
+
output_dir: &str,
|
|
58
|
+
) -> PyResult<Option<Vec<Bound<'py, pyo3::types::PyDict>>>> {
|
|
59
|
+
let acb_path = Path::new(acb_path);
|
|
60
|
+
let output_dir = Path::new(output_dir);
|
|
61
|
+
fs::create_dir_all(output_dir)
|
|
62
|
+
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create output dir: {}", e)))?;
|
|
63
|
+
|
|
64
|
+
let tracks = acb::extract_acb_tracks_from_file(acb_path, output_dir)
|
|
65
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))?;
|
|
66
|
+
|
|
67
|
+
let tracks = match tracks {
|
|
68
|
+
Some(t) => t,
|
|
69
|
+
None => return Ok(None),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let mut out = Vec::with_capacity(tracks.len());
|
|
73
|
+
for track in tracks {
|
|
74
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
75
|
+
dict.set_item("path", track.path)?;
|
|
76
|
+
dict.set_item("name", track.name)?;
|
|
77
|
+
dict.set_item("cue_id", track.cue_id)?;
|
|
78
|
+
dict.set_item("subkey", track.subkey)?;
|
|
79
|
+
out.push(dict);
|
|
80
|
+
}
|
|
81
|
+
Ok(Some(out))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Extract an ACB and decode its HCA tracks straight to WAV files.
|
|
85
|
+
///
|
|
86
|
+
/// The per-AWB AFS2 subkey is read and applied automatically, so encrypted
|
|
87
|
+
/// (type-56) ACBs only need the global ``key``. Non-HCA tracks are written out
|
|
88
|
+
/// verbatim with their original extension.
|
|
89
|
+
///
|
|
90
|
+
/// Args:
|
|
91
|
+
/// acb_path: Path to the ACB file
|
|
92
|
+
/// output_dir: Directory to write the decoded WAV (and any raw) files to
|
|
93
|
+
/// key: Global HCA keycode (omit/None for unencrypted ACBs)
|
|
94
|
+
///
|
|
95
|
+
/// Returns:
|
|
96
|
+
/// List of written file paths.
|
|
97
|
+
#[pyfunction]
|
|
98
|
+
#[pyo3(signature = (acb_path, output_dir, key=None))]
|
|
99
|
+
fn decode_acb_to_wav(acb_path: &str, output_dir: &str, key: Option<u64>) -> PyResult<Vec<String>> {
|
|
100
|
+
let out_dir = Path::new(output_dir);
|
|
101
|
+
fs::create_dir_all(out_dir)
|
|
102
|
+
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create output dir: {}", e)))?;
|
|
103
|
+
|
|
104
|
+
let file = fs::File::open(acb_path)
|
|
105
|
+
.map_err(|e| PyRuntimeError::new_err(format!("Failed to open ACB: {}", e)))?;
|
|
106
|
+
|
|
107
|
+
let tracks = acb::extract_acb_to_memory(file, Some(Path::new(acb_path)))
|
|
108
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))?;
|
|
109
|
+
|
|
110
|
+
let mut outputs = Vec::with_capacity(tracks.len());
|
|
111
|
+
for track in tracks {
|
|
112
|
+
if track.extension == "hca" {
|
|
113
|
+
let mut decoder = HcaDecoder::from_reader(Cursor::new(track.data))
|
|
114
|
+
.map_err(|e| PyRuntimeError::new_err(format!("Failed to parse HCA: {}", e)))?;
|
|
115
|
+
if let Some(k) = key {
|
|
116
|
+
decoder.set_encryption_key(k, track.subkey as u64);
|
|
117
|
+
}
|
|
118
|
+
let wav_path = out_dir.join(format!("{}.wav", track.name));
|
|
119
|
+
let mut wav = fs::File::create(&wav_path)
|
|
120
|
+
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create WAV: {}", e)))?;
|
|
121
|
+
decoder
|
|
122
|
+
.decode_to_wav(&mut wav)
|
|
123
|
+
.map_err(|e| PyRuntimeError::new_err(format!("HCA decode failed: {}", e)))?;
|
|
124
|
+
outputs.push(wav_path.to_string_lossy().into_owned());
|
|
125
|
+
} else {
|
|
126
|
+
let raw_path = out_dir.join(format!("{}.{}", track.name, track.extension));
|
|
127
|
+
fs::write(&raw_path, &track.data)
|
|
128
|
+
.map_err(|e| PyRuntimeError::new_err(format!("Failed to write track: {}", e)))?;
|
|
129
|
+
outputs.push(raw_path.to_string_lossy().into_owned());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
Ok(outputs)
|
|
133
|
+
}
|
|
134
|
+
|
|
40
135
|
/// Build an ACB file from track data.
|
|
41
136
|
///
|
|
42
137
|
/// Args:
|
|
@@ -515,6 +610,8 @@ fn read_usm_metadata(usm_path: &str) -> PyResult<String> {
|
|
|
515
610
|
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
516
611
|
// ACB functions
|
|
517
612
|
m.add_function(wrap_pyfunction!(extract_acb, m)?)?;
|
|
613
|
+
m.add_function(wrap_pyfunction!(extract_acb_tracks, m)?)?;
|
|
614
|
+
m.add_function(wrap_pyfunction!(decode_acb_to_wav, m)?)?;
|
|
518
615
|
m.add_function(wrap_pyfunction!(build_acb, m)?)?;
|
|
519
616
|
m.add_function(wrap_pyfunction!(build_acb_bytes, m)?)?;
|
|
520
617
|
m.add_function(wrap_pyfunction!(build_music_acb_bytes, m)?)?;
|
|
@@ -559,6 +559,43 @@ fn test_acb_builder_basic() {
|
|
|
559
559
|
assert_eq!(&extracted_data[0..4], b"HCA\x00");
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
/// Test extract_acb_tracks surfaces per-track metadata (path, cue id, subkey)
|
|
563
|
+
#[test]
|
|
564
|
+
fn test_acb_extract_tracks_metadata() {
|
|
565
|
+
use cridecoder::{extract_acb_tracks_from_file, AcbBuilder, TrackInput};
|
|
566
|
+
|
|
567
|
+
let dummy_hca = create_minimal_hca_header();
|
|
568
|
+
let mut builder = AcbBuilder::new();
|
|
569
|
+
builder.add_track(TrackInput::new("meta_track", 7, dummy_hca));
|
|
570
|
+
|
|
571
|
+
let mut output = Vec::new();
|
|
572
|
+
builder
|
|
573
|
+
.build(&mut Cursor::new(&mut output), None)
|
|
574
|
+
.expect("ACB build should succeed");
|
|
575
|
+
|
|
576
|
+
let dir = tempfile::tempdir().unwrap();
|
|
577
|
+
let acb_path = dir.path().join("built.acb");
|
|
578
|
+
std::fs::write(&acb_path, &output).unwrap();
|
|
579
|
+
|
|
580
|
+
let tracks = extract_acb_tracks_from_file(&acb_path, dir.path())
|
|
581
|
+
.expect("extract should succeed")
|
|
582
|
+
.expect("built ACB should be valid");
|
|
583
|
+
|
|
584
|
+
assert_eq!(tracks.len(), 1, "Should extract one track");
|
|
585
|
+
let track = &tracks[0];
|
|
586
|
+
assert_eq!(track.name, "meta_track");
|
|
587
|
+
// cue_id is the cue-table index (the builder lays a single track at cue 0).
|
|
588
|
+
assert_eq!(track.cue_id, 0);
|
|
589
|
+
// An unencrypted (builder-produced) AWB has no subkey.
|
|
590
|
+
assert_eq!(track.subkey, 0);
|
|
591
|
+
assert!(
|
|
592
|
+
std::path::Path::new(&track.path).exists(),
|
|
593
|
+
"Written track file should exist"
|
|
594
|
+
);
|
|
595
|
+
let data = std::fs::read(&track.path).expect("Should read extracted track");
|
|
596
|
+
assert_eq!(&data[0..4], b"HCA\x00");
|
|
597
|
+
}
|
|
598
|
+
|
|
562
599
|
/// Test ACB builder keeps Waveform AWB ids aligned with non-zero cue ids
|
|
563
600
|
#[test]
|
|
564
601
|
fn test_acb_builder_nonzero_cue_id() {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|