cridecoder 0.3.0__tar.gz → 0.3.2__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.2}/Cargo.lock +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.2}/Cargo.toml +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.2}/PKG-INFO +1 -1
- {cridecoder-0.3.0 → cridecoder-0.3.2}/cridecoder.pyi +63 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/pyproject.toml +1 -1
- cridecoder-0.3.2/src/acb/decode.rs +107 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/extractor.rs +100 -10
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb.rs +9 -1
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/lib.rs +8 -1
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/python.rs +177 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/tests/integration_tests.rs +67 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/copilot-instructions.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/dependabot.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/workflows/ci.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/workflows/release-crate.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/workflows/release-python.yml +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/.gitignore +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/AGENTS.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/CLAUDE.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/KNOWN_GAPS.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/LICENSE +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/README.md +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/debug_acb.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/profile_hca.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/test_acb.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/test_hca.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/test_usm.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/afs.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/builder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/consts.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/track.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/utf.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/ath.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/bitreader.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/cipher.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/decoder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/encoder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/hca_file.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/imdct.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/tables.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/reader.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm/builder.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm/extractor.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm/metadata.rs +0 -0
- {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm.rs +0 -0
|
@@ -9,6 +9,10 @@ from typing import Optional
|
|
|
9
9
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"extract_acb",
|
|
12
|
+
"extract_acb_tracks",
|
|
13
|
+
"extract_acb_bytes",
|
|
14
|
+
"decode_acb_to_wav",
|
|
15
|
+
"decode_acb_to_wav_bytes",
|
|
12
16
|
"build_acb",
|
|
13
17
|
"build_acb_bytes",
|
|
14
18
|
"build_music_acb_bytes",
|
|
@@ -17,6 +21,7 @@ __all__ = [
|
|
|
17
21
|
"encode_hca",
|
|
18
22
|
"encode_hca_bytes",
|
|
19
23
|
"extract_usm",
|
|
24
|
+
"extract_usm_bytes",
|
|
20
25
|
"build_usm",
|
|
21
26
|
"build_usm_bytes",
|
|
22
27
|
"read_usm_metadata",
|
|
@@ -31,6 +36,51 @@ def extract_acb(acb_path: str, output_dir: str) -> Optional[list[str]]:
|
|
|
31
36
|
"""
|
|
32
37
|
...
|
|
33
38
|
|
|
39
|
+
def extract_acb_tracks(
|
|
40
|
+
acb_path: str, output_dir: str
|
|
41
|
+
) -> Optional[list[dict[str, object]]]:
|
|
42
|
+
"""Extract audio tracks from an ACB, returning per-track metadata.
|
|
43
|
+
|
|
44
|
+
Like :func:`extract_acb`, but each entry is a dict with ``path`` (written
|
|
45
|
+
file), ``name`` (cue name), ``cue_id`` and ``subkey`` — the AFS2 subkey of
|
|
46
|
+
the originating AWB, needed (with the global keycode) to decode type-56
|
|
47
|
+
encrypted HCA. Returns ``None`` if the file is invalid.
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def extract_acb_bytes(acb_data: bytes) -> list[dict[str, object]]:
|
|
52
|
+
"""In-memory counterpart of :func:`extract_acb_tracks` (no disk I/O).
|
|
53
|
+
|
|
54
|
+
Takes the ACB bytes directly and returns the waveform bytes per track as a
|
|
55
|
+
list of dicts ``{"name", "cue_id", "extension", "subkey", "data"}`` (``data``
|
|
56
|
+
is ``bytes``). Only the embedded AWB is read — external streaming ``.awb``
|
|
57
|
+
archives need a path, so use :func:`extract_acb` for those.
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def decode_acb_to_wav(
|
|
62
|
+
acb_path: str, output_dir: str, key: Optional[int] = ...
|
|
63
|
+
) -> list[str]:
|
|
64
|
+
"""Extract an ACB and decode its HCA tracks straight to WAV files.
|
|
65
|
+
|
|
66
|
+
The per-AWB AFS2 subkey is applied automatically, so encrypted (type-56)
|
|
67
|
+
ACBs only need the global ``key`` (omit/``None`` for unencrypted ACBs).
|
|
68
|
+
Non-HCA tracks are written verbatim with their original extension. Returns
|
|
69
|
+
the list of written file paths.
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def decode_acb_to_wav_bytes(
|
|
74
|
+
acb_data: bytes, key: Optional[int] = ...
|
|
75
|
+
) -> list[dict[str, object]]:
|
|
76
|
+
"""In-memory counterpart of :func:`decode_acb_to_wav` (no disk I/O).
|
|
77
|
+
|
|
78
|
+
Returns a list of dicts ``{"name", "cue_id", "extension", "data"}`` where
|
|
79
|
+
``data`` is WAV ``bytes`` for HCA tracks (``extension == "wav"``); non-HCA
|
|
80
|
+
tracks are returned verbatim. Encrypted ACBs only need the global ``key``.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
83
|
+
|
|
34
84
|
def build_acb(tracks: list[tuple[str, int, bytes]], output_path: str) -> None:
|
|
35
85
|
"""Build an ACB file from ``(name, cue_id, hca_data)`` tuples, writing to disk."""
|
|
36
86
|
...
|
|
@@ -128,6 +178,19 @@ def extract_usm(
|
|
|
128
178
|
"""
|
|
129
179
|
...
|
|
130
180
|
|
|
181
|
+
def extract_usm_bytes(
|
|
182
|
+
usm_data: bytes,
|
|
183
|
+
key: Optional[int] = ...,
|
|
184
|
+
export_audio: bool = ...,
|
|
185
|
+
) -> list[dict[str, object]]:
|
|
186
|
+
"""In-memory counterpart of :func:`extract_usm` (no disk I/O).
|
|
187
|
+
|
|
188
|
+
Takes the USM bytes and returns each stream as a dict
|
|
189
|
+
``{"name", "extension", "data"}`` (``data`` is ``bytes``) — video, plus
|
|
190
|
+
audio when ``export_audio`` is set.
|
|
191
|
+
"""
|
|
192
|
+
...
|
|
193
|
+
|
|
131
194
|
def build_usm(
|
|
132
195
|
name: str,
|
|
133
196
|
video_data: bytes,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//! High-level ACB → WAV decoding.
|
|
2
|
+
//!
|
|
3
|
+
//! Composes ACB extraction with the HCA decoder so callers can go straight from
|
|
4
|
+
//! an ACB to decoded audio without managing the intermediate HCA bytes
|
|
5
|
+
//! themselves. The per-AWB AFS2 subkey is applied automatically.
|
|
6
|
+
|
|
7
|
+
use std::fs::{self, File};
|
|
8
|
+
use std::io::{Cursor, Read, Seek};
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
|
|
11
|
+
use thiserror::Error;
|
|
12
|
+
|
|
13
|
+
use crate::acb::extractor::{extract_acb_to_memory, ExtractError};
|
|
14
|
+
use crate::hca::{HcaDecoder, HcaDecoderError};
|
|
15
|
+
|
|
16
|
+
/// A decoded ACB track held in memory.
|
|
17
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
18
|
+
pub struct DecodedAcbTrack {
|
|
19
|
+
/// Cue name of the track (also the output file stem).
|
|
20
|
+
pub name: String,
|
|
21
|
+
/// Cue id of the track (cue-table index).
|
|
22
|
+
pub cue_id: i32,
|
|
23
|
+
/// Output extension: `"wav"` for a decoded HCA track, otherwise the
|
|
24
|
+
/// original waveform extension (non-HCA tracks are passed through raw).
|
|
25
|
+
pub extension: String,
|
|
26
|
+
/// Decoded WAV bytes (HCA), or the raw waveform bytes (non-HCA).
|
|
27
|
+
pub data: Vec<u8>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Error, Debug)]
|
|
31
|
+
pub enum DecodeAcbError {
|
|
32
|
+
#[error("ACB extraction failed: {0}")]
|
|
33
|
+
Extract(#[from] ExtractError),
|
|
34
|
+
#[error("HCA decode failed: {0}")]
|
|
35
|
+
Hca(#[from] HcaDecoderError),
|
|
36
|
+
#[error("IO error: {0}")]
|
|
37
|
+
Io(#[from] std::io::Error),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Extract an ACB and decode its HCA tracks to WAV, returning everything in
|
|
41
|
+
/// memory. The per-AWB AFS2 subkey is applied automatically, so encrypted
|
|
42
|
+
/// (type-56) ACBs only need the global `key`. Non-HCA tracks are returned
|
|
43
|
+
/// verbatim with their original extension.
|
|
44
|
+
pub fn decode_acb_to_wav_to_memory<R: Read + Seek>(
|
|
45
|
+
acb_file: R,
|
|
46
|
+
acb_file_path: Option<&Path>,
|
|
47
|
+
key: Option<u64>,
|
|
48
|
+
) -> Result<Vec<DecodedAcbTrack>, DecodeAcbError> {
|
|
49
|
+
let tracks = extract_acb_to_memory(acb_file, acb_file_path)?;
|
|
50
|
+
|
|
51
|
+
let mut outputs = Vec::with_capacity(tracks.len());
|
|
52
|
+
for track in tracks {
|
|
53
|
+
if track.extension == "hca" {
|
|
54
|
+
let mut decoder = HcaDecoder::from_reader(Cursor::new(track.data))?;
|
|
55
|
+
if let Some(k) = key {
|
|
56
|
+
decoder.set_encryption_key(k, track.subkey as u64);
|
|
57
|
+
}
|
|
58
|
+
let mut wav = Vec::new();
|
|
59
|
+
decoder.decode_to_wav(&mut wav)?;
|
|
60
|
+
outputs.push(DecodedAcbTrack {
|
|
61
|
+
name: track.name,
|
|
62
|
+
cue_id: track.cue_id,
|
|
63
|
+
extension: "wav".to_string(),
|
|
64
|
+
data: wav,
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
outputs.push(DecodedAcbTrack {
|
|
68
|
+
name: track.name,
|
|
69
|
+
cue_id: track.cue_id,
|
|
70
|
+
extension: track.extension,
|
|
71
|
+
data: track.data,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
Ok(outputs)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Like [`decode_acb_to_wav_to_memory`], but writes each decoded track to
|
|
79
|
+
/// `target_dir` and returns the written paths.
|
|
80
|
+
pub fn decode_acb_to_wav<R: Read + Seek>(
|
|
81
|
+
acb_file: R,
|
|
82
|
+
target_dir: &Path,
|
|
83
|
+
acb_file_path: Option<&Path>,
|
|
84
|
+
key: Option<u64>,
|
|
85
|
+
) -> Result<Vec<String>, DecodeAcbError> {
|
|
86
|
+
let tracks = decode_acb_to_wav_to_memory(acb_file, acb_file_path, key)?;
|
|
87
|
+
|
|
88
|
+
fs::create_dir_all(target_dir)?;
|
|
89
|
+
let mut outputs = Vec::with_capacity(tracks.len());
|
|
90
|
+
for track in tracks {
|
|
91
|
+
let path = target_dir.join(format!("{}.{}", track.name, track.extension));
|
|
92
|
+
fs::write(&path, &track.data)?;
|
|
93
|
+
outputs.push(path.to_string_lossy().into_owned());
|
|
94
|
+
}
|
|
95
|
+
Ok(outputs)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Convenience wrapper over [`decode_acb_to_wav`] that reads from a file path
|
|
99
|
+
/// (also used to resolve sibling external streaming `.awb` archives).
|
|
100
|
+
pub fn decode_acb_to_wav_from_file(
|
|
101
|
+
acb_path: &Path,
|
|
102
|
+
target_dir: &Path,
|
|
103
|
+
key: Option<u64>,
|
|
104
|
+
) -> Result<Vec<String>, DecodeAcbError> {
|
|
105
|
+
let file = File::open(acb_path)?;
|
|
106
|
+
decode_acb_to_wav(file, target_dir, Some(acb_path), key)
|
|
107
|
+
}
|
|
@@ -12,6 +12,8 @@ use thiserror::Error;
|
|
|
12
12
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
13
13
|
pub struct ExtractedAcbTrack {
|
|
14
14
|
pub name: String,
|
|
15
|
+
/// Cue id of the track (cue-table index).
|
|
16
|
+
pub cue_id: i32,
|
|
15
17
|
pub extension: String,
|
|
16
18
|
pub data: Vec<u8>,
|
|
17
19
|
/// AFS2 subkey of the AWB this waveform came from. Required (together with the
|
|
@@ -79,6 +81,7 @@ pub fn extract_acb_to_memory<R: Read + Seek>(
|
|
|
79
81
|
};
|
|
80
82
|
outputs.push(ExtractedAcbTrack {
|
|
81
83
|
name: track.name.clone(),
|
|
84
|
+
cue_id: track.cue_id,
|
|
82
85
|
extension,
|
|
83
86
|
data,
|
|
84
87
|
subkey,
|
|
@@ -209,11 +212,78 @@ fn get_track_data(
|
|
|
209
212
|
Ok(None)
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
///
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
/// A track extracted to disk together with the metadata needed to decode it.
|
|
216
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
217
|
+
pub struct ExtractedTrackFile {
|
|
218
|
+
/// Path of the written waveform file.
|
|
219
|
+
pub path: String,
|
|
220
|
+
/// Cue name of the track (also the file stem).
|
|
221
|
+
pub name: String,
|
|
222
|
+
/// Cue id of the track.
|
|
223
|
+
pub cue_id: i32,
|
|
224
|
+
/// AFS2 subkey of the originating AWB. Required (with the global keycode) to
|
|
225
|
+
/// decrypt type-56 encrypted HCA; 0 when the AWB is unencrypted.
|
|
226
|
+
pub subkey: u16,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Extract all audio tracks to `target_dir`, returning per-track metadata
|
|
230
|
+
/// (output path, cue name/id, and the originating AWB's AFS2 subkey).
|
|
231
|
+
pub fn extract_acb_tracks<R: Read + Seek>(
|
|
232
|
+
acb_file: R,
|
|
215
233
|
target_dir: &Path,
|
|
216
|
-
|
|
234
|
+
acb_file_path: Option<&Path>,
|
|
235
|
+
) -> Result<Vec<ExtractedTrackFile>, ExtractError> {
|
|
236
|
+
let utf = UtfTable::new(acb_file)?;
|
|
237
|
+
let track_list = TrackList::new(&utf)?;
|
|
238
|
+
let mut embedded_awb = load_embedded_awb(&utf.rows[0]);
|
|
239
|
+
let mut external_awbs = load_external_awbs(&utf.rows[0], acb_file_path);
|
|
240
|
+
|
|
241
|
+
fs::create_dir_all(target_dir)?;
|
|
242
|
+
|
|
243
|
+
let mut outputs = Vec::new();
|
|
244
|
+
for track in &track_list.tracks {
|
|
245
|
+
if let Some(info) =
|
|
246
|
+
extract_single_track_file(track, target_dir, &mut embedded_awb, &mut external_awbs)?
|
|
247
|
+
{
|
|
248
|
+
outputs.push(info);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
Ok(outputs)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
fn extract_single_track_file(
|
|
255
|
+
track: &Track,
|
|
256
|
+
target_dir: &Path,
|
|
257
|
+
embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
|
|
258
|
+
external_awbs: &mut [AfsArchive<Cursor<Vec<u8>>>],
|
|
259
|
+
) -> Result<Option<ExtractedTrackFile>, ExtractError> {
|
|
260
|
+
let ext = wave_type_extension(track.enc_type);
|
|
261
|
+
let ext = if ext.is_empty() {
|
|
262
|
+
format!(".{}", track.enc_type)
|
|
263
|
+
} else {
|
|
264
|
+
ext.to_string()
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
let filename = format!("{}{}", track.name, ext);
|
|
268
|
+
let output_path = target_dir.join(&filename);
|
|
269
|
+
|
|
270
|
+
let (data, subkey) = match get_track_data(track, embedded_awb, external_awbs)? {
|
|
271
|
+
Some(d) => d,
|
|
272
|
+
None => return Ok(None),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
fs::write(&output_path, data)?;
|
|
276
|
+
Ok(Some(ExtractedTrackFile {
|
|
277
|
+
path: output_path.to_string_lossy().into_owned(),
|
|
278
|
+
name: track.name.clone(),
|
|
279
|
+
cue_id: track.cue_id,
|
|
280
|
+
subkey,
|
|
281
|
+
}))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Open and validate an ACB file, returning the seekable handle positioned at
|
|
285
|
+
/// the start, or `None` if the path is missing or is not a valid ACB.
|
|
286
|
+
fn open_validated_acb(acb_path: &Path) -> Result<Option<File>, ExtractError> {
|
|
217
287
|
let info = match fs::metadata(acb_path) {
|
|
218
288
|
Ok(i) => i,
|
|
219
289
|
Err(_) => return Ok(None),
|
|
@@ -226,20 +296,40 @@ pub fn extract_acb_from_file(
|
|
|
226
296
|
|
|
227
297
|
let mut file = File::open(acb_path)?;
|
|
228
298
|
|
|
229
|
-
// Read and validate the
|
|
299
|
+
// Read and validate the @UTF magic (0x40 0x55 0x54 0x46)
|
|
230
300
|
let mut header = [0u8; 4];
|
|
231
|
-
use std::io::Read;
|
|
232
301
|
file.read_exact(&mut header)?;
|
|
233
|
-
|
|
234
|
-
// Check for @UTF magic (0x40 0x55 0x54 0x46)
|
|
235
302
|
if header != [0x40, 0x55, 0x54, 0x46] {
|
|
236
303
|
return Ok(None); // Not a valid ACB file
|
|
237
304
|
}
|
|
238
305
|
|
|
239
|
-
// Seek back to start
|
|
240
|
-
use std::io::Seek;
|
|
241
306
|
file.seek(std::io::SeekFrom::Start(0))?;
|
|
307
|
+
Ok(Some(file))
|
|
308
|
+
}
|
|
242
309
|
|
|
310
|
+
/// Convenience function to extract from a file path
|
|
311
|
+
pub fn extract_acb_from_file(
|
|
312
|
+
acb_path: &Path,
|
|
313
|
+
target_dir: &Path,
|
|
314
|
+
) -> Result<Option<Vec<String>>, ExtractError> {
|
|
315
|
+
let file = match open_validated_acb(acb_path)? {
|
|
316
|
+
Some(f) => f,
|
|
317
|
+
None => return Ok(None),
|
|
318
|
+
};
|
|
243
319
|
let outputs = extract_acb(file, target_dir, Some(acb_path))?;
|
|
244
320
|
Ok(Some(outputs))
|
|
245
321
|
}
|
|
322
|
+
|
|
323
|
+
/// Like [`extract_acb_from_file`], but returns per-track metadata (output path,
|
|
324
|
+
/// cue name/id, and AFS2 subkey) instead of just the written paths.
|
|
325
|
+
pub fn extract_acb_tracks_from_file(
|
|
326
|
+
acb_path: &Path,
|
|
327
|
+
target_dir: &Path,
|
|
328
|
+
) -> Result<Option<Vec<ExtractedTrackFile>>, ExtractError> {
|
|
329
|
+
let file = match open_validated_acb(acb_path)? {
|
|
330
|
+
Some(f) => f,
|
|
331
|
+
None => return Ok(None),
|
|
332
|
+
};
|
|
333
|
+
let outputs = extract_acb_tracks(file, target_dir, Some(acb_path))?;
|
|
334
|
+
Ok(Some(outputs))
|
|
335
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
mod afs;
|
|
4
4
|
mod builder;
|
|
5
5
|
mod consts;
|
|
6
|
+
mod decode;
|
|
6
7
|
mod extractor;
|
|
7
8
|
mod track;
|
|
8
9
|
mod utf;
|
|
@@ -12,6 +13,13 @@ pub use builder::{
|
|
|
12
13
|
AcbBuilder, AfsArchiveBuilder, BuilderError, ColumnDef, TrackInput, UtfTableBuilder,
|
|
13
14
|
};
|
|
14
15
|
pub use consts::*;
|
|
15
|
-
pub use
|
|
16
|
+
pub use decode::{
|
|
17
|
+
decode_acb_to_wav, decode_acb_to_wav_from_file, decode_acb_to_wav_to_memory, DecodeAcbError,
|
|
18
|
+
DecodedAcbTrack,
|
|
19
|
+
};
|
|
20
|
+
pub use extractor::{
|
|
21
|
+
extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
|
|
22
|
+
extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
|
|
23
|
+
};
|
|
16
24
|
pub use track::{Track, TrackList};
|
|
17
25
|
pub use utf::{UtfHeader, UtfTable, Value};
|
|
@@ -14,7 +14,14 @@ pub mod usm;
|
|
|
14
14
|
mod python;
|
|
15
15
|
|
|
16
16
|
// ACB/AWB exports
|
|
17
|
-
pub use acb::{
|
|
17
|
+
pub use acb::{
|
|
18
|
+
decode_acb_to_wav, decode_acb_to_wav_from_file, decode_acb_to_wav_to_memory, DecodeAcbError,
|
|
19
|
+
DecodedAcbTrack,
|
|
20
|
+
};
|
|
21
|
+
pub use acb::{
|
|
22
|
+
extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
|
|
23
|
+
extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
|
|
24
|
+
};
|
|
18
25
|
pub use acb::{AcbBuilder, AfsArchiveBuilder, BuilderError, TrackInput, UtfTableBuilder};
|
|
19
26
|
|
|
20
27
|
// HCA exports
|
|
@@ -37,6 +37,142 @@ 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
|
+
acb::decode_acb_to_wav_from_file(Path::new(acb_path), out_dir, key)
|
|
105
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB decode failed: {}", e)))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Extract audio tracks from in-memory ACB bytes (no disk I/O).
|
|
109
|
+
///
|
|
110
|
+
/// The in-memory counterpart of :func:`extract_acb_tracks`: takes the ACB
|
|
111
|
+
/// bytes directly and returns the waveform bytes per track instead of writing
|
|
112
|
+
/// files. Only the embedded AWB is read — external streaming ``.awb`` archives
|
|
113
|
+
/// can't be resolved without a path, so use :func:`extract_acb` for those.
|
|
114
|
+
///
|
|
115
|
+
/// Args:
|
|
116
|
+
/// acb_data: Raw ACB file bytes
|
|
117
|
+
///
|
|
118
|
+
/// Returns:
|
|
119
|
+
/// List of dicts ``{"name", "cue_id", "extension", "subkey", "data"}``.
|
|
120
|
+
#[pyfunction]
|
|
121
|
+
fn extract_acb_bytes<'py>(
|
|
122
|
+
py: Python<'py>,
|
|
123
|
+
acb_data: &[u8],
|
|
124
|
+
) -> PyResult<Vec<Bound<'py, pyo3::types::PyDict>>> {
|
|
125
|
+
let tracks = acb::extract_acb_to_memory(Cursor::new(acb_data.to_vec()), None)
|
|
126
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))?;
|
|
127
|
+
|
|
128
|
+
let mut out = Vec::with_capacity(tracks.len());
|
|
129
|
+
for track in tracks {
|
|
130
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
131
|
+
dict.set_item("name", track.name)?;
|
|
132
|
+
dict.set_item("cue_id", track.cue_id)?;
|
|
133
|
+
dict.set_item("extension", track.extension)?;
|
|
134
|
+
dict.set_item("subkey", track.subkey)?;
|
|
135
|
+
dict.set_item("data", pyo3::types::PyBytes::new(py, &track.data))?;
|
|
136
|
+
out.push(dict);
|
|
137
|
+
}
|
|
138
|
+
Ok(out)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Decode an in-memory ACB straight to WAV bytes (no disk I/O).
|
|
142
|
+
///
|
|
143
|
+
/// The in-memory counterpart of :func:`decode_acb_to_wav`: each AWB's subkey is
|
|
144
|
+
/// applied automatically, so encrypted ACBs only need the global ``key``.
|
|
145
|
+
/// Non-HCA tracks are returned verbatim (``extension`` reflects this).
|
|
146
|
+
///
|
|
147
|
+
/// Args:
|
|
148
|
+
/// acb_data: Raw ACB file bytes
|
|
149
|
+
/// key: Global HCA keycode (omit/None for unencrypted ACBs)
|
|
150
|
+
///
|
|
151
|
+
/// Returns:
|
|
152
|
+
/// List of dicts ``{"name", "cue_id", "extension", "data"}`` where ``data``
|
|
153
|
+
/// is WAV bytes for HCA tracks (``extension == "wav"``).
|
|
154
|
+
#[pyfunction]
|
|
155
|
+
#[pyo3(signature = (acb_data, key=None))]
|
|
156
|
+
fn decode_acb_to_wav_bytes<'py>(
|
|
157
|
+
py: Python<'py>,
|
|
158
|
+
acb_data: &[u8],
|
|
159
|
+
key: Option<u64>,
|
|
160
|
+
) -> PyResult<Vec<Bound<'py, pyo3::types::PyDict>>> {
|
|
161
|
+
let tracks = acb::decode_acb_to_wav_to_memory(Cursor::new(acb_data.to_vec()), None, key)
|
|
162
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB decode failed: {}", e)))?;
|
|
163
|
+
|
|
164
|
+
let mut out = Vec::with_capacity(tracks.len());
|
|
165
|
+
for track in tracks {
|
|
166
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
167
|
+
dict.set_item("name", track.name)?;
|
|
168
|
+
dict.set_item("cue_id", track.cue_id)?;
|
|
169
|
+
dict.set_item("extension", track.extension)?;
|
|
170
|
+
dict.set_item("data", pyo3::types::PyBytes::new(py, &track.data))?;
|
|
171
|
+
out.push(dict);
|
|
172
|
+
}
|
|
173
|
+
Ok(out)
|
|
174
|
+
}
|
|
175
|
+
|
|
40
176
|
/// Build an ACB file from track data.
|
|
41
177
|
///
|
|
42
178
|
/// Args:
|
|
@@ -430,6 +566,42 @@ fn extract_usm(
|
|
|
430
566
|
.collect())
|
|
431
567
|
}
|
|
432
568
|
|
|
569
|
+
/// Extract USM streams from in-memory bytes (no disk I/O).
|
|
570
|
+
///
|
|
571
|
+
/// The in-memory counterpart of :func:`extract_usm`: takes the USM bytes and
|
|
572
|
+
/// returns each stream's bytes instead of writing files.
|
|
573
|
+
///
|
|
574
|
+
/// Args:
|
|
575
|
+
/// usm_data: Raw USM file bytes
|
|
576
|
+
/// key: Optional decryption key (u64)
|
|
577
|
+
/// export_audio: Whether to include audio streams (default: false)
|
|
578
|
+
///
|
|
579
|
+
/// Returns:
|
|
580
|
+
/// List of dicts ``{"name", "extension", "data"}`` (video, and audio when
|
|
581
|
+
/// ``export_audio`` is set).
|
|
582
|
+
#[pyfunction]
|
|
583
|
+
#[pyo3(signature = (usm_data, key=None, export_audio=false))]
|
|
584
|
+
fn extract_usm_bytes<'py>(
|
|
585
|
+
py: Python<'py>,
|
|
586
|
+
usm_data: &[u8],
|
|
587
|
+
key: Option<u64>,
|
|
588
|
+
export_audio: bool,
|
|
589
|
+
) -> PyResult<Vec<Bound<'py, pyo3::types::PyDict>>> {
|
|
590
|
+
let streams =
|
|
591
|
+
usm::extract_usm_to_memory(Cursor::new(usm_data.to_vec()), b"", key, export_audio)
|
|
592
|
+
.map_err(|e| PyRuntimeError::new_err(format!("USM extraction failed: {}", e)))?;
|
|
593
|
+
|
|
594
|
+
let mut out = Vec::with_capacity(streams.len());
|
|
595
|
+
for stream in streams {
|
|
596
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
597
|
+
dict.set_item("name", stream.name)?;
|
|
598
|
+
dict.set_item("extension", stream.extension)?;
|
|
599
|
+
dict.set_item("data", pyo3::types::PyBytes::new(py, &stream.data))?;
|
|
600
|
+
out.push(dict);
|
|
601
|
+
}
|
|
602
|
+
Ok(out)
|
|
603
|
+
}
|
|
604
|
+
|
|
433
605
|
/// Build a USM file from video data.
|
|
434
606
|
///
|
|
435
607
|
/// Args:
|
|
@@ -515,6 +687,10 @@ fn read_usm_metadata(usm_path: &str) -> PyResult<String> {
|
|
|
515
687
|
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
516
688
|
// ACB functions
|
|
517
689
|
m.add_function(wrap_pyfunction!(extract_acb, m)?)?;
|
|
690
|
+
m.add_function(wrap_pyfunction!(extract_acb_tracks, m)?)?;
|
|
691
|
+
m.add_function(wrap_pyfunction!(extract_acb_bytes, m)?)?;
|
|
692
|
+
m.add_function(wrap_pyfunction!(decode_acb_to_wav, m)?)?;
|
|
693
|
+
m.add_function(wrap_pyfunction!(decode_acb_to_wav_bytes, m)?)?;
|
|
518
694
|
m.add_function(wrap_pyfunction!(build_acb, m)?)?;
|
|
519
695
|
m.add_function(wrap_pyfunction!(build_acb_bytes, m)?)?;
|
|
520
696
|
m.add_function(wrap_pyfunction!(build_music_acb_bytes, m)?)?;
|
|
@@ -527,6 +703,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
527
703
|
|
|
528
704
|
// USM functions
|
|
529
705
|
m.add_function(wrap_pyfunction!(extract_usm, m)?)?;
|
|
706
|
+
m.add_function(wrap_pyfunction!(extract_usm_bytes, m)?)?;
|
|
530
707
|
m.add_function(wrap_pyfunction!(build_usm, m)?)?;
|
|
531
708
|
m.add_function(wrap_pyfunction!(build_usm_bytes, m)?)?;
|
|
532
709
|
m.add_function(wrap_pyfunction!(read_usm_metadata, m)?)?;
|
|
@@ -559,6 +559,73 @@ 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
|
+
|
|
599
|
+
/// Test extract_acb_to_memory returns track bytes with cue id and subkey
|
|
600
|
+
#[test]
|
|
601
|
+
fn test_acb_extract_to_memory() {
|
|
602
|
+
use cridecoder::{extract_acb_to_memory, AcbBuilder, TrackInput};
|
|
603
|
+
use std::io::Cursor;
|
|
604
|
+
|
|
605
|
+
let mut builder = AcbBuilder::new();
|
|
606
|
+
builder.add_track(TrackInput::new("mem_a", 0, create_minimal_hca_header()));
|
|
607
|
+
builder.add_track(TrackInput::new("mem_b", 1, create_minimal_hca_header()));
|
|
608
|
+
|
|
609
|
+
let mut output = Vec::new();
|
|
610
|
+
builder
|
|
611
|
+
.build(&mut Cursor::new(&mut output), None)
|
|
612
|
+
.expect("ACB build should succeed");
|
|
613
|
+
|
|
614
|
+
let tracks =
|
|
615
|
+
extract_acb_to_memory(Cursor::new(output), None).expect("in-memory extract should succeed");
|
|
616
|
+
|
|
617
|
+
assert_eq!(tracks.len(), 2);
|
|
618
|
+
assert_eq!(tracks[0].name, "mem_a");
|
|
619
|
+
assert_eq!(tracks[0].cue_id, 0);
|
|
620
|
+
assert_eq!(tracks[1].name, "mem_b");
|
|
621
|
+
assert_eq!(tracks[1].cue_id, 1);
|
|
622
|
+
for track in &tracks {
|
|
623
|
+
assert_eq!(track.extension, "hca");
|
|
624
|
+
assert_eq!(track.subkey, 0); // builder produces unencrypted AWBs
|
|
625
|
+
assert_eq!(&track.data[0..4], b"HCA\x00");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
562
629
|
/// Test ACB builder keeps Waveform AWB ids aligned with non-zero cue ids
|
|
563
630
|
#[test]
|
|
564
631
|
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
|