cridecoder 0.2.0__tar.gz → 0.2.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 (44) hide show
  1. {cridecoder-0.2.0 → cridecoder-0.2.1}/Cargo.lock +1 -1
  2. {cridecoder-0.2.0 → cridecoder-0.2.1}/Cargo.toml +1 -1
  3. {cridecoder-0.2.0 → cridecoder-0.2.1}/PKG-INFO +1 -1
  4. cridecoder-0.2.1/cridecoder.pyi +114 -0
  5. {cridecoder-0.2.0 → cridecoder-0.2.1}/pyproject.toml +2 -1
  6. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb/extractor.rs +64 -0
  7. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb.rs +1 -1
  8. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/lib.rs +4 -2
  9. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/python.rs +79 -0
  10. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/usm/extractor.rs +65 -6
  11. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/usm.rs +3 -1
  12. {cridecoder-0.2.0 → cridecoder-0.2.1}/tests/integration_tests.rs +36 -0
  13. cridecoder-0.2.1/tests/test_python.py +332 -0
  14. {cridecoder-0.2.0 → cridecoder-0.2.1}/.github/copilot-instructions.md +0 -0
  15. {cridecoder-0.2.0 → cridecoder-0.2.1}/.github/dependabot.yml +0 -0
  16. {cridecoder-0.2.0 → cridecoder-0.2.1}/.github/workflows/ci.yml +0 -0
  17. {cridecoder-0.2.0 → cridecoder-0.2.1}/.github/workflows/release-crate.yml +0 -0
  18. {cridecoder-0.2.0 → cridecoder-0.2.1}/.github/workflows/release-python.yml +0 -0
  19. {cridecoder-0.2.0 → cridecoder-0.2.1}/.gitignore +0 -0
  20. {cridecoder-0.2.0 → cridecoder-0.2.1}/AGENTS.md +0 -0
  21. {cridecoder-0.2.0 → cridecoder-0.2.1}/CLAUDE.md +0 -0
  22. {cridecoder-0.2.0 → cridecoder-0.2.1}/LICENSE +0 -0
  23. {cridecoder-0.2.0 → cridecoder-0.2.1}/README.md +0 -0
  24. {cridecoder-0.2.0 → cridecoder-0.2.1}/examples/debug_acb.rs +0 -0
  25. {cridecoder-0.2.0 → cridecoder-0.2.1}/examples/test_acb.rs +0 -0
  26. {cridecoder-0.2.0 → cridecoder-0.2.1}/examples/test_hca.rs +0 -0
  27. {cridecoder-0.2.0 → cridecoder-0.2.1}/examples/test_usm.rs +0 -0
  28. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb/afs.rs +0 -0
  29. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb/builder.rs +0 -0
  30. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb/consts.rs +0 -0
  31. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb/track.rs +0 -0
  32. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/acb/utf.rs +0 -0
  33. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/ath.rs +0 -0
  34. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/bitreader.rs +0 -0
  35. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/cipher.rs +0 -0
  36. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/decoder.rs +0 -0
  37. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/encoder.rs +0 -0
  38. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/hca_file.rs +0 -0
  39. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/imdct.rs +0 -0
  40. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca/tables.rs +0 -0
  41. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/hca.rs +0 -0
  42. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/reader.rs +0 -0
  43. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/usm/builder.rs +0 -0
  44. {cridecoder-0.2.0 → cridecoder-0.2.1}/src/usm/metadata.rs +0 -0
@@ -28,7 +28,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
28
28
 
29
29
  [[package]]
30
30
  name = "cridecoder"
