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.
Files changed (45) hide show
  1. {cridecoder-0.3.0 → cridecoder-0.3.1}/Cargo.lock +1 -1
  2. {cridecoder-0.3.0 → cridecoder-0.3.1}/Cargo.toml +1 -1
  3. {cridecoder-0.3.0 → cridecoder-0.3.1}/PKG-INFO +1 -1
  4. {cridecoder-0.3.0 → cridecoder-0.3.1}/cridecoder.pyi +26 -0
  5. {cridecoder-0.3.0 → cridecoder-0.3.1}/pyproject.toml +1 -1
  6. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/extractor.rs +97 -10
  7. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb.rs +4 -1
  8. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/lib.rs +4 -1
  9. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/python.rs +97 -0
  10. {cridecoder-0.3.0 → cridecoder-0.3.1}/tests/integration_tests.rs +37 -0
  11. {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/copilot-instructions.md +0 -0
  12. {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/dependabot.yml +0 -0
  13. {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/workflows/ci.yml +0 -0
  14. {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/workflows/release-crate.yml +0 -0
  15. {cridecoder-0.3.0 → cridecoder-0.3.1}/.github/workflows/release-python.yml +0 -0
  16. {cridecoder-0.3.0 → cridecoder-0.3.1}/.gitignore +0 -0
  17. {cridecoder-0.3.0 → cridecoder-0.3.1}/AGENTS.md +0 -0
  18. {cridecoder-0.3.0 → cridecoder-0.3.1}/CLAUDE.md +0 -0
  19. {cridecoder-0.3.0 → cridecoder-0.3.1}/KNOWN_GAPS.md +0 -0
  20. {cridecoder-0.3.0 → cridecoder-0.3.1}/LICENSE +0 -0
  21. {cridecoder-0.3.0 → cridecoder-0.3.1}/README.md +0 -0
  22. {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/debug_acb.rs +0 -0
  23. {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/profile_hca.rs +0 -0
  24. {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/test_acb.rs +0 -0
  25. {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/test_hca.rs +0 -0
  26. {cridecoder-0.3.0 → cridecoder-0.3.1}/examples/test_usm.rs +0 -0
  27. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/afs.rs +0 -0
  28. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/builder.rs +0 -0
  29. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/consts.rs +0 -0
  30. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/track.rs +0 -0
  31. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/acb/utf.rs +0 -0
  32. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/ath.rs +0 -0
  33. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/bitreader.rs +0 -0
  34. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/cipher.rs +0 -0
  35. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/decoder.rs +0 -0
  36. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/encoder.rs +0 -0
  37. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/hca_file.rs +0 -0
  38. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/imdct.rs +0 -0
  39. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca/tables.rs +0 -0
  40. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/hca.rs +0 -0
  41. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/reader.rs +0 -0
  42. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm/builder.rs +0 -0
  43. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm/extractor.rs +0 -0
  44. {cridecoder-0.3.0 → cridecoder-0.3.1}/src/usm/metadata.rs +0 -0
  45. {cridecoder-0.3.0 → cridecoder-0.3.1}/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.1"
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.1"
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.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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
  ...
@@ -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.1"
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" }
@@ -209,11 +209,78 @@ fn get_track_data(
209
209
  Ok(None)
210
210
  }
211
211
 
212
- /// Convenience function to extract from a file path
213
- pub fn extract_acb_from_file(
214
- acb_path: &Path,
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
- ) -> Result<Option<Vec<String>>, ExtractError> {
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 first 4 bytes
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::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
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::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
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