cridecoder 0.2.3__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/workflows/ci.yml +1 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/workflows/release-crate.yml +1 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/workflows/release-python.yml +4 -4
- {cridecoder-0.2.3 → cridecoder-0.3.1}/.gitignore +7 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/Cargo.lock +18 -12
- {cridecoder-0.2.3 → cridecoder-0.3.1}/Cargo.toml +3 -2
- cridecoder-0.3.1/KNOWN_GAPS.md +84 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/PKG-INFO +1 -1
- cridecoder-0.3.1/cridecoder.pyi +176 -0
- cridecoder-0.3.1/examples/profile_hca.rs +48 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/pyproject.toml +1 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/afs.rs +7 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/consts.rs +4 -2
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/extractor.rs +111 -19
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/track.rs +48 -47
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/utf.rs +22 -12
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb.rs +4 -1
- cridecoder-0.3.1/src/hca/bitreader.rs +358 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/decoder.rs +151 -33
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/encoder.rs +52 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/hca_file.rs +91 -9
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/imdct.rs +356 -94
- cridecoder-0.3.1/src/hca/tables.rs +122 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/lib.rs +4 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/python.rs +109 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/reader.rs +18 -1
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm/builder.rs +4 -2
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm/extractor.rs +360 -54
- {cridecoder-0.2.3 → cridecoder-0.3.1}/tests/integration_tests.rs +37 -0
- cridecoder-0.2.3/src/hca/bitreader.rs +0 -230
- cridecoder-0.2.3/src/hca/tables.rs +0 -154
- {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/copilot-instructions.md +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/dependabot.yml +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/AGENTS.md +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/CLAUDE.md +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/LICENSE +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/README.md +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/debug_acb.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/test_acb.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/test_hca.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/test_usm.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/builder.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/ath.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/cipher.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm/metadata.rs +0 -0
- {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm.rs +0 -0
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
matrix:
|
|
23
23
|
target: [x86_64, aarch64]
|
|
24
24
|
steps:
|
|
25
|
-
- uses: actions/checkout@
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
26
|
- name: Set version from tag
|
|
27
27
|
if: github.event_name == 'release'
|
|
28
28
|
run: |
|
|
@@ -51,7 +51,7 @@ jobs:
|
|
|
51
51
|
matrix:
|
|
52
52
|
target: [x86_64, aarch64]
|
|
53
53
|
steps:
|
|
54
|
-
- uses: actions/checkout@
|
|
54
|
+
- uses: actions/checkout@v7
|
|
55
55
|
- name: Set version from tag
|
|
56
56
|
if: github.event_name == 'release'
|
|
57
57
|
run: |
|
|
@@ -87,7 +87,7 @@ jobs:
|
|
|
87
87
|
windows:
|
|
88
88
|
runs-on: windows-latest
|
|
89
89
|
steps:
|
|
90
|
-
- uses: actions/checkout@
|
|
90
|
+
- uses: actions/checkout@v7
|
|
91
91
|
- name: Set version from tag
|
|
92
92
|
if: github.event_name == 'release'
|
|
93
93
|
shell: bash
|
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
sdist:
|
|
125
125
|
runs-on: ubuntu-latest
|
|
126
126
|
steps:
|
|
127
|
-
- uses: actions/checkout@
|
|
127
|
+
- uses: actions/checkout@v7
|
|
128
128
|
- name: Set version from tag
|
|
129
129
|
if: github.event_name == 'release'
|
|
130
130
|
run: |
|
|
@@ -28,12 +28,13 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
28
28
|
|
|
29
29
|
[[package]]
|
|
30
30
|
name = "cridecoder"
|
|
31
|
-
version = "0.
|
|
31
|
+
version = "0.3.1"
|
|
32
32
|
dependencies = [
|
|
33
33
|
"byteorder",
|
|
34
34
|
"encoding_rs",
|
|
35
35
|
"hex",
|
|
36
36
|
"pyo3",
|
|
37
|
+
"rustc-hash",
|
|
37
38
|
"serde",
|
|
38
39
|
"serde_json",
|
|
39
40
|
"tempfile",
|
|
@@ -204,9 +205,9 @@ dependencies = [
|
|
|
204
205
|
|
|
205
206
|
[[package]]
|
|
206
207
|
name = "pyo3"
|
|
207
|
-
version = "0.
|
|
208
|
+
version = "0.29.0"
|
|
208
209
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
209
|
-
checksum = "
|
|
210
|
+
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
|
210
211
|
dependencies = [
|
|
211
212
|
"libc",
|
|
212
213
|
"once_cell",
|
|
@@ -218,18 +219,18 @@ dependencies = [
|
|
|
218
219
|
|
|
219
220
|
[[package]]
|
|
220
221
|
name = "pyo3-build-config"
|
|
221
|
-
version = "0.
|
|
222
|
+
version = "0.29.0"
|
|
222
223
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
223
|
-
checksum = "
|
|
224
|
+
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
|
224
225
|
dependencies = [
|
|
225
226
|
"target-lexicon",
|
|
226
227
|
]
|
|
227
228
|
|
|
228
229
|
[[package]]
|
|
229
230
|
name = "pyo3-ffi"
|
|
230
|
-
version = "0.
|
|
231
|
+
version = "0.29.0"
|
|
231
232
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
232
|
-
checksum = "
|
|
233
|
+
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
|
233
234
|
dependencies = [
|
|
234
235
|
"libc",
|
|
235
236
|
"pyo3-build-config",
|
|
@@ -237,9 +238,9 @@ dependencies = [
|
|
|
237
238
|
|
|
238
239
|
[[package]]
|
|
239
240
|
name = "pyo3-macros"
|
|
240
|
-
version = "0.
|
|
241
|
+
version = "0.29.0"
|
|
241
242
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
242
|
-
checksum = "
|
|
243
|
+
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
|
243
244
|
dependencies = [
|
|
244
245
|
"proc-macro2",
|
|
245
246
|
"pyo3-macros-backend",
|
|
@@ -249,13 +250,12 @@ dependencies = [
|
|
|
249
250
|
|
|
250
251
|
[[package]]
|
|
251
252
|
name = "pyo3-macros-backend"
|
|
252
|
-
version = "0.
|
|
253
|
+
version = "0.29.0"
|
|
253
254
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
254
|
-
checksum = "
|
|
255
|
+
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
|
255
256
|
dependencies = [
|
|
256
257
|
"heck",
|
|
257
258
|
"proc-macro2",
|
|
258
|
-
"pyo3-build-config",
|
|
259
259
|
"quote",
|
|
260
260
|
"syn",
|
|
261
261
|
]
|
|
@@ -275,6 +275,12 @@ version = "6.0.0"
|
|
|
275
275
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
276
276
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
277
277
|
|
|
278
|
+
[[package]]
|
|
279
|
+
name = "rustc-hash"
|
|
280
|
+
version = "2.1.2"
|
|
281
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
282
|
+
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
|
283
|
+
|
|
278
284
|
[[package]]
|
|
279
285
|
name = "rustix"
|
|
280
286
|
version = "1.1.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "cridecoder"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
|
|
6
6
|
license = "MIT"
|
|
@@ -33,7 +33,8 @@ encoding_rs = "0.8"
|
|
|
33
33
|
serde = { version = "1", features = ["derive"] }
|
|
34
34
|
serde_json = "1"
|
|
35
35
|
hex = "0.4"
|
|
36
|
-
|
|
36
|
+
rustc-hash = "2"
|
|
37
|
+
pyo3 = { version = "0.29", features = ["extension-module"], optional = true }
|
|
37
38
|
|
|
38
39
|
[dev-dependencies]
|
|
39
40
|
tempfile = "3"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Known gaps / deferred items
|
|
2
|
+
|
|
3
|
+
These are confirmed divergences from CRI/vgmstream behavior that are **intentionally
|
|
4
|
+
not implemented yet**. All of them are **non-essential for the common decode/extract
|
|
5
|
+
path** (and specifically for Project Sekai assets, which use unencrypted HCA-MX in
|
|
6
|
+
ACB/AWB and VP9 USM). They are deferred because they are either pure performance,
|
|
7
|
+
unreachable without new API, or deep restructures with **no test fixtures**, where a
|
|
8
|
+
blind change to working code carries more regression risk than value.
|
|
9
|
+
|
|
10
|
+
Verified against vgmstream `3f860bef` (`src/coding/libs/clhca.c`, `src/meta/acb.c`,
|
|
11
|
+
`src/meta/awb.c`) and PyCriCodecs `usm.py` / `acb.py`.
|
|
12
|
+
|
|
13
|
+
## ACB
|
|
14
|
+
|
|
15
|
+
### Command / Synth traversal breadth — `src/acb/track.rs`
|
|
16
|
+
`extract_tracks_from_event` / `extract_track_from_command` handle only the common path:
|
|
17
|
+
command `0x07d0` (noteOn 2000) → `u1 == 2` (Synth) → **first** ReferenceItem →
|
|
18
|
+
item-type 1 (Waveform). vgmstream (`acb.c:481-600`) also accepts noteOn `2003`, follows
|
|
19
|
+
`tlv_type == 3` (Sequence), iterates **all** ReferenceItems, and recurses on item-types
|
|
20
|
+
2 (Synth) and 3 (Sequence).
|
|
21
|
+
- **Impact:** ACBs whose cues fan out to multiple/ nested waveforms extract only the
|
|
22
|
+
first. pjsk cues are single `noteOn → Synth → one Waveform`, so unaffected.
|
|
23
|
+
- **Why deferred:** rewriting the core recursive traversal can silently change output
|
|
24
|
+
(extra/duplicate/mis-named tracks) on real multi-Synth/Sequence ACBs, and the test
|
|
25
|
+
ACBs are builder-made single-cue — no fixture would catch a semantic regression.
|
|
26
|
+
Needs a real multi-reference ACB fixture first.
|
|
27
|
+
|
|
28
|
+
### Type-8 BlockSequence — `src/acb/track.rs`
|
|
29
|
+
Cue `ReferenceType == 8` is currently routed through the Sequence path. vgmstream uses a
|
|
30
|
+
dedicated `load_acb_blocksequence` → `load_acb_block` over the Block/BlockSequence tables
|
|
31
|
+
(`acb.c:826-968`), which cridecoder does not parse. (Unknown ref types are now skipped
|
|
32
|
+
rather than erroring; see commit `e958c9f`.)
|
|
33
|
+
- **Impact:** none for pjsk (cues are type 3).
|
|
34
|
+
- **Why deferred:** requires new Block/BlockSequence table parsing; implementable from
|
|
35
|
+
vgmstream but unverifiable without a type-8 fixture.
|
|
36
|
+
|
|
37
|
+
### Legacy `Id` waveform field — `src/acb/track.rs`
|
|
38
|
+
`extract_track_from_command` selects the waveform id from `StreamAwbId`/`MemoryAwbId`
|
|
39
|
+
keyed on the `Streaming` flag. vgmstream (`acb.c:335-363`) first tries the legacy single
|
|
40
|
+
`Id` column, falling back by `acb->is_memory`.
|
|
41
|
+
- **Impact:** old ACBs that carry only `Id` (no split ids) resolve to id 0. pjsk uses
|
|
42
|
+
split ids, so unaffected.
|
|
43
|
+
- **Why deferred:** `get_int_field` returns 0 for an absent column (can't distinguish
|
|
44
|
+
absent from 0); doing this correctly needs an optional column reader. Additive and
|
|
45
|
+
low-risk, but the legacy path has no fixture.
|
|
46
|
+
|
|
47
|
+
## USM
|
|
48
|
+
|
|
49
|
+
### Per-channel (`chno`) multi-track — `src/usm/extractor.rs`
|
|
50
|
+
The `chno` byte (chunk offset 0x0C) is read but not used to separate streams; all `@SFA`
|
|
51
|
+
chunks merge into one audio output. vgmstream/PyCriCodecs key outputs by
|
|
52
|
+
`<signature>_<chno>`.
|
|
53
|
+
- **Impact:** multi-track audio interleaves into one corrupt file. pjsk USMs are single
|
|
54
|
+
video + single audio (`chno == 0`), so unaffected.
|
|
55
|
+
- **Why deferred:** requires restructuring the output path to per-`chno` sinks; the
|
|
56
|
+
single-track path is testable but multi-track has no fixture.
|
|
57
|
+
|
|
58
|
+
## HCA encoder (PCM → HCA; not used for decoding/extraction)
|
|
59
|
+
|
|
60
|
+
### Loop frame alignment (P17) — `src/hca/encoder.rs`
|
|
61
|
+
The encoder does not apply CRI's loop pipeline (delay bump, 2048-byte loop-frame
|
|
62
|
+
alignment, post-loop tail) from `clhca.c`, so a loop encoded by cridecoder would loop at
|
|
63
|
+
the wrong sample in clHCA players.
|
|
64
|
+
- **Status:** there is currently **no builder/encoder API that exposes loop config**, so
|
|
65
|
+
this code path is unreachable — implementing it would be dead, untestable code.
|
|
66
|
+
`HcaEncoderConfig.loop_start/loop_end` (if/when added) are therefore **not
|
|
67
|
+
CRI-accurate**. Documented here rather than implemented.
|
|
68
|
+
|
|
69
|
+
### MDCT is O(N²) — `src/hca/encoder.rs`
|
|
70
|
+
`mdct_transform` computes the forward DCT-IV as a naive double loop (`cos()` per (n,k)
|
|
71
|
+
pair, ~16384 calls/subframe). It is mathematically correct (round-trips), just slow. The
|
|
72
|
+
**decoder** side (`imdct.rs`) already uses a factorized DCT-IV with precomputed
|
|
73
|
+
`SIN_TABLES`/`COS_TABLES`; the encoder could reuse the same approach (O(N log N)).
|
|
74
|
+
- **Impact:** encoding speed only. Decoding (the pjsk path) is unaffected.
|
|
75
|
+
- **Why deferred:** bit-sensitive rewrite; the existing round-trip tests only assert
|
|
76
|
+
non-empty output, so a proper encode→decode→compare-to-input fidelity test should be
|
|
77
|
+
added as a guard before changing it.
|
|
78
|
+
|
|
79
|
+
## Intentionally not changed
|
|
80
|
+
|
|
81
|
+
### ms_stereo permissiveness — `src/hca/decoder.rs`
|
|
82
|
+
cridecoder decodes ms_stereo HCA; vgmstream (`clhca.c:985`) rejects it as
|
|
83
|
+
`HCA_ERROR_HEADER` (`//TODO: should work but untested`). cridecoder's behavior is the
|
|
84
|
+
more permissive (and arguably more useful) one, so this divergence is **kept on purpose**.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Type stubs for cridecoder — CRI codec library (ACB/AWB, HCA audio, USM video).
|
|
2
|
+
|
|
3
|
+
These signatures mirror the PyO3 bindings in `src/python.rs`. Arguments typed
|
|
4
|
+
`Optional[...]` without a default are required but accept ``None``; arguments
|
|
5
|
+
with a default are optional.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"extract_acb",
|
|
12
|
+
"extract_acb_tracks",
|
|
13
|
+
"decode_acb_to_wav",
|
|
14
|
+
"build_acb",
|
|
15
|
+
"build_acb_bytes",
|
|
16
|
+
"build_music_acb_bytes",
|
|
17
|
+
"decode_hca",
|
|
18
|
+
"decode_hca_bytes",
|
|
19
|
+
"encode_hca",
|
|
20
|
+
"encode_hca_bytes",
|
|
21
|
+
"extract_usm",
|
|
22
|
+
"build_usm",
|
|
23
|
+
"build_usm_bytes",
|
|
24
|
+
"read_usm_metadata",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# --- ACB ---------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def extract_acb(acb_path: str, output_dir: str) -> Optional[list[str]]:
|
|
30
|
+
"""Extract audio tracks from an ACB file to ``output_dir``.
|
|
31
|
+
|
|
32
|
+
Returns the list of written file paths, or ``None`` if the file is invalid.
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def extract_acb_tracks(
|
|
37
|
+
acb_path: str, output_dir: str
|
|
38
|
+
) -> Optional[list[dict[str, object]]]:
|
|
39
|
+
"""Extract audio tracks from an ACB, returning per-track metadata.
|
|
40
|
+
|
|
41
|
+
Like :func:`extract_acb`, but each entry is a dict with ``path`` (written
|
|
42
|
+
file), ``name`` (cue name), ``cue_id`` and ``subkey`` — the AFS2 subkey of
|
|
43
|
+
the originating AWB, needed (with the global keycode) to decode type-56
|
|
44
|
+
encrypted HCA. Returns ``None`` if the file is invalid.
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def decode_acb_to_wav(
|
|
49
|
+
acb_path: str, output_dir: str, key: Optional[int] = ...
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
"""Extract an ACB and decode its HCA tracks straight to WAV files.
|
|
52
|
+
|
|
53
|
+
The per-AWB AFS2 subkey is applied automatically, so encrypted (type-56)
|
|
54
|
+
ACBs only need the global ``key`` (omit/``None`` for unencrypted ACBs).
|
|
55
|
+
Non-HCA tracks are written verbatim with their original extension. Returns
|
|
56
|
+
the list of written file paths.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def build_acb(tracks: list[tuple[str, int, bytes]], output_path: str) -> None:
|
|
61
|
+
"""Build an ACB file from ``(name, cue_id, hca_data)`` tuples, writing to disk."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
def build_acb_bytes(tracks: list[tuple[str, int, bytes]]) -> bytes:
|
|
65
|
+
"""Build an ACB from ``(name, cue_id, hca_data)`` tuples and return the bytes."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def build_music_acb_bytes(
|
|
69
|
+
name: str,
|
|
70
|
+
hca_data: bytes,
|
|
71
|
+
cue_id: int,
|
|
72
|
+
virtual_cue_suffix: Optional[str],
|
|
73
|
+
memory_awb_id: int,
|
|
74
|
+
reference_num_samples: int,
|
|
75
|
+
reference_length_ms: int,
|
|
76
|
+
acb_version: int,
|
|
77
|
+
acf_md5_hash: bytes,
|
|
78
|
+
acb_guid: bytes,
|
|
79
|
+
version_string: str,
|
|
80
|
+
acb_volume: float,
|
|
81
|
+
category_extension: int,
|
|
82
|
+
cue_priority_type: int,
|
|
83
|
+
acf_category_name: str,
|
|
84
|
+
acf_category_id: int,
|
|
85
|
+
acf_bus_names: list[str],
|
|
86
|
+
) -> bytes:
|
|
87
|
+
"""Build a single-track music ACB from one HCA track and return the bytes.
|
|
88
|
+
|
|
89
|
+
``acf_md5_hash`` and ``acb_guid`` are 16-byte values; ``virtual_cue_suffix``
|
|
90
|
+
may be ``None`` for no paired virtual cue.
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
# --- HCA ---------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def decode_hca(
|
|
97
|
+
hca_path: str,
|
|
98
|
+
wav_path: str,
|
|
99
|
+
key: Optional[int] = ...,
|
|
100
|
+
subkey: Optional[int] = ...,
|
|
101
|
+
) -> dict[str, int]:
|
|
102
|
+
"""Decode an HCA file to a WAV file.
|
|
103
|
+
|
|
104
|
+
``key``/``subkey`` apply the type-56 decryption keycode for encrypted HCA
|
|
105
|
+
(no-op for unencrypted files). Returns a dict with ``sample_rate``,
|
|
106
|
+
``channels``, ``block_count``, ``block_size``, ``encoder_delay`` and
|
|
107
|
+
``samples_per_block``.
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
def decode_hca_bytes(
|
|
112
|
+
hca_data: bytes,
|
|
113
|
+
key: Optional[int] = ...,
|
|
114
|
+
subkey: Optional[int] = ...,
|
|
115
|
+
) -> bytes:
|
|
116
|
+
"""Decode HCA bytes to WAV bytes in memory (``key``/``subkey`` as in :func:`decode_hca`)."""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
def encode_hca_bytes(
|
|
120
|
+
wav_data: bytes,
|
|
121
|
+
sample_rate: Optional[int] = ...,
|
|
122
|
+
channels: Optional[int] = ...,
|
|
123
|
+
bitrate: int = ...,
|
|
124
|
+
encryption_key: Optional[int] = ...,
|
|
125
|
+
) -> bytes:
|
|
126
|
+
"""Encode WAV bytes to HCA bytes.
|
|
127
|
+
|
|
128
|
+
``sample_rate``/``channels`` default to the WAV header when ``None``;
|
|
129
|
+
``bitrate`` defaults to 256000 bps. Supports 16/24/32-bit PCM input.
|
|
130
|
+
"""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
def encode_hca(
|
|
134
|
+
wav_path: str,
|
|
135
|
+
hca_path: str,
|
|
136
|
+
bitrate: int = ...,
|
|
137
|
+
encryption_key: Optional[int] = ...,
|
|
138
|
+
) -> dict[str, int]:
|
|
139
|
+
"""Encode a WAV file to an HCA file. Returns a dict with ``size`` and ``bitrate``."""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
# --- USM ---------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def extract_usm(
|
|
145
|
+
usm_path: str,
|
|
146
|
+
output_dir: str,
|
|
147
|
+
key: Optional[int] = ...,
|
|
148
|
+
export_audio: bool = ...,
|
|
149
|
+
) -> list[str]:
|
|
150
|
+
"""Extract video (and optionally audio) streams from a USM file.
|
|
151
|
+
|
|
152
|
+
``key`` decrypts encrypted USMs; ``export_audio`` (default ``False``) also
|
|
153
|
+
writes audio streams. Returns the list of written file paths.
|
|
154
|
+
"""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def build_usm(
|
|
158
|
+
name: str,
|
|
159
|
+
video_data: bytes,
|
|
160
|
+
output_path: str,
|
|
161
|
+
encryption_key: Optional[int] = ...,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Build a USM file from M2V video data, writing to disk."""
|
|
164
|
+
...
|
|
165
|
+
|
|
166
|
+
def build_usm_bytes(
|
|
167
|
+
name: str,
|
|
168
|
+
video_data: bytes,
|
|
169
|
+
encryption_key: Optional[int] = ...,
|
|
170
|
+
) -> bytes:
|
|
171
|
+
"""Build a USM from M2V video data and return the bytes."""
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
def read_usm_metadata(usm_path: str) -> str:
|
|
175
|
+
"""Read USM metadata and return it as a pretty-printed JSON string."""
|
|
176
|
+
...
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//! HCA decode profiling harness. Decodes the file many times in a tight loop so
|
|
2
|
+
//! a sampling profiler (`sample <pid>`) can attribute CPU time to hot functions.
|
|
3
|
+
//!
|
|
4
|
+
//! Also reports pure-Rust decode timing (to /dev/null, no WAV Vec growth and no
|
|
5
|
+
//! input copy), isolating the decoder core from Python-binding overhead.
|
|
6
|
+
|
|
7
|
+
use cridecoder::HcaDecoder;
|
|
8
|
+
use std::env;
|
|
9
|
+
use std::io::sink;
|
|
10
|
+
use std::time::Instant;
|
|
11
|
+
|
|
12
|
+
fn main() {
|
|
13
|
+
let args: Vec<String> = env::args().collect();
|
|
14
|
+
let hca_file = args.get(1).map(|s| s.as_str()).unwrap_or("music_5031.hca");
|
|
15
|
+
let iters: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(400);
|
|
16
|
+
|
|
17
|
+
let probe = HcaDecoder::from_file(hca_file).expect("open HCA");
|
|
18
|
+
println!(
|
|
19
|
+
"profiling: {} iters, {} blocks, {} ch, sr {}",
|
|
20
|
+
iters,
|
|
21
|
+
probe.info().block_count,
|
|
22
|
+
probe.info().channel_count,
|
|
23
|
+
probe.info().sampling_rate
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Warm up once.
|
|
27
|
+
{
|
|
28
|
+
let mut d = HcaDecoder::from_file(hca_file).unwrap();
|
|
29
|
+
d.decode_to_wav(&mut sink()).unwrap();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Timed pure-decode loop: decode to /dev/null so we measure only the core.
|
|
33
|
+
let start = Instant::now();
|
|
34
|
+
let mut total = 0u64;
|
|
35
|
+
for _ in 0..iters {
|
|
36
|
+
let mut d = HcaDecoder::from_file(hca_file).unwrap();
|
|
37
|
+
d.decode_to_wav(&mut sink()).unwrap();
|
|
38
|
+
total += 1;
|
|
39
|
+
}
|
|
40
|
+
let elapsed = start.elapsed();
|
|
41
|
+
println!(
|
|
42
|
+
"pure decode core: {} iters in {:?} = {:.3} ms/decode (to /dev/null)",
|
|
43
|
+
iters,
|
|
44
|
+
elapsed,
|
|
45
|
+
elapsed.as_secs_f64() * 1000.0 / iters as f64
|
|
46
|
+
);
|
|
47
|
+
std::hint::black_box(total);
|
|
48
|
+
}
|
|
@@ -86,7 +86,13 @@ impl<R: Read + Seek> AfsArchive<R> {
|
|
|
86
86
|
let mut files = Vec::with_capacity(file_count as usize);
|
|
87
87
|
for i in 0..file_count as usize {
|
|
88
88
|
let aligned_offset = align(alignment, offsets[i]);
|
|
89
|
-
|
|
89
|
+
// Align the next boundary too (except the final archive end), matching
|
|
90
|
+
// vgmstream awb.c so a subfile's size spans up to the next aligned start.
|
|
91
|
+
let next_offset = if i + 1 < file_count as usize {
|
|
92
|
+
align(alignment, offsets[i + 1])
|
|
93
|
+
} else {
|
|
94
|
+
offsets[i + 1]
|
|
95
|
+
};
|
|
90
96
|
let size = next_offset - aligned_offset;
|
|
91
97
|
|
|
92
98
|
files.push(AfsFileEntry {
|
|
@@ -52,7 +52,9 @@ pub fn wave_type_extension(enc_type: i32) -> &'static str {
|
|
|
52
52
|
WAVEFORM_ENCODE_TYPE_HCA => ".hca",
|
|
53
53
|
WAVEFORM_ENCODE_TYPE_WII_ADPCM => ".wiiadpcm",
|
|
54
54
|
WAVEFORM_ENCODE_TYPE_DS_ADPCM => ".dsadpcm",
|
|
55
|
-
|
|
55
|
+
// HCA-MX is a standard HCA bitstream (HCA\0 magic); vgmstream awb.c:178
|
|
56
|
+
// and PyCriCodecs both return ".hca" for type 6.
|
|
57
|
+
WAVEFORM_ENCODE_TYPE_HCA_MX => ".hca",
|
|
56
58
|
WAVEFORM_ENCODE_TYPE_VAG | WAVEFORM_ENCODE_TYPE_HEVAG => ".vag",
|
|
57
59
|
WAVEFORM_ENCODE_TYPE_ATRAC3 => ".at3",
|
|
58
60
|
WAVEFORM_ENCODE_TYPE_BCWAV => ".bcwav",
|
|
@@ -83,7 +85,7 @@ mod tests {
|
|
|
83
85
|
wave_type_extension(WAVEFORM_ENCODE_TYPE_DS_ADPCM),
|
|
84
86
|
".dsadpcm"
|
|
85
87
|
);
|
|
86
|
-
assert_eq!(wave_type_extension(WAVEFORM_ENCODE_TYPE_HCA_MX), ".
|
|
88
|
+
assert_eq!(wave_type_extension(WAVEFORM_ENCODE_TYPE_HCA_MX), ".hca");
|
|
87
89
|
assert_eq!(wave_type_extension(WAVEFORM_ENCODE_TYPE_VAG), ".vag");
|
|
88
90
|
assert_eq!(wave_type_extension(WAVEFORM_ENCODE_TYPE_ATRAC3), ".at3");
|
|
89
91
|
assert_eq!(wave_type_extension(WAVEFORM_ENCODE_TYPE_BCWAV), ".bcwav");
|