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.
- {cridecoder-0.3.3 → cridecoder-0.3.5}/AGENTS.md +27 -6
- {cridecoder-0.3.3 → cridecoder-0.3.5}/Cargo.lock +3 -3
- {cridecoder-0.3.3 → cridecoder-0.3.5}/Cargo.toml +4 -4
- {cridecoder-0.3.3 → cridecoder-0.3.5}/PKG-INFO +81 -4
- {cridecoder-0.3.3 → cridecoder-0.3.5}/README.md +79 -2
- {cridecoder-0.3.3 → cridecoder-0.3.5}/cridecoder.pyi +16 -5
- cridecoder-0.3.5/examples/hca_attrib.rs +63 -0
- cridecoder-0.3.5/examples/hca_parallel.rs +60 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/pyproject.toml +1 -1
- cridecoder-0.3.5/src/acb/decode.rs +211 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb.rs +3 -2
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/bitreader.rs +73 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/decoder.rs +115 -19
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/hca_file.rs +298 -22
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/imdct.rs +53 -6
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/tables.rs +4 -1
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/lib.rs +3 -2
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/python.rs +115 -50
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/reader.rs +14 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm/extractor.rs +48 -35
- {cridecoder-0.3.3 → cridecoder-0.3.5}/tests/integration_tests.rs +98 -0
- cridecoder-0.3.3/src/acb/decode.rs +0 -112
- {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/copilot-instructions.md +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/dependabot.yml +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/workflows/ci.yml +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/workflows/release-crate.yml +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/.github/workflows/release-python.yml +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/.gitignore +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/CLAUDE.md +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/KNOWN_GAPS.md +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/LICENSE +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/debug_acb.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/profile_acb.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/profile_hca.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/test_acb.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/test_hca.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/examples/test_usm.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/afs.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/builder.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/consts.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/extractor.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/track.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/acb/utf.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/ath.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/cipher.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca/encoder.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/hca.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm/builder.rs +0 -0
- {cridecoder-0.3.3 → cridecoder-0.3.5}/src/usm/metadata.rs +0 -0
- {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
|
-
│
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
- `
|
|
45
|
-
- `
|
|
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.
|
|
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.
|
|
280
|
+
version = "2.1.3"
|
|
281
281
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
282
|
-
checksum = "
|
|
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
|
+
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 = ["
|
|
8
|
-
repository = "https://github.com/
|
|
9
|
-
homepage = "https://github.com/
|
|
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
|
+
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/
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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,
|
|
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.
|
|
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,
|
|
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).
|
|
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
|
+
}
|