cridecoder 0.3.3__tar.gz → 0.3.5__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 (50) hide show
  1. {cridecoder-0.3.3 → cridecoder-0.3.5}/AGENTS.md +27 -6
  2. {cridecoder-0.3.3 → cridecoder-0.3.5}/Cargo.lock +3 -3
  3. {cridecoder-0.3.3 → cridecoder-0.3.5}/Cargo.toml +4 -4
  4. {cridecoder-0.3.3 → cridecoder-0.3.5}/PKG-INFO +81 -4
  5. {cridecoder-0.3.3 → cridecoder-0.3.5}/README.md +79 -2
  6. {cridecoder-0.3.3 → cridecoder-0.3.5}/cridecoder.pyi +16 -5
  7. cridecoder-0.3.5/examples/hca_attrib.rs +63 -0
  8. cridecoder-0.3.5/examples/hca_parallel.rs +60 -0
  9. {cridecoder-0.3.3 → cridecoder-0.3.5}/pyproject.toml +1 -1
  10. cridecoder-0.3.5/src/acb/decode.rs +211 -0
  11. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb.rs +3 -2
  12. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/bitreader.rs +73 -0
  13. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/decoder.rs +115 -19
  14. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/hca_file.rs +298 -22
  15. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/imdct.rs +53 -6
  16. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/tables.rs +4 -1
  17. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/lib.rs +3 -2
  18. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/python.rs +115 -50
  19. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/reader.rs +14 -0
  20. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm/extractor.rs +48 -35
  21. {cridecoder-0.3.3 → cridecoder-0.3.5}/tests/integration_tests.rs +98 -0
  22. cridecoder-0.3.3/src/acb/decode.rs +0 -112
  23. {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/copilot-instructions.md +0 -0
  24. {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/dependabot.yml +0 -0
  25. {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/workflows/ci.yml +0 -0
  26. {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/workflows/release-crate.yml +0 -0
  27. {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/workflows/release-python.yml +0 -0
  28. {cridecoder-0.3.3 → cridecoder-0.3.5}/.gitignore +0 -0
  29. {cridecoder-0.3.3 → cridecoder-0.3.5}/CLAUDE.md +0 -0
  30. {cridecoder-0.3.3 → cridecoder-0.3.5}/KNOWN_GAPS.md +0 -0
  31. {cridecoder-0.3.3 → cridecoder-0.3.5}/LICENSE +0 -0
  32. {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/debug_acb.rs +0 -0
  33. {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/profile_acb.rs +0 -0
  34. {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/profile_hca.rs +0 -0
  35. {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/test_acb.rs +0 -0
  36. {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/test_hca.rs +0 -0
  37. {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/test_usm.rs +0 -0
  38. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/afs.rs +0 -0
  39. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/builder.rs +0 -0
  40. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/consts.rs +0 -0
  41. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/extractor.rs +0 -0
  42. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/track.rs +0 -0
  43. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/utf.rs +0 -0
  44. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/ath.rs +0 -0
  45. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/cipher.rs +0 -0
  46. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/encoder.rs +0 -0
  47. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca.rs +0 -0
  48. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm/builder.rs +0 -0
  49. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm/metadata.rs +0 -0
  50. {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm.rs +0 -0
@@ -6,7 +6,7 @@
6
6
 
7
7
  ### Credits
8
8
 
9
- CRI format implementation is based on [vgmstream](https://github.com/vgmstream/vgmstream).
9
+ CRI format implementation is based on [vgmstream](https://github.com/vgmstream/vgmstream) and [PyCriCodecs](https://github.com/Youjose/PyCriCodecs/).
10
10
 
11
11
  ## Architecture
12
12
 
@@ -20,10 +20,13 @@ src/
20
20
  │ ├── utf.rs # CRI UTF table parser
21
21
  │ ├── afs.rs # AFS2 archive parser
22
22
  │ ├── track.rs # Track list extraction from ACB
23
- │ └── extractor.rs # ACB extraction logic
23
+ │ ├── extractor.rs # ACB extraction logic (disk + in-memory + de-duplicated)
24
+ │ ├── decode.rs # High-level ACB → WAV decoding (extract + HCA decode + subkey)
25
+ │ └── builder.rs # ACB/AWB/UTF table builders
24
26
  ├── hca.rs # HCA module root (re-exports submodules)
25
27
  ├── hca/
26
28
  │ ├── decoder.rs # Core HCA decoder (ClHca, header parsing, block decoding)
29
+ │ ├── encoder.rs # HCA encoder (PCM/WAV → HCA, optional encryption)
27
30
  │ ├── hca_file.rs # High-level HcaDecoder with streaming, WAV output, key testing
28
31
  │ ├── tables.rs # Lookup tables for HCA decoding
29
32
  │ ├── cipher.rs # HCA encryption/decryption cipher
@@ -33,16 +36,33 @@ src/
33
36
  ├── usm.rs # USM module root (re-exports submodules)
34
37
  ├── usm/
35
38
  │ ├── extractor.rs # USM extraction (video/audio stream demuxing)
39
+ │ ├── builder.rs # USM container builder
36
40
  │ └── metadata.rs # USM metadata reading and JSON export
37
41
  └── python.rs # Python bindings (behind "python" feature)
38
42
  ```
39
43
 
40
44
  ## Key Types
41
45
 
42
- - `extract_acb_from_file()` / `extract_acb()` — ACB extraction entry points
43
- - `HcaDecoder` — High-level HCA to WAV/PCM decoder
44
- - `extract_usm_file()` / `extract_usm()` — USM extraction entry points
45
- - `ClHca` — Low-level HCA decoder state machine
46
+ ACB extraction has three flavors, each with a disk and an in-memory variant:
47
+
48
+ - `extract_acb_from_file()` / `extract_acb()` — extract tracks to a directory (returns written paths)
49
+ - `extract_acb_tracks_from_file()` / `extract_acb_tracks()` — same, plus per-track metadata (`ExtractedTrackFile` with `name`, `cue_id`, `subkey`)
50
+ - `extract_acb_to_memory()` — extract waveform bytes per cue without touching disk (`ExtractedAcbTrack`)
51
+ - `extract_acb_unique_to_memory()` — extract each physical waveform **once**, mapping shared cues onto it (`UniqueWaveform` + `AcbCueRef`); ACBs often point several cues at one waveform
52
+
53
+ High-level decode (extract + HCA decode in one call, per-AWB AFS2 subkey applied automatically):
54
+
55
+ - `decode_acb_to_wav_from_file()` / `decode_acb_to_wav()` — write decoded WAVs to a directory
56
+ - `decode_acb_to_wav_to_memory()` — return decoded `DecodedAcbTrack`s in memory; encrypted (type-56) ACBs need only the global keycode
57
+
58
+ Other entry points:
59
+
60
+ - `AcbBuilder` / `TrackInput` — build ACB/AWB containers
61
+ - `HcaDecoder` — high-level HCA to WAV/PCM decoder; `HcaEncoder` — PCM/WAV to HCA
62
+ - `ClHca` — low-level HCA decoder state machine
63
+ - `extract_usm_file()` / `extract_usm()` — USM extraction (disk); `extract_usm_to_memory()` — in-memory; `UsmBuilder` — build USM
64
+
65
+ **Python bindings** (`src/python.rs`, behind the `python` feature) mirror this surface. Each disk function has a `*_bytes` in-memory counterpart that takes/returns `bytes` via `Cursor` (no `.to_vec()` copy). The `.pyi` stubs in `cridecoder.pyi` are the source of truth for the Python signatures and must stay in sync with the bindings.
46
66
 
47
67
  ## Building
48
68
 
@@ -89,6 +109,7 @@ Allowed types:
89
109
  | `[Fix]` | Bug fix |
90
110
  | `[Chore]` | Maintenance, refactoring, dependency or build changes |
91
111
  | `[Docs]` | Documentation-only changes |
112
+ | `[Perf]` | Performance improvement (no behavior change) |
92
113
 
93
114
  Rules:
94
115
 
@@ -28,7 +28,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
28
28
 
29
29
  [[package]]
30
30
  name = "cridecoder"
31
- version = "0.3.3"
31
+ version = "0.3.5"
32
32
  dependencies = [
33
33
  "byteorder",
34
34
  "encoding_rs",
@@ -277,9 +277,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
277
277
 
278
278
  [[package]]
279
279
  name = "rustc-hash"
280
- version = "2.1.2"
280
+ version = "2.1.3"
281
281
  source = "registry+https://github.com/rust-lang/crates.io-index"
282
- checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
282
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
283
283
 
284
284
  [[package]]
285
285
  name = "rustix"
@@ -1,12 +1,12 @@
1
1
  [package]
2
2
  name = "cridecoder"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  edition = "2021"
5
5
  description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
6
6
  license = "MIT"
7
- authors = ["Team-Haruki"]
8
- repository = "https://github.com/Team-Haruki/cridecoder"
9
- homepage = "https://github.com/Team-Haruki/cridecoder"
7
+ authors = ["Seiunx Dev"]
8
+ repository = "https://github.com/seiunx-dev/cridecoder"
9
+ homepage = "https://github.com/seiunx-dev/cridecoder"
10
10
  readme = "README.md"
11
11
  keywords = ["cri", "hca", "acb", "usm", "audio"]
12
12
  categories = ["multimedia::audio", "multimedia::video", "parser-implementations"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cridecoder
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -11,7 +11,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio
11
11
  Classifier: Topic :: Multimedia :: Video
12
12
  License-File: LICENSE
13
13
  Summary: CRI codec library for ACB/AWB, HCA audio, and USM video extraction
14
- Home-Page: https://github.com/Team-Haruki/cridecoder
14
+ Home-Page: https://github.com/seiunx-dev/cridecoder
15
15
  License: MIT
16
16
  Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
@@ -22,16 +22,25 @@ A pure Rust library for CRI Middleware codec encoding and decoding. Supports ACB
22
22
 
23
23
  ## Credits
24
24
 
25
- This project's CRI format implementation is based on and inspired by [vgmstream](https://github.com/vgmstream/vgmstream), a library for playing streamed audio from video games. Many thanks to the vgmstream contributors for their reverse-engineering work on CRI Middleware formats.
25
+ This project's CRI format implementation is based on and inspired by:
26
+
27
+ - [vgmstream](https://github.com/vgmstream/vgmstream) — a library for playing streamed audio from video games.
28
+ - [PyCriCodecs](https://github.com/Youjose/PyCriCodecs/) — a Python library for CRI Middleware codecs.
29
+
30
+ Many thanks to the vgmstream and PyCriCodecs contributors for their reverse-engineering work on CRI Middleware formats.
26
31
 
27
32
  ## Features
28
33
 
29
34
  - **ACB/AWB Extraction & Building** — Parse and create CRI ACB audio containers
35
+ - **ACB → WAV in one call** — Extract and decode an ACB straight to WAV; the per-AWB AFS2 subkey is applied automatically for encrypted (type-56) HCA
36
+ - **In-memory APIs** — Every extract/decode/build operation has a variant that works on `bytes`/`Vec<u8>` without touching disk
37
+ - **De-duplicated extraction** — Emit each physical waveform once even when several cues share it
30
38
  - **HCA Encoding & Decoding** — Encode PCM to HCA, decode HCA to PCM/WAV
31
39
  - **USM Extraction & Building** — Extract or create USM video containers
32
40
  - **USM Metadata** — Read and export USM metadata as structured JSON
33
41
  - **Key Testing** — Test decryption keys for encrypted HCA files
34
42
  - **Encryption Support** — Encode HCA with encryption keys
43
+ - **Python bindings** — `pip install cridecoder` exposes the full API via PyO3
35
44
  - **Pure Rust** — No C dependencies, works on any platform Rust supports
36
45
 
37
46
  ## Usage
@@ -40,7 +49,7 @@ Add to your `Cargo.toml`:
40
49
 
41
50
  ```toml
42
51
  [dependencies]
43
- cridecoder = "0.1"
52
+ cridecoder = "0.3"
44
53
  ```
45
54
 
46
55
  ### ACB Extraction
@@ -61,6 +70,46 @@ if let Some(tracks) = tracks {
61
70
  }
62
71
  ```
63
72
 
73
+ ### ACB → WAV (decode in one call)
74
+
75
+ Extract an ACB and decode its HCA tracks straight to WAV. The per-AWB AFS2
76
+ subkey is applied automatically, so encrypted (type-56) ACBs only need the
77
+ global keycode (pass `None` for unencrypted ACBs):
78
+
79
+ ```rust
80
+ use std::path::Path;
81
+ use cridecoder::decode_acb_to_wav_from_file;
82
+
83
+ let paths = decode_acb_to_wav_from_file(
84
+ Path::new("audio.acb"),
85
+ Path::new("output/"),
86
+ None, // Option<u64> global keycode
87
+ ).unwrap();
88
+
89
+ for path in &paths {
90
+ println!("Wrote: {path}");
91
+ }
92
+ ```
93
+
94
+ ### In-memory extraction (no disk I/O)
95
+
96
+ `extract_acb_to_memory` returns the waveform bytes per cue. For ACBs where
97
+ several cues share one physical waveform, `extract_acb_unique_to_memory`
98
+ emits each waveform once and lists the cues that reference it:
99
+
100
+ ```rust
101
+ use std::io::Cursor;
102
+ use cridecoder::extract_acb_unique_to_memory;
103
+
104
+ let acb = std::fs::read("audio.acb").unwrap();
105
+ let waveforms = extract_acb_unique_to_memory(Cursor::new(acb), None).unwrap();
106
+
107
+ for wf in &waveforms {
108
+ let cue_names: Vec<_> = wf.cues.iter().map(|c| c.name.as_str()).collect();
109
+ println!("{} bytes ({}) → {:?}", wf.data.len(), wf.extension, cue_names);
110
+ }
111
+ ```
112
+
64
113
  ### ACB Building
65
114
 
66
115
  ```rust
@@ -141,6 +190,34 @@ let mut output = Cursor::new(Vec::new());
141
190
  builder.build(&mut output).unwrap();
142
191
  ```
143
192
 
193
+ ## Python
194
+
195
+ The same API is available from Python via PyO3 bindings:
196
+
197
+ ```bash
198
+ pip install cridecoder
199
+ ```
200
+
201
+ ```python
202
+ import cridecoder
203
+
204
+ # Extract + decode an ACB to WAV bytes in one call (no disk I/O).
205
+ # Encrypted (type-56) ACBs only need the global key; pass key=None otherwise.
206
+ acb = open("audio.acb", "rb").read()
207
+ for track in cridecoder.decode_acb_to_wav_bytes(acb, key=None):
208
+ print(track["name"], track["extension"], len(track["data"]))
209
+
210
+ # De-duplicated extraction: each physical waveform once, with its cues.
211
+ for wf in cridecoder.extract_acb_unique_bytes(acb):
212
+ print(wf["extension"], [c["name"] for c in wf["cues"]])
213
+
214
+ # Decode a standalone HCA (key/subkey for encrypted files).
215
+ wav = cridecoder.decode_hca_bytes(open("audio.hca", "rb").read())
216
+ ```
217
+
218
+ Every disk-based function has an in-memory `*_bytes` counterpart that takes and
219
+ returns `bytes`. See `cridecoder.pyi` for the full typed signatures.
220
+
144
221
  ## Supported Formats
145
222
 
146
223
  | Format | Description | Operations |
@@ -4,16 +4,25 @@ A pure Rust library for CRI Middleware codec encoding and decoding. Supports ACB
4
4
 
5
5
  ## Credits
6
6
 
7
- This project's CRI format implementation is based on and inspired by [vgmstream](https://github.com/vgmstream/vgmstream), a library for playing streamed audio from video games. Many thanks to the vgmstream contributors for their reverse-engineering work on CRI Middleware formats.
7
+ This project's CRI format implementation is based on and inspired by:
8
+
9
+ - [vgmstream](https://github.com/vgmstream/vgmstream) — a library for playing streamed audio from video games.
10
+ - [PyCriCodecs](https://github.com/Youjose/PyCriCodecs/) — a Python library for CRI Middleware codecs.
11
+
12
+ Many thanks to the vgmstream and PyCriCodecs contributors for their reverse-engineering work on CRI Middleware formats.
8
13
 
9
14
  ## Features
10
15
 
11
16
  - **ACB/AWB Extraction & Building** — Parse and create CRI ACB audio containers
17
+ - **ACB → WAV in one call** — Extract and decode an ACB straight to WAV; the per-AWB AFS2 subkey is applied automatically for encrypted (type-56) HCA
18
+ - **In-memory APIs** — Every extract/decode/build operation has a variant that works on `bytes`/`Vec<u8>` without touching disk
19
+ - **De-duplicated extraction** — Emit each physical waveform once even when several cues share it
12
20
  - **HCA Encoding & Decoding** — Encode PCM to HCA, decode HCA to PCM/WAV
13
21
  - **USM Extraction & Building** — Extract or create USM video containers
14
22
  - **USM Metadata** — Read and export USM metadata as structured JSON
15
23
  - **Key Testing** — Test decryption keys for encrypted HCA files
16
24
  - **Encryption Support** — Encode HCA with encryption keys
25
+ - **Python bindings** — `pip install cridecoder` exposes the full API via PyO3
17
26
  - **Pure Rust** — No C dependencies, works on any platform Rust supports
18
27
 
19
28
  ## Usage
@@ -22,7 +31,7 @@ Add to your `Cargo.toml`:
22
31
 
23
32
  ```toml
24
33
  [dependencies]
25
- cridecoder = "0.1"
34
+ cridecoder = "0.3"
26
35
  ```
27
36
 
28
37
  ### ACB Extraction
@@ -43,6 +52,46 @@ if let Some(tracks) = tracks {
43
52
  }
44
53
  ```
45
54
 
55
+ ### ACB → WAV (decode in one call)
56
+
57
+ Extract an ACB and decode its HCA tracks straight to WAV. The per-AWB AFS2
58
+ subkey is applied automatically, so encrypted (type-56) ACBs only need the
59
+ global keycode (pass `None` for unencrypted ACBs):
60
+
61
+ ```rust
62
+ use std::path::Path;
63
+ use cridecoder::decode_acb_to_wav_from_file;
64
+
65
+ let paths = decode_acb_to_wav_from_file(
66
+ Path::new("audio.acb"),
67
+ Path::new("output/"),
68
+ None, // Option<u64> global keycode
69
+ ).unwrap();
70
+
71
+ for path in &paths {
72
+ println!("Wrote: {path}");
73
+ }
74
+ ```
75
+
76
+ ### In-memory extraction (no disk I/O)
77
+
78
+ `extract_acb_to_memory` returns the waveform bytes per cue. For ACBs where
79
+ several cues share one physical waveform, `extract_acb_unique_to_memory`
80
+ emits each waveform once and lists the cues that reference it:
81
+
82
+ ```rust
83
+ use std::io::Cursor;
84
+ use cridecoder::extract_acb_unique_to_memory;
85
+
86
+ let acb = std::fs::read("audio.acb").unwrap();
87
+ let waveforms = extract_acb_unique_to_memory(Cursor::new(acb), None).unwrap();
88
+
89
+ for wf in &waveforms {
90
+ let cue_names: Vec<_> = wf.cues.iter().map(|c| c.name.as_str()).collect();
91
+ println!("{} bytes ({}) → {:?}", wf.data.len(), wf.extension, cue_names);
92
+ }
93
+ ```
94
+
46
95
  ### ACB Building
47
96
 
48
97
  ```rust
@@ -123,6 +172,34 @@ let mut output = Cursor::new(Vec::new());
123
172
  builder.build(&mut output).unwrap();
124
173
  ```
125
174
 
175
+ ## Python
176
+
177
+ The same API is available from Python via PyO3 bindings:
178
+
179
+ ```bash
180
+ pip install cridecoder
181
+ ```
182
+
183
+ ```python
184
+ import cridecoder
185
+
186
+ # Extract + decode an ACB to WAV bytes in one call (no disk I/O).
187
+ # Encrypted (type-56) ACBs only need the global key; pass key=None otherwise.
188
+ acb = open("audio.acb", "rb").read()
189
+ for track in cridecoder.decode_acb_to_wav_bytes(acb, key=None):
190
+ print(track["name"], track["extension"], len(track["data"]))
191
+
192
+ # De-duplicated extraction: each physical waveform once, with its cues.
193
+ for wf in cridecoder.extract_acb_unique_bytes(acb):
194
+ print(wf["extension"], [c["name"] for c in wf["cues"]])
195
+
196
+ # Decode a standalone HCA (key/subkey for encrypted files).
197
+ wav = cridecoder.decode_hca_bytes(open("audio.hca", "rb").read())
198
+ ```
199
+
200
+ Every disk-based function has an in-memory `*_bytes` counterpart that takes and
201
+ returns `bytes`. See `cridecoder.pyi` for the full typed signatures.
202
+
126
203
  ## Supported Formats
127
204
 
128
205
  | Format | Description | Operations |
@@ -71,19 +71,26 @@ def extract_acb_unique_bytes(acb_data: bytes) -> list[dict[str, object]]:
71
71
  ...
72
72
 
73
73
  def decode_acb_to_wav(
74
- acb_path: str, output_dir: str, key: Optional[int] = ...
74
+ acb_path: str,
75
+ output_dir: str,
76
+ key: Optional[int] = ...,
77
+ threads: Optional[int] = ...,
75
78
  ) -> list[str]:
76
79
  """Extract an ACB and decode its HCA tracks straight to WAV files.
77
80
 
78
81
  The per-AWB AFS2 subkey is applied automatically, so encrypted (type-56)
79
82
  ACBs only need the global ``key`` (omit/``None`` for unencrypted ACBs).
80
- Non-HCA tracks are written verbatim with their original extension. Returns
83
+ Non-HCA tracks are written verbatim with their original extension.
84
+ ``threads`` selects multithreaded decoding (``None`` = serial, ``0`` = all
85
+ CPU cores, ``N`` = N threads); output is identical regardless. Returns
81
86
  the list of written file paths.
82
87
  """
83
88
  ...
84
89
 
85
90
  def decode_acb_to_wav_bytes(
86
- acb_data: bytes, key: Optional[int] = ...
91
+ acb_data: bytes,
92
+ key: Optional[int] = ...,
93
+ threads: Optional[int] = ...,
87
94
  ) -> list[dict[str, object]]:
88
95
  """In-memory counterpart of :func:`decode_acb_to_wav` (no disk I/O).
89
96
 
@@ -134,11 +141,14 @@ def decode_hca(
134
141
  wav_path: str,
135
142
  key: Optional[int] = ...,
136
143
  subkey: Optional[int] = ...,
144
+ threads: Optional[int] = ...,
137
145
  ) -> dict[str, int]:
138
146
  """Decode an HCA file to a WAV file.
139
147
 
140
148
  ``key``/``subkey`` apply the type-56 decryption keycode for encrypted HCA
141
- (no-op for unencrypted files). Returns a dict with ``sample_rate``,
149
+ (no-op for unencrypted files). ``threads`` selects multithreaded decoding:
150
+ ``None`` decodes serially, ``0`` uses all CPU cores, ``N`` uses N threads;
151
+ output is byte-identical regardless. Returns a dict with ``sample_rate``,
142
152
  ``channels``, ``block_count``, ``block_size``, ``encoder_delay`` and
143
153
  ``samples_per_block``.
144
154
  """
@@ -148,8 +158,9 @@ def decode_hca_bytes(
148
158
  hca_data: bytes,
149
159
  key: Optional[int] = ...,
150
160
  subkey: Optional[int] = ...,
161
+ threads: Optional[int] = ...,
151
162
  ) -> bytes:
152
- """Decode HCA bytes to WAV bytes in memory (``key``/``subkey`` as in :func:`decode_hca`)."""
163
+ """Decode HCA bytes to WAV bytes in memory (``key``/``subkey``/``threads`` as in :func:`decode_hca`)."""
153
164
  ...
154
165
 
155
166
  def encode_hca_bytes(
@@ -0,0 +1,63 @@
1
+ //! In-memory HCA decode benchmark: decodes every block from a preloaded buffer
2
+ //! (no file I/O in the loop) and prints an FNV hash of the PCM output so
3
+ //! optimizations can be checked for bit-exactness against a baseline.
4
+ use cridecoder::hca::ClHca;
5
+ use std::env;
6
+ use std::time::Instant;
7
+
8
+ fn main() {
9
+ let args: Vec<String> = env::args().collect();
10
+ let hca_file = args.get(1).map(|s| s.as_str()).unwrap_or("music_5031.hca");
11
+ let iters: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(400);
12
+
13
+ let data = std::fs::read(hca_file).unwrap();
14
+ let mut hca = Box::new(ClHca::new());
15
+ hca.decode_header(&data).unwrap();
16
+ let info = hca.get_info().unwrap();
17
+ println!(
18
+ "blocks={} block_size={} ch={} encrypted={}",
19
+ info.block_count, info.block_size, info.channel_count, info.encryption_enabled
20
+ );
21
+ println!(
22
+ "version={:#x} min_res={} max_res={} stereo_bands={} hfr_bands_per_group={} ms_stereo={}",
23
+ hca.version,
24
+ hca.min_resolution,
25
+ hca.max_resolution,
26
+ hca.stereo_band_count,
27
+ hca.bands_per_hfr_group,
28
+ hca.ms_stereo
29
+ );
30
+
31
+ let hs = info.header_size as usize;
32
+ let bs = info.block_size as usize;
33
+ let n = info.block_count as usize;
34
+ let mut frame = vec![0u8; bs];
35
+ let mut pcm = vec![0i16; info.samples_per_block * info.channel_count as usize];
36
+
37
+ // Correctness hash over one full decode.
38
+ let mut hash: u64 = 0xcbf29ce484222325;
39
+ for b in 0..n {
40
+ frame.copy_from_slice(&data[hs + b * bs..hs + (b + 1) * bs]);
41
+ hca.decode_block(&mut frame).unwrap();
42
+ hca.read_samples_16(&mut pcm);
43
+ for &s in &pcm {
44
+ hash = (hash ^ (s as u16 as u64)).wrapping_mul(0x100000001b3);
45
+ }
46
+ }
47
+ println!("pcm fnv hash: {:016x}", hash);
48
+
49
+ let start = Instant::now();
50
+ for _ in 0..iters {
51
+ for b in 0..n {
52
+ frame.copy_from_slice(&data[hs + b * bs..hs + (b + 1) * bs]);
53
+ hca.decode_block(&mut frame).unwrap();
54
+ hca.read_samples_16(&mut pcm);
55
+ std::hint::black_box(&pcm);
56
+ }
57
+ }
58
+ let el = start.elapsed();
59
+ println!(
60
+ "in-memory decode: {:.3} ms/decode",
61
+ el.as_secs_f64() * 1000.0 / iters as f64
62
+ );
63
+ }
@@ -0,0 +1,60 @@
1
+ //! Benchmark and verify decode_to_wav_parallel against the serial decoder.
2
+ use cridecoder::HcaDecoder;
3
+ use std::env;
4
+ use std::time::Instant;
5
+
6
+ fn main() {
7
+ let args: Vec<String> = env::args().collect();
8
+ let hca_file = args.get(1).map(|s| s.as_str()).unwrap_or("music_5031.hca");
9
+ let iters: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(50);
10
+ let threads: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or_else(|| {
11
+ std::thread::available_parallelism()
12
+ .map(|n| n.get())
13
+ .unwrap_or(1)
14
+ });
15
+
16
+ // Correctness: byte-identical WAV output.
17
+ let mut serial_out = Vec::new();
18
+ let mut parallel_out = Vec::new();
19
+ {
20
+ let mut d = HcaDecoder::from_file(hca_file).unwrap();
21
+ d.decode_to_wav(&mut serial_out).unwrap();
22
+ let mut d = HcaDecoder::from_file(hca_file).unwrap();
23
+ d.decode_to_wav_parallel(&mut parallel_out, threads)
24
+ .unwrap();
25
+ }
26
+ assert_eq!(serial_out.len(), parallel_out.len(), "length mismatch");
27
+ assert!(serial_out == parallel_out, "WAV bytes differ");
28
+ println!(
29
+ "byte-identical: yes ({} bytes, {} threads)",
30
+ serial_out.len(),
31
+ threads
32
+ );
33
+
34
+ let start = Instant::now();
35
+ for _ in 0..iters {
36
+ let mut d = HcaDecoder::from_file(hca_file).unwrap();
37
+ let mut sink = std::io::sink();
38
+ d.decode_to_wav(&mut sink).unwrap();
39
+ }
40
+ let serial = start.elapsed();
41
+
42
+ let start = Instant::now();
43
+ for _ in 0..iters {
44
+ let mut d = HcaDecoder::from_file(hca_file).unwrap();
45
+ let mut sink = std::io::sink();
46
+ d.decode_to_wav_parallel(&mut sink, threads).unwrap();
47
+ }
48
+ let parallel = start.elapsed();
49
+
50
+ println!(
51
+ "serial: {:.3} ms/decode",
52
+ serial.as_secs_f64() * 1000.0 / iters as f64
53
+ );
54
+ println!(
55
+ "parallel: {:.3} ms/decode ({} threads, {:.2}x)",
56
+ parallel.as_secs_f64() * 1000.0 / iters as f64,
57
+ threads,
58
+ serial.as_secs_f64() / parallel.as_secs_f64()
59
+ );
60
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cridecoder"
7
- version = "0.3.3"
7
+ version = "0.3.5"
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" }