31
- version = "0.2.0"
31
+ version = "0.2.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.2.0"
3
+ version = "0.2.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.2.0
3
+ Version: 0.2.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, TypedDict
4
+
5
+
6
+ class ExtractedTrack(TypedDict):
7
+ name: str
8
+ extension: str
9
+ filename: str
10
+ data: bytes
11
+
12
+
13
+ class HcaInfo(TypedDict):
14
+ sample_rate: int
15
+ channels: int
16
+ block_count: int
17
+ block_size: int
18
+ encoder_delay: int
19
+ samples_per_block: int
20
+
21
+
22
+ class EncodeInfo(TypedDict):
23
+ size: int
24
+ bitrate: int
25
+
26
+
27
+ def extract_acb(acb_path: str, output_dir: str) -> Optional[list[str]]: ...
28
+
29
+
30
+ def extract_acb_bytes(acb_data: bytes, acb_path: Optional[str] = None) -> list[ExtractedTrack]: ...
31
+
32
+
33
+ def build_acb(tracks: list[tuple[str, int, bytes]], output_path: str) -> None: ...
34
+
35
+
36
+ def build_acb_bytes(tracks: list[tuple[str, int, bytes]]) -> bytes: ...
37
+
38
+
39
+ def build_music_acb_bytes(
40
+ name: str,
41
+ hca_data: bytes,
42
+ cue_id: int,
43
+ virtual_cue_suffix: Optional[str],
44
+ memory_awb_id: int,
45
+ reference_num_samples: int,
46
+ reference_length_ms: int,
47
+ acb_version: int,
48
+ acf_md5_hash: bytes,
49
+ acb_guid: bytes,
50
+ version_string: str,
51
+ acb_volume: float,
52
+ category_extension: int,
53
+ cue_priority_type: int,
54
+ acf_category_name: str,
55
+ acf_category_id: int,
56
+ acf_bus_names: list[str],
57
+ ) -> bytes: ...
58
+
59
+
60
+ def decode_hca(hca_path: str, wav_path: str) -> HcaInfo: ...
61
+
62
+
63
+ def decode_hca_bytes(hca_data: bytes) -> bytes: ...
64
+
65
+
66
+ def encode_hca(
67
+ wav_path: str,
68
+ hca_path: str,
69
+ bitrate: int = 256000,
70
+ encryption_key: Optional[int] = None,
71
+ ) -> EncodeInfo: ...
72
+
73
+
74
+ def encode_hca_bytes(
75
+ wav_data: bytes,
76
+ sample_rate: Optional[int] = None,
77
+ channels: Optional[int] = None,
78
+ bitrate: int = 256000,
79
+ encryption_key: Optional[int] = None,
80
+ ) -> bytes: ...
81
+
82
+
83
+ def extract_usm(
84
+ usm_path: str,
85
+ output_dir: str,
86
+ key: Optional[int] = None,
87
+ export_audio: bool = False,
88
+ ) -> list[str]: ...
89
+
90
+
91
+ def extract_usm_bytes(
92
+ usm_data: bytes,
93
+ fallback_name: str = "input.usm",
94
+ key: Optional[int] = None,
95
+ export_audio: bool = False,
96
+ ) -> list[ExtractedTrack]: ...
97
+
98
+
99
+ def build_usm(
100
+ name: str,
101
+ video_data: bytes,
102
+ output_path: str,
103
+ encryption_key: Optional[int] = None,
104
+ ) -> None: ...
105
+
106
+
107
+ def build_usm_bytes(
108
+ name: str,
109
+ video_data: bytes,
110
+ encryption_key: Optional[int] = None,
111
+ ) -> bytes: ...
112
+
113
+
114
+ def read_usm_metadata(usm_path: str) -> str: ...
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cridecoder"
7
- version = "0.2.0"
7
+ version = "0.2.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" }
@@ -23,3 +23,4 @@ free-threaded = true
23
23
 
24
24
  [tool.maturin]
25
25
  features = ["python"]
26
+ include = [{ path = "cridecoder.pyi", format = "wheel" }]
@@ -23,6 +23,14 @@ pub enum ExtractError {
23
23
  InvalidAcb,
24
24
  }
25
25
 
26
+ /// Audio track data extracted from an ACB/AWB container.
27
+ #[derive(Debug, Clone, PartialEq, Eq)]
28
+ pub struct ExtractedAcbTrack {
29
+ pub name: String,
30
+ pub extension: String,
31
+ pub data: Vec<u8>,
32
+ }
33
+
26
34
  /// Extract all audio files from an ACB file
27
35
  pub fn extract_acb<R: Read + Seek>(
28
36
  acb_file: R,
@@ -44,6 +52,21 @@ pub fn extract_acb<R: Read + Seek>(
44
52
  )
45
53
  }
46
54
 
