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.
Files changed (47) hide show
  1. {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/workflows/ci.yml +1 -1
  2. {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/workflows/release-crate.yml +1 -1
  3. {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/workflows/release-python.yml +4 -4
  4. {cridecoder-0.2.3 → cridecoder-0.3.1}/.gitignore +7 -0
  5. {cridecoder-0.2.3 → cridecoder-0.3.1}/Cargo.lock +18 -12
  6. {cridecoder-0.2.3 → cridecoder-0.3.1}/Cargo.toml +3 -2
  7. cridecoder-0.3.1/KNOWN_GAPS.md +84 -0
  8. {cridecoder-0.2.3 → cridecoder-0.3.1}/PKG-INFO +1 -1
  9. cridecoder-0.3.1/cridecoder.pyi +176 -0
  10. cridecoder-0.3.1/examples/profile_hca.rs +48 -0
  11. {cridecoder-0.2.3 → cridecoder-0.3.1}/pyproject.toml +1 -1
  12. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/afs.rs +7 -1
  13. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/consts.rs +4 -2
  14. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/extractor.rs +111 -19
  15. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/track.rs +48 -47
  16. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/utf.rs +22 -12
  17. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb.rs +4 -1
  18. cridecoder-0.3.1/src/hca/bitreader.rs +358 -0
  19. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/decoder.rs +151 -33
  20. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/encoder.rs +52 -0
  21. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/hca_file.rs +91 -9
  22. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/imdct.rs +356 -94
  23. cridecoder-0.3.1/src/hca/tables.rs +122 -0
  24. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/lib.rs +4 -1
  25. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/python.rs +109 -1
  26. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/reader.rs +18 -1
  27. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm/builder.rs +4 -2
  28. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm/extractor.rs +360 -54
  29. {cridecoder-0.2.3 → cridecoder-0.3.1}/tests/integration_tests.rs +37 -0
  30. cridecoder-0.2.3/src/hca/bitreader.rs +0 -230
  31. cridecoder-0.2.3/src/hca/tables.rs +0 -154
  32. {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/copilot-instructions.md +0 -0
  33. {cridecoder-0.2.3 → cridecoder-0.3.1}/.github/dependabot.yml +0 -0
  34. {cridecoder-0.2.3 → cridecoder-0.3.1}/AGENTS.md +0 -0
  35. {cridecoder-0.2.3 → cridecoder-0.3.1}/CLAUDE.md +0 -0
  36. {cridecoder-0.2.3 → cridecoder-0.3.1}/LICENSE +0 -0
  37. {cridecoder-0.2.3 → cridecoder-0.3.1}/README.md +0 -0
  38. {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/debug_acb.rs +0 -0
  39. {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/test_acb.rs +0 -0
  40. {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/test_hca.rs +0 -0
  41. {cridecoder-0.2.3 → cridecoder-0.3.1}/examples/test_usm.rs +0 -0
  42. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/acb/builder.rs +0 -0
  43. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/ath.rs +0 -0
  44. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca/cipher.rs +0 -0
  45. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/hca.rs +0 -0
  46. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm/metadata.rs +0 -0
  47. {cridecoder-0.2.3 → cridecoder-0.3.1}/src/usm.rs +0 -0
@@ -23,7 +23,7 @@ jobs:
23
23
  runs-on: ubuntu-latest
24
24
  steps:
25
25
  - name: Checkout
26
- uses: actions/checkout@v6
26
+ uses: actions/checkout@v7
27
27
 
28
28
  - name: Install Rust stable
29
29
  uses: dtolnay/rust-toolchain@stable
@@ -10,7 +10,7 @@ jobs:
10
10
  name: Publish to crates.io
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@v6
13
+ - uses: actions/checkout@v7
14
14
  - uses: dtolnay/rust-toolchain@stable
15
15
  - uses: Swatinem/rust-cache@v2
16
16
  - name: Set version from tag
@@ -22,7 +22,7 @@ jobs:
22
22
  matrix:
23
23
  target: [x86_64, aarch64]
24
24
  steps:
25
- - uses: actions/checkout@v6
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@v6
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@v6
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@v6
127
+ - uses: actions/checkout@v7
128
128
  - name: Set version from tag
129
129
  if: github.event_name == 'release'
130
130
  run: |
@@ -30,3 +30,10 @@ test_output_usm/
30
30
  *.hca
31
31
  *.wav
32
32
  *.m2v
33
+
34
+ # OS / editor / local benchmarking
35
+ .DS_Store
36
+ .idea/
37
+ bench/
38
+ __pycache__/
39
+ *.pyc
@@ -28,12 +28,13 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
28
28
 
29
29
  [[package]]
30
30
  name = "cridecoder"
31
- version = "0.2.3"
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.28.3"
208
+ version = "0.29.0"
208
209
  source = "registry+https://github.com/rust-lang/crates.io-index"
209
- checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
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.28.3"
222
+ version = "0.29.0"
222
223
  source = "registry+https://github.com/rust-lang/crates.io-index"
223
- checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
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.28.3"
231
+ version = "0.29.0"
231
232
  source = "registry+https://github.com/rust-lang/crates.io-index"
232
- checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
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.28.3"
241
+ version = "0.29.0"
241
242
  source = "registry+https://github.com/rust-lang/crates.io-index"
242
- checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
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.28.3"
253
+ version = "0.29.0"
253
254
  source = "registry+https://github.com/rust-lang/crates.io-index"
254
- checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
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.2.3"
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
- pyo3 = { version = "0.28", features = ["extension-module"], optional = true }
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**.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cridecoder
3
- Version: 0.2.3
3
+ Version: 0.3.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -0,0 +1,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
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cridecoder"
7
- version = "0.2.3"
7
+ version = "0.3.1"
8
8
  description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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
- let next_offset = offsets[i + 1];
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
- WAVEFORM_ENCODE_TYPE_HCA_MX => ".hcamx",
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), ".hcamx");
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");