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.
Files changed (46) hide show
  1. {cridecoder-0.3.0 → cridecoder-0.3.2}/Cargo.lock +1 -1
  2. {cridecoder-0.3.0 → cridecoder-0.3.2}/Cargo.toml +1 -1
  3. {cridecoder-0.3.0 → cridecoder-0.3.2}/PKG-INFO +1 -1
  4. {cridecoder-0.3.0 → cridecoder-0.3.2}/cridecoder.pyi +63 -0
  5. {cridecoder-0.3.0 → cridecoder-0.3.2}/pyproject.toml +1 -1
  6. cridecoder-0.3.2/src/acb/decode.rs +107 -0
  7. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/extractor.rs +100 -10
  8. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb.rs +9 -1
  9. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/lib.rs +8 -1
  10. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/python.rs +177 -0
  11. {cridecoder-0.3.0 → cridecoder-0.3.2}/tests/integration_tests.rs +67 -0
  12. {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/copilot-instructions.md +0 -0
  13. {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/dependabot.yml +0 -0
  14. {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/workflows/ci.yml +0 -0
  15. {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/workflows/release-crate.yml +0 -0
  16. {cridecoder-0.3.0 → cridecoder-0.3.2}/.github/workflows/release-python.yml +0 -0
  17. {cridecoder-0.3.0 → cridecoder-0.3.2}/.gitignore +0 -0
  18. {cridecoder-0.3.0 → cridecoder-0.3.2}/AGENTS.md +0 -0
  19. {cridecoder-0.3.0 → cridecoder-0.3.2}/CLAUDE.md +0 -0
  20. {cridecoder-0.3.0 → cridecoder-0.3.2}/KNOWN_GAPS.md +0 -0
  21. {cridecoder-0.3.0 → cridecoder-0.3.2}/LICENSE +0 -0
  22. {cridecoder-0.3.0 → cridecoder-0.3.2}/README.md +0 -0
  23. {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/debug_acb.rs +0 -0
  24. {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/profile_hca.rs +0 -0
  25. {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/test_acb.rs +0 -0
  26. {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/test_hca.rs +0 -0
  27. {cridecoder-0.3.0 → cridecoder-0.3.2}/examples/test_usm.rs +0 -0
  28. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/afs.rs +0 -0
  29. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/builder.rs +0 -0
  30. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/consts.rs +0 -0
  31. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/track.rs +0 -0
  32. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/acb/utf.rs +0 -0
  33. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/ath.rs +0 -0
  34. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/bitreader.rs +0 -0
  35. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/cipher.rs +0 -0
  36. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/decoder.rs +0 -0
  37. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/encoder.rs +0 -0
  38. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/hca_file.rs +0 -0
  39. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/imdct.rs +0 -0
  40. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca/tables.rs +0 -0
  41. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/hca.rs +0 -0
  42. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/reader.rs +0 -0
  43. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm/builder.rs +0 -0
  44. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm/extractor.rs +0 -0
  45. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm/metadata.rs +0 -0
  46. {cridecoder-0.3.0 → cridecoder-0.3.2}/src/usm.rs +0 -0
@@ -28,7 +28,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
28
28
 
29
29
  [[package]]
30
30
  name = "cridecoder"
31
- version = "0.3.0"
31
+ version = "0.3.2"
32
32
  dependencies = [
33
33
  "byteorder",
34
34
  "encoding_rs",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cridecoder"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  edition = "2021"
5
5
  description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cridecoder
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cridecoder"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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
- /// Convenience function to extract from a file path
213
- pub fn extract_acb_from_file(
214
- acb_path: &Path,
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
- ) -> Result<Option<Vec<String>>, ExtractError> {
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 first 4 bytes
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 extractor::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
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::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
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