cridecoder 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {cridecoder-0.2.1 → cridecoder-0.2.2}/.github/workflows/release-python.yml +2 -2
  2. {cridecoder-0.2.1 → cridecoder-0.2.2}/Cargo.lock +13 -13
  3. {cridecoder-0.2.1 → cridecoder-0.2.2}/Cargo.toml +13 -2
  4. {cridecoder-0.2.1 → cridecoder-0.2.2}/PKG-INFO +1 -1
  5. {cridecoder-0.2.1 → cridecoder-0.2.2}/pyproject.toml +1 -2
  6. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb/afs.rs +22 -6
  7. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb/extractor.rs +0 -64
  8. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb.rs +1 -1
  9. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/decoder.rs +77 -35
  10. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/hca_file.rs +98 -36
  11. cridecoder-0.2.2/src/hca/imdct.rs +350 -0
  12. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/lib.rs +2 -4
  13. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/python.rs +0 -79
  14. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/reader.rs +15 -1
  15. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/usm/extractor.rs +39 -94
  16. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/usm.rs +1 -3
  17. {cridecoder-0.2.1 → cridecoder-0.2.2}/tests/integration_tests.rs +37 -36
  18. cridecoder-0.2.1/cridecoder.pyi +0 -114
  19. cridecoder-0.2.1/src/hca/imdct.rs +0 -328
  20. cridecoder-0.2.1/tests/test_python.py +0 -332
  21. {cridecoder-0.2.1 → cridecoder-0.2.2}/.github/copilot-instructions.md +0 -0
  22. {cridecoder-0.2.1 → cridecoder-0.2.2}/.github/dependabot.yml +0 -0
  23. {cridecoder-0.2.1 → cridecoder-0.2.2}/.github/workflows/ci.yml +0 -0
  24. {cridecoder-0.2.1 → cridecoder-0.2.2}/.github/workflows/release-crate.yml +0 -0
  25. {cridecoder-0.2.1 → cridecoder-0.2.2}/.gitignore +0 -0
  26. {cridecoder-0.2.1 → cridecoder-0.2.2}/AGENTS.md +0 -0
  27. {cridecoder-0.2.1 → cridecoder-0.2.2}/CLAUDE.md +0 -0
  28. {cridecoder-0.2.1 → cridecoder-0.2.2}/LICENSE +0 -0
  29. {cridecoder-0.2.1 → cridecoder-0.2.2}/README.md +0 -0
  30. {cridecoder-0.2.1 → cridecoder-0.2.2}/examples/debug_acb.rs +0 -0
  31. {cridecoder-0.2.1 → cridecoder-0.2.2}/examples/test_acb.rs +0 -0
  32. {cridecoder-0.2.1 → cridecoder-0.2.2}/examples/test_hca.rs +0 -0
  33. {cridecoder-0.2.1 → cridecoder-0.2.2}/examples/test_usm.rs +0 -0
  34. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb/builder.rs +0 -0
  35. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb/consts.rs +0 -0
  36. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb/track.rs +0 -0
  37. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/acb/utf.rs +0 -0
  38. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/ath.rs +0 -0
  39. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/bitreader.rs +0 -0
  40. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/cipher.rs +0 -0
  41. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/encoder.rs +0 -0
  42. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca/tables.rs +0 -0
  43. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/hca.rs +0 -0
  44. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/usm/builder.rs +0 -0
  45. {cridecoder-0.2.1 → cridecoder-0.2.2}/src/usm/metadata.rs +0 -0
@@ -58,7 +58,7 @@ jobs:
58
58
  VERSION="${GITHUB_REF_NAME#v}"
59
59
  sed -i '' "1,/^version = .*/{s/^version = .*/version = \"${VERSION}\"/;}" Cargo.toml
60
60
  sed -i '' "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml
61
- - uses: actions/setup-python@v5
61
+ - uses: actions/setup-python@v6
62
62
  with:
63
63
  python-version: |
64
64
  3.9
@@ -95,7 +95,7 @@ jobs:
95
95
  VERSION="${GITHUB_REF_NAME#v}"
96
96
  sed -i "0,/^version = .*/s//version = \"${VERSION}\"/" Cargo.toml
97
97
  sed -i "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml
98
- - uses: actions/setup-python@v5
98
+ - uses: actions/setup-python@v6
99
99
  with:
100
100
  python-version: |
101
101
  3.9
