cridecoder 0.3.1__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.1 → cridecoder-0.3.2}/Cargo.lock +1 -1
  2. {cridecoder-0.3.1 → cridecoder-0.3.2}/Cargo.toml +1 -1
  3. {cridecoder-0.3.1 → cridecoder-0.3.2}/PKG-INFO +1 -1
  4. {cridecoder-0.3.1 → cridecoder-0.3.2}/cridecoder.pyi +37 -0
  5. {cridecoder-0.3.1 → cridecoder-0.3.2}/pyproject.toml +1 -1
  6. cridecoder-0.3.2/src/acb/decode.rs +107 -0
  7. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/extractor.rs +3 -0
  8. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb.rs +5 -0
  9. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/lib.rs +4 -0
  10. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/python.rs +104 -24
  11. {cridecoder-0.3.1 → cridecoder-0.3.2}/tests/integration_tests.rs +30 -0
  12. {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/copilot-instructions.md +0 -0
  13. {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/dependabot.yml +0 -0
  14. {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/workflows/ci.yml +0 -0
  15. {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/workflows/release-crate.yml +0 -0
  16. {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/workflows/release-python.yml +0 -0
  17. {cridecoder-0.3.1 → cridecoder-0.3.2}/.gitignore +0 -0
  18. {cridecoder-0.3.1 → cridecoder-0.3.2}/AGENTS.md +0 -0
  19. {cridecoder-0.3.1 → cridecoder-0.3.2}/CLAUDE.md +0 -0
  20. {cridecoder-0.3.1 → cridecoder-0.3.2}/KNOWN_GAPS.md +0 -0
  21. {cridecoder-0.3.1 → cridecoder-0.3.2}/LICENSE +0 -0
  22. {cridecoder-0.3.1 → cridecoder-0.3.2}/README.md +0 -0
  23. {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/debug_acb.rs +0 -0
  24. {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/profile_hca.rs +0 -0
  25. {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/test_acb.rs +0 -0
  26. {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/test_hca.rs +0 -0
  27. {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/test_usm.rs +0 -0
  28. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/afs.rs +0 -0
  29. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/builder.rs +0 -0
  30. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/consts.rs +0 -0
  31. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/track.rs +0 -0
  32. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/utf.rs +0 -0
  33. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/ath.rs +0 -0
  34. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/bitreader.rs +0 -0
  35. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/cipher.rs +0 -0
  36. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/decoder.rs +0 -0
  37. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/encoder.rs +0 -0
  38. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/hca_file.rs +0 -0
  39. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/imdct.rs +0 -0
  40. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/tables.rs +0 -0
  41. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca.rs +0 -0
  42. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/reader.rs +0 -0
  43. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm/builder.rs +0 -0
  44. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm/extractor.rs +0 -0
  45. {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm/metadata.rs +0 -0
  46. {cridecoder-0.3.1 → 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.1"
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.1"
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.1
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
@@ -10,7 +10,9 @@ from typing import Optional
10
10
  __all__ = [
11
11
  "extract_acb",
12
12
  "extract_acb_tracks",
13
+ "extract_acb_bytes",
13
14
  "decode_acb_to_wav",
15
+ "decode_acb_to_wav_bytes",
14
16
  "build_acb",
15
17
  "build_acb_bytes",
16
18
  "build_music_acb_bytes",
@@ -19,6 +21,7 @@ __all__ = [
19
21
  "encode_hca",
20
22
  "encode_hca_bytes",
21
23
  "extract_usm",
24
+ "extract_usm_bytes",
22
25
  "build_usm",
23
26
  "build_usm_bytes",
24
27
  "read_usm_metadata",
@@ -45,6 +48,16 @@ def extract_acb_tracks(
45
48
  """
46
49
  ...
47
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
+
48
61
  def decode_acb_to_wav(
49
62
  acb_path: str, output_dir: str, key: Optional[int] = ...
50
63
  ) -> list[str]:
@@ -57,6 +70,17 @@ def decode_acb_to_wav(
57
70
  """
58
71
  ...
59
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
+
60
84
  def build_acb(tracks: list[tuple[str, int, bytes]], output_path: str) -> None:
61
85
  """Build an ACB file from ``(name, cue_id, hca_data)`` tuples, writing to disk."""
62
86
  ...
@@ -154,6 +178,19 @@ def extract_usm(
154
178
  """
155
179
  ...
156
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
+
157
194
  def build_usm(
158
195
  name: str,
159
196
  video_data: bytes,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cridecoder"
7
- version = "0.3.1"
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,
@@ -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,10 @@ pub use builder::{
12
13
  AcbBuilder, AfsArchiveBuilder, BuilderError, ColumnDef, TrackInput, UtfTableBuilder,
13
14
  };
14
15
  pub use consts::*;
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
+ };
15
20
  pub use extractor::{
16
21
  extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
17
22
  extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
@@ -14,6 +14,10 @@ pub mod usm;
14
14
  mod python;
15
15
 
16
16
  // ACB/AWB exports
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
+ };
17
21
  pub use acb::{
18
22
  extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
19
23
  extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
@@ -101,35 +101,76 @@ fn decode_acb_to_wav(acb_path: &str, output_dir: &str, key: Option<u64>) -> PyRe
101
101
  fs::create_dir_all(out_dir)
102
102
  .map_err(|e| PyRuntimeError::new_err(format!("Failed to create output dir: {}", e)))?;
103
103
 
104
- let file = fs::File::open(acb_path)
105
- .map_err(|e| PyRuntimeError::new_err(format!("Failed to open ACB: {}", e)))?;
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
+ }
106
107
 
107
- let tracks = acb::extract_acb_to_memory(file, Some(Path::new(acb_path)))
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)
108
126
  .map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))?;
109
127
 
110
- let mut outputs = Vec::with_capacity(tracks.len());
128
+ let mut out = Vec::with_capacity(tracks.len());
111
129
  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
- }
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);
131
172
  }
132
- Ok(outputs)
173
+ Ok(out)
133
174
  }
134
175
 
135
176
  /// Build an ACB file from track data.
@@ -525,6 +566,42 @@ fn extract_usm(
525
566
  .collect())
526
567
  }
527
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
+
528
605
  /// Build a USM file from video data.
529
606
  ///
530
607
  /// Args:
@@ -611,7 +688,9 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
611
688
  // ACB functions
612
689
  m.add_function(wrap_pyfunction!(extract_acb, m)?)?;
613
690
  m.add_function(wrap_pyfunction!(extract_acb_tracks, m)?)?;
691
+ m.add_function(wrap_pyfunction!(extract_acb_bytes, m)?)?;
614
692
  m.add_function(wrap_pyfunction!(decode_acb_to_wav, m)?)?;
693
+ m.add_function(wrap_pyfunction!(decode_acb_to_wav_bytes, m)?)?;
615
694
  m.add_function(wrap_pyfunction!(build_acb, m)?)?;
616
695
  m.add_function(wrap_pyfunction!(build_acb_bytes, m)?)?;
617
696
  m.add_function(wrap_pyfunction!(build_music_acb_bytes, m)?)?;
@@ -624,6 +703,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
624
703
 
625
704
  // USM functions
626
705
  m.add_function(wrap_pyfunction!(extract_usm, m)?)?;
706
+ m.add_function(wrap_pyfunction!(extract_usm_bytes, m)?)?;
627
707
  m.add_function(wrap_pyfunction!(build_usm, m)?)?;
628
708
  m.add_function(wrap_pyfunction!(build_usm_bytes, m)?)?;
629
709
  m.add_function(wrap_pyfunction!(read_usm_metadata, m)?)?;
@@ -596,6 +596,36 @@ fn test_acb_extract_tracks_metadata() {
596
596
  assert_eq!(&data[0..4], b"HCA\x00");
597
597
  }
598
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
+
599
629
  /// Test ACB builder keeps Waveform AWB ids aligned with non-zero cue ids
600
630
  #[test]
601
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