55
+ /// Extract all audio tracks from an ACB reader into memory.
56
+ pub fn extract_acb_to_memory<R: Read + Seek>(
57
+ acb_file: R,
58
+ acb_file_path: Option<&Path>,
59
+ ) -> Result<Vec<ExtractedAcbTrack>, ExtractError> {
60
+ let utf = UtfTable::new(acb_file)?;
61
+
62
+ let track_list = TrackList::new(&utf)?;
63
+
64
+ let mut embedded_awb = load_embedded_awb(&utf.rows[0]);
65
+ let mut external_awbs = load_external_awbs(&utf.rows[0], acb_file_path);
66
+
67
+ extract_all_tracks_to_memory(&track_list, &mut embedded_awb, &mut external_awbs)
68
+ }
69
+
47
70
  fn load_embedded_awb(
48
71
  row: &std::collections::HashMap<String, crate::acb::utf::Value>,
49
72
  ) -> Option<AfsArchive<Cursor<Vec<u8>>>> {
@@ -120,6 +143,22 @@ fn extract_all_tracks(
120
143
  Ok(outputs)
121
144
  }
122
145
 
146
+ fn extract_all_tracks_to_memory(
147
+ track_list: &TrackList,
148
+ embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
149
+ external_awbs: &mut [AfsArchive<Cursor<Vec<u8>>>],
150
+ ) -> Result<Vec<ExtractedAcbTrack>, ExtractError> {
151
+ let mut outputs = Vec::new();
152
+
153
+ for track in &track_list.tracks {
154
+ if let Some(output) = extract_single_track_to_memory(track, embedded_awb, external_awbs)? {
155
+ outputs.push(output);
156
+ }
157
+ }
158
+
159
+ Ok(outputs)
160
+ }
161
+
123
162
  fn extract_single_track(
124
163
  track: &Track,
125
164
  target_dir: &Path,
@@ -146,6 +185,31 @@ fn extract_single_track(
146
185
  Ok(Some(output_path.to_string_lossy().into_owned()))
147
186
  }
148
187
 
188
+ fn extract_single_track_to_memory(
189
+ track: &Track,
190
+ embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
191
+ external_awbs: &mut [AfsArchive<Cursor<Vec<u8>>>],
192
+ ) -> Result<Option<ExtractedAcbTrack>, ExtractError> {
193
+ let ext = wave_type_extension(track.enc_type);
194
+ let extension = if ext.is_empty() {
195
+ track.enc_type.to_string()
196
+ } else {
197
+ ext.trim_start_matches('.').to_string()
198
+ };
199
+
200
+ let data = get_track_data(track, embedded_awb, external_awbs)?;
201
+ let data = match data {
202
+ Some(d) => d,
203
+ None => return Ok(None),
204
+ };
205
+
206
+ Ok(Some(ExtractedAcbTrack {
207
+ name: track.name.clone(),
208
+ extension,
209
+ data,
210
+ }))
211
+ }
212
+
149
213
  fn get_track_data(
150
214
  track: &Track,
151
215
  embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
@@ -12,6 +12,6 @@ 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};
15
+ pub use extractor::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
16
16
  pub use track::{Track, TrackList};
17
17
  pub use utf::{UtfHeader, UtfTable, Value};
@@ -14,7 +14,7 @@ pub mod usm;
14
14
  mod python;
15
15
 
16
16
  // ACB/AWB exports
17
- pub use acb::{extract_acb, extract_acb_from_file};
17
+ pub use acb::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
18
18
  pub use acb::{AcbBuilder, AfsArchiveBuilder, BuilderError, TrackInput, UtfTableBuilder};
19
19
 
20
20
  // HCA exports
@@ -22,7 +22,9 @@ pub use hca::{encode_wav_to_hca, HcaEncoder, HcaEncoderConfig, HcaEncoderError};
22
22
  pub use hca::{HcaDecoder, HcaDecoderError, HcaInfo};
23
23
 
24
24
  // USM exports
25
- pub use usm::{extract_usm, extract_usm_file, Metadata, UsmError};
25
+ pub use usm::{
26
+ extract_usm, extract_usm_file, extract_usm_to_memory, ExtractedUsmStream, Metadata, UsmError,
27
+ };
26
28
  pub use usm::{UsmBuilder, UsmBuilderError};
27
29
 
28
30
  #[cfg(feature = "python")]
@@ -7,6 +7,7 @@
7
7
 
8
8
  use pyo3::exceptions::PyRuntimeError;
9
9
  use pyo3::prelude::*;
10
+ use pyo3::types::{PyBytes, PyDict, PyList};
10
11
 
11
12
  use std::fs;
12
13
  use std::io::Cursor;
@@ -37,6 +38,38 @@ fn extract_acb(acb_path: &str, output_dir: &str) -> PyResult<Option<Vec<String>>
37
38
  .map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))
38
39
  }
39
40
 
41
+ /// Extract audio tracks from ACB data into memory.
42
+ ///
43
+ /// Args:
44
+ /// acb_data: Raw ACB file data as bytes
45
+ /// acb_path: Optional ACB path, used to resolve external AWB files
46
+ ///
47
+ /// Returns:
48
+ /// List of dicts with name, extension, filename, and data bytes
49
+ #[pyfunction]
50
+ #[pyo3(signature = (acb_data, acb_path=None))]
51
+ fn extract_acb_bytes<'py>(
52
+ py: Python<'py>,
53
+ acb_data: &[u8],
54
+ acb_path: Option<String>,
55
+ ) -> PyResult<Bound<'py, PyList>> {
56
+ let acb_path = acb_path.as_deref().map(Path::new);
57
+ let tracks = acb::extract_acb_to_memory(Cursor::new(acb_data.to_vec()), acb_path)
58
+ .map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))?;
59
+
60
+ let list = PyList::empty(py);
61
+ for track in tracks {
62
+ let dict = PyDict::new(py);
63
+ let filename = format!("{}.{}", track.name, track.extension);
64
+ dict.set_item("name", track.name)?;
65
+ dict.set_item("extension", track.extension)?;
66
+ dict.set_item("filename", filename)?;
67
+ dict.set_item("data", PyBytes::new(py, &track.data))?;
68
+ list.append(dict)?;
69
+ }
70
+ Ok(list)
71
+ }
72
+
40
73
  /// Build an ACB file from track data.
41
74
  ///
42
75
  /// Args:
@@ -419,6 +452,50 @@ fn extract_usm(
419
452
  .collect())
420
453
  }
421
454
 