@@ -28,7 +28,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
28
28
 
29
29
  [[package]]
30
30
  name = "cridecoder"
31
- version = "0.2.1"
31
+ version = "0.2.2"
32
32
  dependencies = [
33
33
  "byteorder",
34
34
  "encoding_rs",
@@ -204,9 +204,9 @@ dependencies = [
204
204
 
205
205
  [[package]]
206
206
  name = "pyo3"
207
- version = "0.28.2"
207
+ version = "0.28.3"
208
208
  source = "registry+https://github.com/rust-lang/crates.io-index"
209
- checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1"
209
+ checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
210
210
  dependencies = [
211
211
  "libc",
212
212
  "once_cell",
@@ -218,18 +218,18 @@ dependencies = [
218
218
 
219
219
  [[package]]
220
220
  name = "pyo3-build-config"
221
- version = "0.28.2"
221
+ version = "0.28.3"
222
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
223
- checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7"
223
+ checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
224
224
  dependencies = [
225
225
  "target-lexicon",
226
226
  ]
227
227
 
228
228
  [[package]]
229
229
  name = "pyo3-ffi"
230
- version = "0.28.2"
230
+ version = "0.28.3"
231
231
  source = "registry+https://github.com/rust-lang/crates.io-index"
232
- checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc"
232
+ checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
233
233
  dependencies = [
234
234
  "libc",
235
235
  "pyo3-build-config",
@@ -237,9 +237,9 @@ dependencies = [
237
237
 
238
238
  [[package]]
239
239
  name = "pyo3-macros"
240
- version = "0.28.2"
240
+ version = "0.28.3"
241
241
  source = "registry+https://github.com/rust-lang/crates.io-index"
242
- checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e"
242
+ checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
243
243
  dependencies = [
244
244
  "proc-macro2",
245
245
  "pyo3-macros-backend",
@@ -249,9 +249,9 @@ dependencies = [
249
249
 
250
250
  [[package]]
251
251
  name = "pyo3-macros-backend"
252
- version = "0.28.2"
252
+ version = "0.28.3"
253
253
  source = "registry+https://github.com/rust-lang/crates.io-index"
254
- checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a"
254
+ checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
255
255
  dependencies = [
256
256
  "heck",
257
257
  "proc-macro2",
@@ -326,9 +326,9 @@ dependencies = [
326
326
 
327
327
  [[package]]
328
328
  name = "serde_json"
329
- version = "1.0.149"
329
+ version = "1.0.150"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
331
+ checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
332
332
  dependencies = [
333
333
  "itoa",
334
334
  "memchr",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cridecoder"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
  description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
6
6
  license = "MIT"
@@ -10,7 +10,18 @@ homepage = "https://github.com/Team-Haruki/cridecoder"
10
10
  readme = "README.md"
11
11
  keywords = ["cri", "hca", "acb", "usm", "audio"]
12
12
  categories = ["multimedia::audio", "multimedia::video", "parser-implementations"]
13
- exclude = ["*.acb", "*.usm", "*.hca", "*.wav", "*.m2v", "test_output_*"]
13
+ exclude = [
14
+ "*.acb",
15
+ "*.usm",
16
+ "*.hca",
17
+ "*.wav",
18
+ "*.m2v",
19
+ "test_output_*",
20
+ ".DS_Store",
21
+ ".idea/*",
22
+ "Cargo.toml.orig",
23
+ "tests/__pycache__/*",
24
+ ]
14
25
 
15
26
  [lib]
16
27
  crate-type = ["cdylib", "rlib"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cridecoder
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cridecoder"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "CRI codec library for ACB/AWB, HCA audio, and USM video extraction"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -23,4 +23,3 @@ free-threaded = true
23
23
 
24
24
  [tool.maturin]
25
25
  features = ["python"]
26
- include = [{ path = "cridecoder.pyi", format = "wheel" }]
@@ -1,6 +1,7 @@
1
1
  //! AFS2 archive parser
2
2
 
3
3
  use crate::reader::{align, Reader};
4
+ use std::collections::HashMap;
4
5
  use std::io::{Read, Seek, SeekFrom};
5
6
  use thiserror::Error;
6
7
 
@@ -15,7 +16,7 @@ pub enum AfsError {
15
16
  }
16
17
 
17
18
  /// A file entry in an AFS2 archive
18
- #[derive(Debug, Clone)]
19
+ #[derive(Debug, Clone, Copy)]
19
20
  pub struct AfsFileEntry {
20
21
  pub cue_id: i32,
21
22
  pub offset: u32,
@@ -27,6 +28,7 @@ pub struct AfsArchive<R: Read + Seek> {
27
28
  pub alignment: u32,
28
29
  pub subkey: u16,
29
30
  pub files: Vec<AfsFileEntry>,
31
+ cue_index: HashMap<i32, usize>,
30
32
  reader: Reader<R>,
31
33
  }
32
34
 
@@ -94,25 +96,30 @@ impl<R: Read + Seek> AfsArchive<R> {
94
96
  });
95
97
  }
96
98
 
99
+ let cue_index = files
100
+ .iter()
101
+ .enumerate()
102
+ .map(|(idx, file)| (file.cue_id, idx))
103
+ .collect();
104
+
97
105
  Ok(Self {
98
106
  alignment,
99
107
  subkey,
100
108
  files,
109
+ cue_index,
101
110
  reader: buf,
102
111
  })
103
112
  }
104
113
 
105
114
  /// Get file data for a specific cue ID
106
115
  pub fn file_data_for_cue_id(&mut self, cue_id: i32) -> Result<Vec<u8>, AfsError> {
107
- for f in &self.files {
108
- if f.cue_id == cue_id {
109
- return self.file_data(f.clone());
110
- }
116
+ if let Some(&idx) = self.cue_index.get(&cue_id) {
117
+ return self.file_data_at_index(idx);
111
118
  }
112
119
 
113
120
  // Fallback to first file if cue IDs start at 0
114
121
  if !self.files.is_empty() && self.files[0].cue_id == 0 {
115
- return self.file_data(self.files[0].clone());
122
+ return self.file_data_at_index(0);
116
123
  }
117
124
 
118
125
  Err(AfsError::CueNotFound(cue_id))
@@ -120,6 +127,15 @@ impl<R: Read + Seek> AfsArchive<R> {
120
127
 
121
128
  /// Get file data for an entry
122
129
  pub fn file_data(&mut self, entry: AfsFileEntry) -> Result<Vec<u8>, AfsError> {
130
+ self.file_data_by_entry(&entry)
131
+ }
132
+
133
+ fn file_data_at_index(&mut self, index: usize) -> Result<Vec<u8>, AfsError> {
134
+ let entry = self.files[index];
135
+ self.file_data_by_entry(&entry)
136
+ }
137
+
138
+ fn file_data_by_entry(&mut self, entry: &AfsFileEntry) -> Result<Vec<u8>, AfsError> {
123
139
  Ok(self
124
140
  .reader
125
141
  .read_bytes_at(entry.size as usize, entry.offset as u64)?)
@@ -23,14 +23,6 @@ pub enum ExtractError {
23
23
  InvalidAcb,
24
24
  }
25
25
 
26
- /// Audio track data extracted from an ACB/AWB container.
27
- #[derive(Debug, Clone, PartialEq, Eq)]
28
- pub struct ExtractedAcbTrack {
29
- pub name: String,
30
- pub extension: String,
31
- pub data: Vec<u8>,
32
- }
33
-
34
26
  /// Extract all audio files from an ACB file
35
27
  pub fn extract_acb<R: Read + Seek>(
36
28
  acb_file: R,
@@ -52,21 +44,6 @@ pub fn extract_acb<R: Read + Seek>(
52
44
  )
53
45
  }
54
46
 
55
- /// Extract all audio tracks from an ACB reader into memory.
56
- pub fn extract_acb_to_memory<R: Read + Seek>(
57
- acb_file: R,
58
- acb_file_path: Option<&Path>,
59
- ) -> Result<Vec<ExtractedAcbTrack>, ExtractError> {
60
- let utf = UtfTable::new(acb_file)?;
61
-
62
- let track_list = TrackList::new(&utf)?;
63
-
64
- let mut embedded_awb = load_embedded_awb(&utf.rows[0]);
65
- let mut external_awbs = load_external_awbs(&utf.rows[0], acb_file_path);
66
-
67
- extract_all_tracks_to_memory(&track_list, &mut embedded_awb, &mut external_awbs)
68
- }
69
-
70
47
  fn load_embedded_awb(
71
48
  row: &std::collections::HashMap<String, crate::acb::utf::Value>,
72
49
  ) -> Option<AfsArchive<Cursor<Vec<u8>>>> {
@@ -143,22 +120,6 @@ fn extract_all_tracks(
143
120
  Ok(outputs)
144
121
  }
145
122
 
146
- fn extract_all_tracks_to_memory(
147
- track_list: &TrackList,
148
- embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
149
- external_awbs: &mut [AfsArchive<Cursor<Vec<u8>>>],
150
- ) -> Result<Vec<ExtractedAcbTrack>, ExtractError> {
151
- let mut outputs = Vec::new();
152
-
153
- for track in &track_list.tracks {
154
- if let Some(output) = extract_single_track_to_memory(track, embedded_awb, external_awbs)? {
155
- outputs.push(output);
156
- }
157
- }
158
-
159
- Ok(outputs)
160
- }
161
-
162
123
  fn extract_single_track(
163
124
  track: &Track,
164
125
  target_dir: &Path,
@@ -185,31 +146,6 @@ fn extract_single_track(
185
146
  Ok(Some(output_path.to_string_lossy().into_owned()))
186
147
  }
187
148
 
188
- fn extract_single_track_to_memory(
189
- track: &Track,
190
- embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
191
- external_awbs: &mut [AfsArchive<Cursor<Vec<u8>>>],
192
- ) -> Result<Option<ExtractedAcbTrack>, ExtractError> {
193
- let ext = wave_type_extension(track.enc_type);
194
- let extension = if ext.is_empty() {
195
- track.enc_type.to_string()
196
- } else {
197
- ext.trim_start_matches('.').to_string()
198
- };
199
-
200
- let data = get_track_data(track, embedded_awb, external_awbs)?;
201
- let data = match data {
202
- Some(d) => d,
203
- None => return Ok(None),
204
- };
205
-
206
- Ok(Some(ExtractedAcbTrack {
207
- name: track.name.clone(),
208
- extension,
209
- data,
210
- }))
211
- }
212
-
213
149
  fn get_track_data(
214
150
  track: &Track,
215
151
  embedded_awb: &mut Option<AfsArchive<Cursor<Vec<u8>>>>,
@@ -12,6 +12,6 @@ pub use builder::{
12
12
  AcbBuilder, AfsArchiveBuilder, BuilderError, ColumnDef, TrackInput, UtfTableBuilder,
13
13
  };
14
14
  pub use consts::*;
15
- pub use extractor::{extract_acb, extract_acb_from_file, extract_acb_to_memory, ExtractedAcbTrack};
15
+ pub use extractor::{extract_acb, extract_acb_from_file};
16
16
  pub use track::{Track, TrackList};
17
17
  pub use utf::{UtfHeader, UtfTable, Value};
@@ -24,7 +24,6 @@ pub const HCA_MASK: u32 = 0x7F7F7F7F;
24
24
  pub const HCA_SUBFRAMES: usize = 8;
25
25
  pub const HCA_SAMPLES_PER_SUBFRAME: usize = 128;
26
26
  pub const HCA_SAMPLES_PER_FRAME: usize = HCA_SUBFRAMES * HCA_SAMPLES_PER_SUBFRAME;
27
- pub const HCA_MDCT_BITS: usize = 7;
28
27
  pub const HCA_MIN_CHANNELS: u32 = 1;
29
28
  pub const HCA_MAX_CHANNELS: usize = 16;
30
29
  pub const HCA_MIN_SAMPLE_RATE: u32 = 1;
@@ -726,16 +725,36 @@ impl ClHca {
726
725
 
727
726
  /// Read decoded samples as 16-bit PCM
728
727
  pub fn read_samples_16(&self, samples: &mut [i16]) {
729
- const SCALE_F: f32 = 32768.0;
730
-
731
- let mut idx = 0;
732
- for i in 0..HCA_SUBFRAMES {
733
- for j in 0..HCA_SAMPLES_PER_SUBFRAME {
734
- for k in 0..self.channels as usize {
735
- let f = self.channel[k].wave[i][j];
736
- let s = (f * SCALE_F) as i32;
737
- samples[idx] = s.clamp(-32768, 32767) as i16;
738
- idx += 1;
728
+ let channels = self.channels as usize;
729
+
730
+ match channels {
731
+ 1 => {
732
+ for i in 0..HCA_SUBFRAMES {
733
+ let base = i * HCA_SAMPLES_PER_SUBFRAME;
734
+ for j in 0..HCA_SAMPLES_PER_SUBFRAME {
735
+ samples[base + j] = pcm_f32_to_i16(self.channel[0].wave[i][j]);
736
+ }
737
+ }
738
+ }
739
+ 2 => {
740
+ for i in 0..HCA_SUBFRAMES {
741
+ let base = i * HCA_SAMPLES_PER_SUBFRAME * 2;
742
+ for j in 0..HCA_SAMPLES_PER_SUBFRAME {
743
+ let idx = base + j * 2;
744
+ samples[idx] = pcm_f32_to_i16(self.channel[0].wave[i][j]);
745
+ samples[idx + 1] = pcm_f32_to_i16(self.channel[1].wave[i][j]);
746
+ }
747
+ }
748
+ }
749
+ _ => {
750
+ let mut idx = 0;
751
+ for i in 0..HCA_SUBFRAMES {
752
+ for j in 0..HCA_SAMPLES_PER_SUBFRAME {
753
+ for k in 0..channels {
754
+ samples[idx] = pcm_f32_to_i16(self.channel[k].wave[i][j]);
755
+ idx += 1;
756
+ }
757
+ }
739
758
  }
740
759
  }
741
760
  }
@@ -899,8 +918,10 @@ impl ClHca {
899
918
  return Err(HcaError::ChecksumFailed);
900
919
  }
901
920
 
902
- // Decrypt
903
- cipher_decrypt(&self.cipher_table, &mut data[..self.frame_size as usize]);
921
+ // Decrypt only when the file uses a non-identity cipher.
922
+ if self.ciph_type != 0 {
923
+ cipher_decrypt(&self.cipher_table, &mut data[..self.frame_size as usize]);
924
+ }
904
925
 
905
926
  // Re-initialize bitreader after decryption
906
927
  let mut br = BitReader::with_offset(data, 2); // Skip sync word
@@ -1113,40 +1134,54 @@ impl ClHca {
1113
1134
  fn dequantize_coefficients(&mut self, ch: usize, br: &mut BitReader, subframe: usize) {
1114
1135
  let channel = &mut self.channel[ch];
1115
1136
  let cc_count = channel.coded_count;
1137
+ let spectra = &mut channel.spectra[subframe];
1116
1138
 
1117
- for i in 0..cc_count {
1139
+ for (i, sample) in spectra.iter_mut().enumerate().take(cc_count) {
1118
1140
  let resolution = channel.resolution[i];
1119
- let bits = MAX_BIT_TABLE[resolution as usize];
1120
- let code = br.read(bits as usize);
1121
-
1122
- let qc: f32 = if resolution > 7 {
1123
- // Sign-magnitude form: sign is bit 0, magnitude is bits 1+
1124
- let sign = if (code & 1) != 0 { -1i32 } else { 1i32 };
1125
- let signed_code = sign * (code >> 1) as i32;
1126
- if signed_code == 0 {
1127
- br.set_position(br.position() - 1);
1141
+ let qc = match resolution {
1142
+ 0 => 0.0,
1143
+ 1..=7 => {
1144
+ let bits = MAX_BIT_TABLE[resolution as usize];
1145
+ let code = br.read(bits as usize);
1146
+ let index = ((resolution as usize) << 4) + code as usize;
1147
+ let skip = READ_BIT_TABLE[index] as i32 - bits as i32;
1148
+ if skip > 0 {
1149
+ br.skip(skip as usize);
1150
+ } else if skip < 0 {
1151
+ br.set_position(br.position().saturating_sub((-skip) as usize));
1152
+ }
1153
+ READ_VAL_TABLE[index]
1128
1154
  }
1129
- signed_code as f32
1130
- } else {
1131
- // Prefix codebooks
1132
- let index = ((resolution as usize) << 4) + code as usize;
1133
- let skip = READ_BIT_TABLE[index] as i32 - bits as i32;
1134
- if skip > 0 {
1135
- br.skip(skip as usize);
1136
- } else if skip < 0 {
1137
- br.set_position(br.position().saturating_sub((-skip) as usize));
1155
+ _ => {
1156
+ let bits = MAX_BIT_TABLE[resolution as usize];
1157
+ let code = br.read(bits as usize);
1158
+ // Sign-magnitude form: sign is bit 0, magnitude is bits 1+
1159
+ let sign = if (code & 1) != 0 { -1i32 } else { 1i32 };
1160
+ let signed_code = sign * (code >> 1) as i32;
1161
+ if signed_code == 0 {
1162
+ br.set_position(br.position() - 1);
1163
+ }
1164
+ signed_code as f32
1138
1165
  }
1139
- READ_VAL_TABLE[index]
1140
1166
  };
1141
1167
 
1142
- channel.spectra[subframe][i] = channel.gain[i] * qc;
1168
+ *sample = channel.gain[i] * qc;
1143
1169
  }
1144
1170
 
1145
1171
  // Clean rest of spectra
1146
- channel.spectra[subframe][cc_count..HCA_SAMPLES_PER_SUBFRAME].fill(0.0);
1172
+ spectra[cc_count..HCA_SAMPLES_PER_SUBFRAME].fill(0.0);
1147
1173
  }
1148
1174
 
1149
1175
  fn decode_block_transform(&mut self) {
1176
+ if self.min_resolution > 0 && self.bands_per_hfr_group == 0 && self.stereo_band_count == 0 {
1177
+ for subframe in 0..HCA_SUBFRAMES {
1178
+ for ch in 0..self.channels as usize {
1179
+ imdct_transform(&mut self.channel[ch], subframe);
1180
+ }
1181
+ }
1182
+ return;
1183
+ }
1184
+
1150
1185
  for subframe in 0..HCA_SUBFRAMES {
1151
1186
  // Restore missing bands
1152
1187
  for ch in 0..self.channels as usize {
@@ -1286,6 +1321,13 @@ impl ClHca {
1286
1321
  }
1287
1322
  }
1288
1323
 
1324
+ #[inline]
1325
+ fn pcm_f32_to_i16(sample: f32) -> i16 {
1326
+ const SCALE_F: f32 = 32768.0;
1327
+ let scaled = (sample * SCALE_F) as i32;
1328
+ scaled.clamp(-32768, 32767) as i16
1329
+ }
1330
+
1289
1331
  #[cfg(test)]
1290
1332
  mod tests {
1291
1333
  use super::*;
@@ -155,6 +155,40 @@ impl<R: Read + Seek> HcaDecoder<R> {
155
155
  Ok((&self.fbuf[start_idx..], num_samples))
156
156
  }
157
157
 
158
+ /// Decode a single frame into interleaved 16-bit PCM samples.
159
+ ///
160
+ /// Returns the number of sample frames written, not the number of i16
161
+ /// values. The caller-provided buffer must fit one full decoded HCA frame.
162
+ pub fn decode_frame_i16(&mut self, pcm: &mut [i16]) -> Result<usize, HcaDecoderError> {
163
+ let frame_len = self.info.samples_per_block * self.info.channel_count as usize;
164
+ if pcm.len() < frame_len {
165
+ return Err(HcaDecoderError::InvalidSampleRange);
166
+ }
167
+
168
+ self.read_packet()?;
169
+ self.handle.decode_block(&mut self.buf)?;
170
+ self.handle.read_samples_16(&mut pcm[..frame_len]);
171
+
172
+ let samples = self.info.samples_per_block as i32;
173
+ let mut discard = 0;
174
+
175
+ if self.current_delay > 0 {
176
+ if self.current_delay >= samples {
177
+ self.current_delay -= samples;
178
+ return Ok(0);
179
+ }
180
+ discard = self.current_delay;
181
+ self.current_delay = 0;
182
+ }
183
+
184
+ let start = discard as usize * self.info.channel_count as usize;
185
+ if start > 0 {
186
+ pcm.copy_within(start..frame_len, 0);
187
+ }
188
+
189
+ Ok((samples - discard) as usize)
190
+ }
191
+
158
192
  /// Decode the entire HCA file and return all samples
159
193
  pub fn decode_all(&mut self) -> Result<Vec<f32>, HcaDecoderError> {
160
194
  self.reset();
@@ -177,6 +211,35 @@ impl<R: Read + Seek> HcaDecoder<R> {
177
211
  Ok(all_samples)
178
212
  }
179
213
 
214
+ /// Decode the entire HCA file as interleaved 16-bit PCM chunks.
215
+ ///
216
+ /// The callback receives only valid samples after encoder-delay trimming.
217
+ /// This is useful when piping HCA directly into an audio encoder without
218
+ /// materializing an intermediate WAV file.
219
+ pub fn decode_to_pcm16_chunks<F>(&mut self, mut on_chunk: F) -> Result<(), HcaDecoderError>
220
+ where
221
+ F: FnMut(&[i16]) -> Result<(), HcaDecoderError>,
222
+ {
223
+ self.reset();
224
+
225
+ let channels = self.info.channel_count as usize;
226
+ let mut pcm_buf = vec![0i16; self.info.samples_per_block * channels];
227
+
228
+ loop {
229
+ match self.decode_frame_i16(&mut pcm_buf) {
230
+ Ok(0) => {}
231
+ Ok(sample_frames) => {
232
+ let sample_count = sample_frames * channels;
233
+ on_chunk(&pcm_buf[..sample_count])?;
234
+ }
235
+ Err(HcaDecoderError::Eof) => break,
236
+ Err(e) => return Err(e),
237
+ }
238
+ }
239
+
240
+ Ok(())
241
+ }
242
+
180
243
  /// Seek to a specific sample position
181
244
  pub fn seek(&mut self, sample_num: u32) {
182
245
  let target_sample = sample_num + self.info.encoder_delay;
@@ -309,48 +372,47 @@ impl<R: Read + Seek> HcaDecoder<R> {
309
372
 
310
373
  w.write_all(&header)?;
311
374
 
312
- let mut pcm_buf =
313
- vec![0i16; self.info.samples_per_block * self.info.channel_count as usize];
314
-
315
- loop {
316
- match self.read_packet() {
317
- Ok(()) => {}
318
- Err(HcaDecoderError::Eof) => break,
319
- Err(e) => return Err(e),
320
- }
321
-
322
- self.handle.decode_block(&mut self.buf)?;
323
- self.handle.read_samples_16(&mut pcm_buf);
375
+ let frame_len = self.info.samples_per_block * self.info.channel_count as usize;
376
+ let mut data_buf = vec![0u8; frame_len * 2];
324
377
 
325
- let samples = self.info.samples_per_block as i32;
326
- let mut discard = 0;
327
-
328
- if self.current_delay > 0 {
329
- if self.current_delay >= samples {
330
- self.current_delay -= samples;
331
- continue;
332
- }
333
- discard = self.current_delay;
334
- self.current_delay = 0;
335
- }
336
-
337
- let start = discard as usize * self.info.channel_count as usize;
338
- let end = samples as usize * self.info.channel_count as usize;
339
-
340
- if start >= end || end > pcm_buf.len() {
378
+ self.decode_to_pcm16_chunks(|pcm| {
379
+ if pcm.len() > frame_len {
341
380
  return Err(HcaDecoderError::InvalidSampleRange);
342
381
  }
382
+ write_pcm_i16_le(w, pcm, &mut data_buf)?;
383
+ Ok(())
384
+ })
385
+ }
386
+ }
343
387
 
344
- // Write samples as little-endian bytes
345
- let mut data = vec![0u8; (end - start) * 2];
346
- for (i, &sample) in pcm_buf[start..end].iter().enumerate() {
347
- data[i * 2..i * 2 + 2].copy_from_slice(&sample.to_le_bytes());
348
- }
349
- w.write_all(&data)?;
350
- }
388
+ #[cfg(target_endian = "little")]
389
+ fn write_pcm_i16_le<W: Write>(
390
+ writer: &mut W,
391
+ samples: &[i16],
392
+ _scratch: &mut [u8],
393
+ ) -> io::Result<()> {
394
+ let bytes = unsafe {
395
+ // SAFETY: i16 is a plain integer type, and on little-endian targets its
396
+ // in-memory representation is exactly the little-endian PCM byte order.
397
+ std::slice::from_raw_parts(
398
+ samples.as_ptr().cast::<u8>(),
399
+ std::mem::size_of_val(samples),
400
+ )
401
+ };
402
+ writer.write_all(bytes)
403
+ }
351
404
 
352
- Ok(())
405
+ #[cfg(not(target_endian = "little"))]
406
+ fn write_pcm_i16_le<W: Write>(
407
+ writer: &mut W,
408
+ samples: &[i16],
409
+ scratch: &mut [u8],
410
+ ) -> io::Result<()> {
411
+ let byte_len = samples.len() * 2;
412
+ for (chunk, &sample) in scratch[..byte_len].chunks_exact_mut(2).zip(samples) {
413
+ chunk.copy_from_slice(&sample.to_le_bytes());
353
414
  }
415
+ writer.write_all(&scratch[..byte_len])
354
416
  }
355
417
 
356
418
  fn scale_frame_score(score: i32) -> i32 {