455
+ /// Extract video/audio streams from USM data into memory.
456
+ ///
457
+ /// Args:
458
+ /// usm_data: Raw USM file data as bytes
459
+ /// fallback_name: Name used when the USM metadata has no filename
460
+ /// key: Optional decryption key (u64)
461
+ /// export_audio: Whether to export audio streams (default: false)
462
+ ///
463
+ /// Returns:
464
+ /// List of dicts with name, extension, filename, and data bytes
465
+ #[pyfunction]
466
+ #[pyo3(signature = (usm_data, fallback_name="input.usm", key=None, export_audio=false))]
467
+ fn extract_usm_bytes<'py>(
468
+ py: Python<'py>,
469
+ usm_data: &[u8],
470
+ fallback_name: &str,
471
+ key: Option<u64>,
472
+ export_audio: bool,
473
+ ) -> PyResult<Bound<'py, PyList>> {
474
+ let streams = usm::extract_usm_to_memory(
475
+ Cursor::new(usm_data.to_vec()),
476
+ fallback_name.as_bytes(),
477
+ key,
478
+ export_audio,
479
+ )
480
+ .map_err(|e| PyRuntimeError::new_err(format!("USM extraction failed: {}", e)))?;
481
+
482
+ let list = PyList::empty(py);
483
+ for stream in streams {
484
+ let dict = PyDict::new(py);
485
+ let name = Path::new(&stream.filename)
486
+ .file_stem()
487
+ .and_then(|stem| stem.to_str())
488
+ .unwrap_or(&stream.filename)
489
+ .to_string();
490
+ dict.set_item("name", name)?;
491
+ dict.set_item("extension", stream.extension)?;
492
+ dict.set_item("filename", stream.filename)?;
493
+ dict.set_item("data", PyBytes::new(py, &stream.data))?;
494
+ list.append(dict)?;
495
+ }
496
+ Ok(list)
497
+ }
498
+
422
499
  /// Build a USM file from video data.
423
500
  ///
424
501
  /// Args:
@@ -504,6 +581,7 @@ fn read_usm_metadata(usm_path: &str) -> PyResult<String> {
504
581
  pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
505
582
  // ACB functions
506
583
  m.add_function(wrap_pyfunction!(extract_acb, m)?)?;
584
+ m.add_function(wrap_pyfunction!(extract_acb_bytes, m)?)?;
507
585
  m.add_function(wrap_pyfunction!(build_acb, m)?)?;
508
586
  m.add_function(wrap_pyfunction!(build_acb_bytes, m)?)?;
509
587
  m.add_function(wrap_pyfunction!(build_music_acb_bytes, m)?)?;
@@ -516,6 +594,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
516
594
 
517
595
  // USM functions
518
596
  m.add_function(wrap_pyfunction!(extract_usm, m)?)?;
597
+ m.add_function(wrap_pyfunction!(extract_usm_bytes, m)?)?;
519
598
  m.add_function(wrap_pyfunction!(build_usm, m)?)?;
520
599
  m.add_function(wrap_pyfunction!(build_usm_bytes, m)?)?;
521
600
  m.add_function(wrap_pyfunction!(read_usm_metadata, m)?)?;
@@ -66,6 +66,14 @@ pub type UtfRow = std::collections::HashMap<String, UtfValue>;
66
66
  /// A UTF table
67
67
  pub type UtfTable = Vec<UtfRow>;
68
68
 
69
+ /// A stream extracted from a USM container.
70
+ #[derive(Debug, Clone, PartialEq, Eq)]
71
+ pub struct ExtractedUsmStream {
72
+ pub filename: String,
73
+ pub extension: String,
74
+ pub data: Vec<u8>,
75
+ }
76
+
69
77
  /// Read column data from a UTF table
70
78
  fn read_column_data<R: Read + Seek>(
71
79
  reader: &mut Reader<R>,
@@ -352,6 +360,57 @@ pub fn extract_usm<R: Read + Seek>(
352
360
  Ok(output_files)
353
361
  }
354
362
 
363
+ /// Extract USM video/audio streams into memory.
364
+ pub fn extract_usm_to_memory<R: Read + Seek>(
365
+ usm: R,
366
+ fallback_name: &[u8],
367
+ key: Option<u64>,
368
+ export_audio: bool,
369
+ ) -> Result<Vec<ExtractedUsmStream>, UsmError> {
370
+ let mut reader = Reader::new(usm);
371
+
372
+ let (vmask, amask) = key.map(get_mask).unzip();
373
+
374
+ let (filename, has_audio) = parse_usm_header(&mut reader, fallback_name)?;
375
+ let decoded_filename = decode_shift_jis(&filename);
376
+
377
+ let base_name = Path::new(&decoded_filename)
378
+ .file_stem()
379
+ .and_then(|s| s.to_str())
380
+ .unwrap_or(&decoded_filename)
381
+ .to_string();
382
+
383
+ let mut video = Vec::new();
384
+ let mut audio = if has_audio && export_audio {
385
+ Some(Vec::new())
386
+ } else {
387
+ None
388
+ };
389
+
390
+ extract_usm_chunks(
391
+ &mut reader,
392
+ &mut video,
393
+ audio.as_mut(),
394
+ vmask.as_ref(),
395
+ amask.as_ref(),
396
+ )?;
397
+
398
+ let mut streams = vec![ExtractedUsmStream {
399
+ filename: format!("{base_name}.m2v"),
400
+ extension: "m2v".to_string(),
401
+ data: video,
402
+ }];
403
+ if let Some(audio) = audio {
404
+ streams.push(ExtractedUsmStream {
405
+ filename: format!("{base_name}.adx"),
406
+ extension: "adx".to_string(),
407
+ data: audio,
408
+ });
409
+ }
410
+
411
+ Ok(streams)
412
+ }
413
+
355
414
  /// Parse USM header
356
415
  fn parse_usm_header<R: Read + Seek>(
357
416
  reader: &mut Reader<R>,
@@ -511,10 +570,10 @@ fn create_output_files(
511
570
  }
512
571
 
513
572
  /// Extract USM chunks
514
- fn extract_usm_chunks<R: Read + Seek>(
573
+ fn extract_usm_chunks<R: Read + Seek, V: Write, A: Write>(
515
574
  reader: &mut Reader<R>,
516
- video_file: &mut File,
517
- mut audio_file: Option<&mut File>,
575
+ video_file: &mut V,
576
+ mut audio_file: Option<&mut A>,
518
577
  vmask: Option<&Vec<Vec<u8>>>,
519
578
  amask: Option<&Vec<u8>>,
520
579
  ) -> Result<(), UsmError> {
@@ -558,13 +617,13 @@ fn extract_usm_chunks<R: Read + Seek>(
558
617
 
559
618
  /// Process a chunk
560
619
  #[allow(clippy::too_many_arguments)]
561
- fn process_chunk<R: Read + Seek>(
620
+ fn process_chunk<R: Read + Seek, V: Write, A: Write>(
562
621
  reader: &mut Reader<R>,
563
622
  sig: &[u8],
564
623
  read_data_len: usize,
565
624
  data_type: u8,
566
- video_file: &mut File,
567
- audio_file: &mut Option<&mut File>,
625
+ video_file: &mut V,
626
+ audio_file: &mut Option<&mut A>,
568
627
  vmask: Option<&Vec<Vec<u8>>>,
569
628
  amask: Option<&Vec<u8>>,
570
629
  ) -> Result<(), UsmError> {
@@ -8,7 +8,9 @@ mod extractor;
8
8
  mod metadata;
9
9
 
10
10
  pub use builder::{StreamInput, StreamType, UsmBuilder, UsmBuilderError};
11
- pub use extractor::{extract_usm, extract_usm_file, UsmError};
11
+ pub use extractor::{
12
+ extract_usm, extract_usm_file, extract_usm_to_memory, ExtractedUsmStream, UsmError,
13
+ };
12
14
  pub use metadata::{
13
15
  export_metadata_file, read_metadata, read_metadata_file, Metadata, MetadataSection,
14
16
  };
@@ -387,6 +387,23 @@ fn test_acb_from_memory() {
387
387
  assert_eq!(tracks.len(), 4, "Should extract 4 tracks from memory");
388
388
  }
389
389
 
390
+ /// Test extract_acb_to_memory returns track bytes without writing files
391
+ #[test]
392
+ fn test_acb_to_memory() {
393
+ let acb_path = Path::new("se_0126_01.acb");
394
+ if !acb_path.exists() {
395
+ eprintln!("Skipping test: se_0126_01.acb not found");
396
+ return;
397
+ }
398
+
399
+ let data = fs::read(acb_path).unwrap();
400
+ let tracks = cridecoder::extract_acb_to_memory(Cursor::new(data), None)
401
+ .expect("Should extract ACB tracks into memory");
402
+ assert_eq!(tracks.len(), 4, "Should extract 4 tracks into memory");
403
+ assert!(tracks.iter().all(|track| track.extension == "hca"));
404
+ assert!(tracks.iter().all(|track| !track.data.is_empty()));
405
+ }
406
+
390
407
  /// Test extract_usm with in-memory data
391
408
  #[test]
392
409
  fn test_usm_from_memory() {
@@ -404,6 +421,25 @@ fn test_usm_from_memory() {
404
421
  assert!(!files.is_empty(), "Should extract at least one file");
405
422
  }
406
423
 
424
+ /// Test extract_usm_to_memory returns stream bytes without writing files
425
+ #[test]
426
+ fn test_usm_to_memory() {
427
+ let usm_path = Path::new("0703.usm");
428
+ if !usm_path.exists() {
429
+ eprintln!("Skipping test: 0703.usm not found");
430
+ return;
431
+ }
432
+
433
+ let data = fs::read(usm_path).unwrap();
434
+ let streams =
435
+ cridecoder::usm::extract_usm_to_memory(Cursor::new(data), b"0703.usm", None, false)
436
+ .expect("Should extract USM streams into memory");
437
+ assert_eq!(streams.len(), 1);
438
+ assert_eq!(streams[0].filename, "0703.m2v");
439
+ assert_eq!(streams[0].extension, "m2v");
440
+ assert!(!streams[0].data.is_empty());
441
+ }
442
+
407
443
  // =============================================================================
408
444
  // Encoder Tests
409
445
  // =============================================================================
@@ -0,0 +1,332 @@
1
+ """
2
+ Python integration tests for cridecoder.
3
+
4
+ Tests encoding and decoding functionality:
5
+ - HCA encoding/decoding round-trip
6
+ - ACB/USM container building (structure verification)
7
+ - Real file extraction and HCA encoding
8
+ """
9
+
10
+ import os
11
+ import tempfile
12
+ import pytest
13
+
14
+ # Skip if cridecoder not available
15
+ pytest.importorskip("cridecoder")
16
+
17
+ import cridecoder
18
+
19
+
20
+ class TestHcaRoundtrip:
21
+ """Test HCA encoding and decoding round-trip."""
22
+
23
+ def test_encode_decode_roundtrip(self):
24
+ """Test encoding WAV to HCA and decoding back."""
25
+ # Create a simple sine wave WAV
26
+ wav_data = create_test_wav(sample_rate=44100, channels=2, duration_sec=0.5)
27
+
28
+ # Encode to HCA
29
+ hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=256000)
30
+
31
+ # Verify HCA magic
32
+ assert hca_data[:4] == b"HCA\x00", "Should have HCA magic"
33
+
34
+ # Decode back to WAV
35
+ decoded_wav = cridecoder.decode_hca_bytes(hca_data)
36
+
37
+ # Verify WAV magic
38
+ assert decoded_wav[:4] == b"RIFF", "Should have RIFF magic"
39
+ assert decoded_wav[8:12] == b"WAVE", "Should have WAVE format"
40
+
41
+ def test_encode_with_encryption(self):
42
+ """Test HCA encoding with encryption key."""
43
+ wav_data = create_test_wav(sample_rate=44100, channels=1, duration_sec=0.2)
44
+
45
+ # Encode with encryption
46
+ hca_data = cridecoder.encode_hca_bytes(
47
+ wav_data,
48
+ bitrate=128000,
49
+ encryption_key=0x1234567890ABCDEF
50
+ )
51
+
52
+ # Verify it's encrypted (header will be masked)
53
+ # Encrypted HCA has masked magic: 0xC8C3C100 instead of 0x48434100
54
+ assert hca_data[0] == 0xC8, "Should have masked HCA magic"
55
+
56
+ def test_encode_mono(self):
57
+ """Test encoding mono audio."""
58
+ wav_data = create_test_wav(sample_rate=22050, channels=1, duration_sec=0.3)
59
+ hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=128000)
60
+ assert hca_data[:4] == b"HCA\x00"
61
+
62
+ # Verify can decode
63
+ decoded = cridecoder.decode_hca_bytes(hca_data)
64
+ assert decoded[:4] == b"RIFF"
65
+
66
+ def test_encode_stereo(self):
67
+ """Test encoding stereo audio."""
68
+ wav_data = create_test_wav(sample_rate=48000, channels=2, duration_sec=0.3)
69
+ hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=320000)
70
+ assert hca_data[:4] == b"HCA\x00"
71
+
72
+ decoded = cridecoder.decode_hca_bytes(hca_data)
73
+ assert decoded[:4] == b"RIFF"
74
+
75
+
76
+ class TestContainerBuilding:
77
+ """Test container building (structure verification only)."""
78
+
79
+ def test_build_acb_structure(self):
80
+ """Test ACB building produces valid UTF structure."""
81
+ wav_data = create_test_wav(sample_rate=44100, channels=2, duration_sec=0.1)
82
+ hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=256000)
83
+
84
+ tracks = [("test_track", 0, hca_data)]
85
+ acb_data = cridecoder.build_acb_bytes(tracks)
86
+
87
+ # Verify ACB magic (@UTF)
88
+ assert acb_data[:4] == b"@UTF", "Should have UTF magic"
89
+ assert len(acb_data) > 100, "Should have substantial content"
90
+
91
+ def test_extract_acb_bytes(self):
92
+ """Test extracting ACB tracks without writing files."""
93
+ wav_data = create_test_wav(sample_rate=44100, channels=1, duration_sec=0.1)
94
+ hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=128000)
95
+ acb_data = cridecoder.build_acb_bytes([("memory_track", 0, hca_data)])
96
+
97
+ tracks = cridecoder.extract_acb_bytes(acb_data)
98
+
99
+ assert len(tracks) == 1
100
+ assert tracks[0]["name"] == "memory_track"
101
+ assert tracks[0]["extension"] == "hca"
102
+ assert tracks[0]["filename"] == "memory_track.hca"
103
+ assert tracks[0]["data"][:4] == b"HCA\x00"
104
+
105
+ def test_build_acb_multiple_tracks(self):
106
+ """Test ACB building with multiple tracks."""
107
+ tracks = []
108
+ for i in range(3):
109
+ wav_data = create_test_wav(sample_rate=44100, channels=1, duration_sec=0.1)
110
+ hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=128000)
111
+ tracks.append((f"track_{i}", i, hca_data))
112
+
113
+ acb_data = cridecoder.build_acb_bytes(tracks)
114
+ assert acb_data[:4] == b"@UTF"
115
+
116
+ def test_build_usm_structure(self):
117
+ """Test USM building produces valid CRID structure."""
118
+ # Create minimal M2V header
119
+ video_data = bytes([
120
+ 0x00, 0x00, 0x01, 0xB3, # sequence_header_code
121
+ 0x14, 0x00, 0xF0, 0x24, # picture size
122
+ 0xFF, 0xFF, 0xE0, 0x00, # bit rate
123
+ ])
124
+
125
+ usm_data = cridecoder.build_usm_bytes("test_video", video_data)
126
+
127
+ # Verify CRID magic
128
+ assert usm_data[:4] == b"CRID", "Should have CRID magic"
129
+ assert len(usm_data) > 100, "Should have substantial content"
130
+
131
+
132
+ class TestRealFileOperations:
133
+ """Test operations with real fixture files."""
134
+
135
+ @pytest.fixture
136
+ def acb_path(self):
137
+ """Path to test ACB file."""
138
+ path = "se_0126_01.acb"
139
+ if not os.path.exists(path):
140
+ pytest.skip("Test fixture se_0126_01.acb not found")
141
+ return path
142
+
143
+ @pytest.fixture
144
+ def usm_path(self):
145
+ """Path to test USM file."""
146
+ path = "0703.usm"
147
+ if not os.path.exists(path):
148
+ pytest.skip("Test fixture 0703.usm not found")
149
+ return path
150
+
151
+ def test_extract_acb(self, acb_path):
152
+ """Test extracting real ACB file."""
153
+ with tempfile.TemporaryDirectory() as tmpdir:
154
+ extracted = cridecoder.extract_acb(acb_path, tmpdir)
155
+ assert extracted is not None
156
+ assert len(extracted) > 0, "Should extract tracks"
157
+
158
+ # Verify extracted files exist
159
+ for path in extracted:
160
+ assert os.path.exists(path), f"Extracted file should exist: {path}"
161
+
162
+ def test_extract_usm(self, usm_path):
163
+ """Test extracting real USM file."""
164
+ with tempfile.TemporaryDirectory() as tmpdir:
165
+ extracted = cridecoder.extract_usm(usm_path, tmpdir)
166
+ assert len(extracted) > 0, "Should extract files"
167
+
168
+ # Verify M2V extracted
169
+ m2v_files = [p for p in extracted if p.endswith(".m2v")]
170
+ assert len(m2v_files) > 0, "Should extract M2V video"
171
+
172
+ def test_extract_usm_bytes(self, usm_path):
173
+ """Test extracting real USM streams without writing files."""
174
+ with open(usm_path, "rb") as f:
175
+ usm_data = f.read()
176
+
177
+ streams = cridecoder.extract_usm_bytes(usm_data, fallback_name=os.path.basename(usm_path))
178
+
179
+ assert len(streams) > 0
180
+ m2v_streams = [stream for stream in streams if stream["extension"] == "m2v"]
181
+ assert len(m2v_streams) > 0, "Should extract M2V video"
182
+ assert m2v_streams[0]["data"].startswith(b"\x00\x00\x01")
183
+
184
+ def test_hca_decode_and_reencode(self, acb_path):
185
+ """Extract HCA, decode to WAV, and re-encode to HCA."""
186
+ with tempfile.TemporaryDirectory() as tmpdir:
187
+ # Extract ACB
188
+ extracted = cridecoder.extract_acb(acb_path, tmpdir)
189
+ assert extracted is not None and len(extracted) > 0
190
+
191
+ # Decode first HCA to WAV
192
+ hca_path = extracted[0]
193
+ wav_path = os.path.join(tmpdir, "decoded.wav")
194
+ info = cridecoder.decode_hca(hca_path, wav_path)
195
+
196
+ assert info["sample_rate"] > 0
197
+ assert info["channels"] > 0
198
+
199
+ # Read WAV and re-encode
200
+ with open(wav_path, "rb") as f:
201
+ wav_data = f.read()
202
+
203
+ new_hca_data = cridecoder.encode_hca_bytes(
204
+ wav_data,
205
+ sample_rate=info["sample_rate"],
206
+ channels=info["channels"],
207
+ bitrate=256000
208
+ )
209
+
210
+ assert new_hca_data[:4] == b"HCA\x00", "Should produce valid HCA"
211
+
212
+ # Verify the new HCA can be decoded
213
+ decoded_again = cridecoder.decode_hca_bytes(new_hca_data)
214
+ assert decoded_again[:4] == b"RIFF", "Re-encoded HCA should decode"
215
+
216
+ def test_usm_extract_and_rebuild_video(self, usm_path):
217
+ """Extract M2V from USM and build new USM with it."""
218
+ with tempfile.TemporaryDirectory() as tmpdir:
219
+ # Extract
220
+ extracted = cridecoder.extract_usm(usm_path, tmpdir)
221
+
222
+ # Find M2V
223
+ m2v_path = None
224
+ for path in extracted:
225
+ if path.endswith(".m2v"):
226
+ m2v_path = path
227
+ break
228
+
229
+ assert m2v_path is not None, "Should have M2V"
230
+
231
+ # Read M2V and build new USM
232
+ with open(m2v_path, "rb") as f:
233
+ m2v_data = f.read()
234
+
235
+ rebuilt_path = os.path.join(tmpdir, "rebuilt.usm")
236
+ cridecoder.build_usm("rebuilt", m2v_data, rebuilt_path)
237
+
238
+ # Verify file created and has CRID header
239
+ assert os.path.exists(rebuilt_path)
240
+ with open(rebuilt_path, "rb") as f:
241
+ header = f.read(4)
242
+ assert header == b"CRID", "Rebuilt USM should have CRID magic"
243
+
244
+ def test_full_acb_pipeline(self, acb_path):
245
+ """Full pipeline: extract ACB -> decode HCA -> encode HCA -> build ACB."""
246
+ with tempfile.TemporaryDirectory() as tmpdir:
247
+ # 1. Extract original ACB
248
+ extract_dir = os.path.join(tmpdir, "extract")
249
+ os.makedirs(extract_dir)
250
+ extracted = cridecoder.extract_acb(acb_path, extract_dir)
251
+ assert extracted and len(extracted) > 0
252
+
253
+ # 2. Process each track: decode and re-encode
254
+ new_tracks = []
255
+ for i, hca_path in enumerate(extracted[:2]): # First 2 tracks
256
+ # Decode
257
+ wav_path = os.path.join(tmpdir, f"track{i}.wav")
258
+ info = cridecoder.decode_hca(hca_path, wav_path)
259
+
260
+ # Re-encode
261
+ with open(wav_path, "rb") as f:
262
+ wav_data = f.read()
263
+
264
+ new_hca = cridecoder.encode_hca_bytes(
265
+ wav_data,
266
+ sample_rate=info["sample_rate"],
267
+ channels=info["channels"],
268
+ bitrate=256000
269
+ )
270
+
271
+ new_tracks.append((f"track_{i}", i, new_hca))
272
+
273
+ # 3. Build new ACB
274
+ rebuilt_path = os.path.join(tmpdir, "rebuilt.acb")
275
+ cridecoder.build_acb(new_tracks, rebuilt_path)
276
+
277
+ # Verify ACB created
278
+ assert os.path.exists(rebuilt_path)
279
+ with open(rebuilt_path, "rb") as f:
280
+ header = f.read(4)
281
+ assert header == b"@UTF", "Rebuilt ACB should have UTF magic"
282
+
283
+ # Verify substantial size
284
+ size = os.path.getsize(rebuilt_path)
285
+ assert size > 1000, f"ACB should have substantial size, got {size}"
286
+
287
+
288
+ def create_test_wav(sample_rate: int, channels: int, duration_sec: float) -> bytes:
289
+ """Create a simple test WAV file with a sine wave."""
290
+ import struct
291
+ import math
292
+
293
+ num_samples = int(sample_rate * duration_sec)
294
+ bits_per_sample = 16
295
+ byte_rate = sample_rate * channels * bits_per_sample // 8
296
+ block_align = channels * bits_per_sample // 8
297
+ data_size = num_samples * channels * bits_per_sample // 8
298
+
299
+ # Generate sine wave samples
300
+ frequency = 440.0 # A4 note
301
+ samples = []
302
+ for i in range(num_samples):
303
+ t = i / sample_rate
304
+ value = int(32767 * 0.5 * math.sin(2 * math.pi * frequency * t))
305
+ for _ in range(channels):
306
+ samples.append(value)
307
+
308
+ # Build WAV header
309
+ wav = bytearray()
310
+ wav.extend(b"RIFF")
311
+ wav.extend(struct.pack("<I", 36 + data_size))
312
+ wav.extend(b"WAVE")
313
+ wav.extend(b"fmt ")
314
+ wav.extend(struct.pack("<I", 16)) # fmt chunk size
315
+ wav.extend(struct.pack("<H", 1)) # PCM format
316
+ wav.extend(struct.pack("<H", channels))
317
+ wav.extend(struct.pack("<I", sample_rate))
318
+ wav.extend(struct.pack("<I", byte_rate))
319
+ wav.extend(struct.pack("<H", block_align))
320
+ wav.extend(struct.pack("<H", bits_per_sample))
321
+ wav.extend(b"data")
322
+ wav.extend(struct.pack("<I", data_size))
323
+
324
+ # Add sample data
325
+ for sample in samples:
326
+ wav.extend(struct.pack("<h", sample))
327
+
328
+ return bytes(wav)
329
+
330
+
331
+ if __name__ == "__main__":
332
+ pytest.main([__file__, "-v"])
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