cridecoder 0.3.1__tar.gz → 0.3.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.
- {cridecoder-0.3.1 → cridecoder-0.3.2}/Cargo.lock +1 -1
- {cridecoder-0.3.1 → cridecoder-0.3.2}/Cargo.toml +1 -1
- {cridecoder-0.3.1 → cridecoder-0.3.2}/PKG-INFO +1 -1
- {cridecoder-0.3.1 → cridecoder-0.3.2}/cridecoder.pyi +37 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/pyproject.toml +1 -1
- cridecoder-0.3.2/src/acb/decode.rs +107 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/extractor.rs +3 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb.rs +5 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/lib.rs +4 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/python.rs +104 -24
- {cridecoder-0.3.1 → cridecoder-0.3.2}/tests/integration_tests.rs +30 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/copilot-instructions.md +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/dependabot.yml +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/workflows/ci.yml +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/workflows/release-crate.yml +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/.github/workflows/release-python.yml +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/.gitignore +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/AGENTS.md +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/CLAUDE.md +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/KNOWN_GAPS.md +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/LICENSE +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/README.md +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/debug_acb.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/profile_hca.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/test_acb.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/test_hca.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/examples/test_usm.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/afs.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/builder.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/consts.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/track.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/acb/utf.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/ath.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/bitreader.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/cipher.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/decoder.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/encoder.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/hca_file.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/imdct.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca/tables.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/hca.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/reader.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm/builder.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm/extractor.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm/metadata.rs +0 -0
- {cridecoder-0.3.1 → cridecoder-0.3.2}/src/usm.rs +0 -0
|
@@ -10,7 +10,9 @@ from typing import Optional
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"extract_acb",
|
|
12
12
|
"extract_acb_tracks",
|
|
13
|
+
"extract_acb_bytes",
|
|
13
14
|
"decode_acb_to_wav",
|
|
15
|
+
"decode_acb_to_wav_bytes",
|
|
14
16
|
"build_acb",
|
|
15
17
|
"build_acb_bytes",
|
|
16
18
|
"build_music_acb_bytes",
|
|
@@ -19,6 +21,7 @@ __all__ = [
|
|
|
19
21
|
"encode_hca",
|
|
20
22
|
"encode_hca_bytes",
|
|
21
23
|
"extract_usm",
|
|
24
|
+
"extract_usm_bytes",
|
|
22
25
|
"build_usm",
|
|
23
26
|
"build_usm_bytes",
|
|
24
27
|
"read_usm_metadata",
|
|
@@ -45,6 +48,16 @@ def extract_acb_tracks(
|
|
|
45
48
|
"""
|
|
46
49
|
...
|
|
47
50
|
|
|
51
|
+
def extract_acb_bytes(acb_data: bytes) -> list[dict[str, object]]:
|
|
52
|
+
"""In-memory counterpart of :func:`extract_acb_tracks` (no disk I/O).
|
|
53
|
+
|
|
54
|
+
Takes the ACB bytes directly and returns the waveform bytes per track as a
|
|
55
|
+
list of dicts ``{"name", "cue_id", "extension", "subkey", "data"}`` (``data``
|
|
56
|
+
is ``bytes``). Only the embedded AWB is read — external streaming ``.awb``
|
|
57
|
+
archives need a path, so use :func:`extract_acb` for those.
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
48
61
|
def decode_acb_to_wav(
|
|
49
62
|
acb_path: str, output_dir: str, key: Optional[int] = ...
|
|
50
63
|
) -> list[str]:
|
|
@@ -57,6 +70,17 @@ def decode_acb_to_wav(
|
|
|
57
70
|
"""
|
|
58
71
|
...
|
|
59
72
|
|
|
73
|
+
def decode_acb_to_wav_bytes(
|
|
74
|
+
acb_data: bytes, key: Optional[int] = ...
|
|
75
|
+
) -> list[dict[str, object]]:
|
|
76
|
+
"""In-memory counterpart of :func:`decode_acb_to_wav` (no disk I/O).
|
|
77
|
+
|
|
78
|
+
Returns a list of dicts ``{"name", "cue_id", "extension", "data"}`` where
|
|
79
|
+
``data`` is WAV ``bytes`` for HCA tracks (``extension == "wav"``); non-HCA
|
|
80
|
+
tracks are returned verbatim. Encrypted ACBs only need the global ``key``.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
83
|
+
|
|
60
84
|
def build_acb(tracks: list[tuple[str, int, bytes]], output_path: str) -> None:
|
|
61
85
|
"""Build an ACB file from ``(name, cue_id, hca_data)`` tuples, writing to disk."""
|
|
62
86
|
...
|
|
@@ -154,6 +178,19 @@ def extract_usm(
|
|
|
154
178
|
"""
|
|
155
179
|
...
|
|
156
180
|
|
|
181
|
+
def extract_usm_bytes(
|
|
182
|
+
usm_data: bytes,
|
|
183
|
+
key: Optional[int] = ...,
|
|
184
|
+
export_audio: bool = ...,
|
|
185
|
+
) -> list[dict[str, object]]:
|
|
186
|
+
"""In-memory counterpart of :func:`extract_usm` (no disk I/O).
|
|
187
|
+
|
|
188
|
+
Takes the USM bytes and returns each stream as a dict
|
|
189
|
+
``{"name", "extension", "data"}`` (``data`` is ``bytes``) — video, plus
|
|
190
|
+
audio when ``export_audio`` is set.
|
|
191
|
+
"""
|
|
192
|
+
...
|
|
193
|
+
|
|
157
194
|
def build_usm(
|
|
158
195
|
name: str,
|
|
159
196
|
video_data: bytes,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//! High-level ACB → WAV decoding.
|
|
2
|
+
//!
|
|
3
|
+
//! Composes ACB extraction with the HCA decoder so callers can go straight from
|
|
4
|
+
//! an ACB to decoded audio without managing the intermediate HCA bytes
|
|
5
|
+
//! themselves. The per-AWB AFS2 subkey is applied automatically.
|
|
6
|
+
|
|
7
|
+
use std::fs::{self, File};
|
|
8
|
+
use std::io::{Cursor, Read, Seek};
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
|
|
11
|
+
use thiserror::Error;
|
|
12
|
+
|
|
13
|
+
use crate::acb::extractor::{extract_acb_to_memory, ExtractError};
|
|
14
|
+
use crate::hca::{HcaDecoder, HcaDecoderError};
|
|
15
|
+
|
|
16
|
+
/// A decoded ACB track held in memory.
|
|
17
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
18
|
+
pub struct DecodedAcbTrack {
|
|
19
|
+
/// Cue name of the track (also the output file stem).
|
|
20
|
+
pub name: String,
|
|
21
|
+
/// Cue id of the track (cue-table index).
|
|
22
|
+
pub cue_id: i32,
|
|
23
|
+
/// Output extension: `"wav"` for a decoded HCA track, otherwise the
|
|
24
|
+
/// original waveform extension (non-HCA tracks are passed through raw).
|
|
25
|
+
pub extension: String,
|
|
26
|
+
/// Decoded WAV bytes (HCA), or the raw waveform bytes (non-HCA).
|
|
27
|
+
pub data: Vec<u8>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Error, Debug)]
|
|
31
|
+
pub enum DecodeAcbError {
|
|
32
|
+
#[error("ACB extraction failed: {0}")]
|
|
33
|
+
Extract(#[from] ExtractError),
|
|
34
|
+
#[error("HCA decode failed: {0}")]
|
|
35
|
+
Hca(#[from] HcaDecoderError),
|
|
36
|
+
#[error("IO error: {0}")]
|
|
37
|
+
Io(#[from] std::io::Error),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Extract an ACB and decode its HCA tracks to WAV, returning everything in
|
|
41
|
+
/// memory. The per-AWB AFS2 subkey is applied automatically, so encrypted
|
|
42
|
+
/// (type-56) ACBs only need the global `key`. Non-HCA tracks are returned
|
|
43
|
+
/// verbatim with their original extension.
|
|
44
|
+
pub fn decode_acb_to_wav_to_memory<R: Read + Seek>(
|
|
45
|
+
acb_file: R,
|
|
46
|
+
acb_file_path: Option<&Path>,
|
|
47
|
+
key: Option<u64>,
|
|
48
|
+
) -> Result<Vec<DecodedAcbTrack>, DecodeAcbError> {
|
|
49
|
+
let tracks = extract_acb_to_memory(acb_file, acb_file_path)?;
|
|
50
|
+
|
|
51
|
+
let mut outputs = Vec::with_capacity(tracks.len());
|
|
52
|
+
for track in tracks {
|
|
53
|
+
if track.extension == "hca" {
|
|
54
|
+
let mut decoder = HcaDecoder::from_reader(Cursor::new(track.data))?;
|
|
55
|
+
if let Some(k) = key {
|
|
56
|
+
decoder.set_encryption_key(k, track.subkey as u64);
|
|
57
|
+
}
|
|
58
|
+
let mut wav = Vec::new();
|
|
59
|
+
decoder.decode_to_wav(&mut wav)?;
|
|
60
|
+
outputs.push(DecodedAcbTrack {
|
|
61
|
+
name: track.name,
|
|
62
|
+
cue_id: track.cue_id,
|
|
63
|
+
extension: "wav".to_string(),
|
|
64
|
+
data: wav,
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
outputs.push(DecodedAcbTrack {
|
|
68
|
+
name: track.name,
|
|
69
|
+
cue_id: track.cue_id,
|
|
70
|
+
extension: track.extension,
|
|
71
|
+
data: track.data,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
Ok(outputs)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Like [`decode_acb_to_wav_to_memory`], but writes each decoded track to
|
|
79
|
+
/// `target_dir` and returns the written paths.
|
|
80
|
+
pub fn decode_acb_to_wav<R: Read + Seek>(
|
|
81
|
+
acb_file: R,
|
|
82
|
+
target_dir: &Path,
|
|
83
|
+
acb_file_path: Option<&Path>,
|
|
84
|
+
key: Option<u64>,
|
|
85
|
+
) -> Result<Vec<String>, DecodeAcbError> {
|
|
86
|
+
let tracks = decode_acb_to_wav_to_memory(acb_file, acb_file_path, key)?;
|
|
87
|
+
|
|
88
|
+
fs::create_dir_all(target_dir)?;
|
|
89
|
+
let mut outputs = Vec::with_capacity(tracks.len());
|
|
90
|
+
for track in tracks {
|
|
91
|
+
let path = target_dir.join(format!("{}.{}", track.name, track.extension));
|
|
92
|
+
fs::write(&path, &track.data)?;
|
|
93
|
+
outputs.push(path.to_string_lossy().into_owned());
|
|
94
|
+
}
|
|
95
|
+
Ok(outputs)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Convenience wrapper over [`decode_acb_to_wav`] that reads from a file path
|
|
99
|
+
/// (also used to resolve sibling external streaming `.awb` archives).
|
|
100
|
+
pub fn decode_acb_to_wav_from_file(
|
|
101
|
+
acb_path: &Path,
|
|
102
|
+
target_dir: &Path,
|
|
103
|
+
key: Option<u64>,
|
|
104
|
+
) -> Result<Vec<String>, DecodeAcbError> {
|
|
105
|
+
let file = File::open(acb_path)?;
|
|
106
|
+
decode_acb_to_wav(file, target_dir, Some(acb_path), key)
|
|
107
|
+
}
|
|
@@ -12,6 +12,8 @@ use thiserror::Error;
|
|
|
12
12
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
13
13
|
pub struct ExtractedAcbTrack {
|
|
14
14
|
pub name: String,
|
|
15
|
+
/// Cue id of the track (cue-table index).
|
|
16
|
+
pub cue_id: i32,
|
|
15
17
|
pub extension: String,
|
|
16
18
|
pub data: Vec<u8>,
|
|
17
19
|
/// AFS2 subkey of the AWB this waveform came from. Required (together with the
|
|
@@ -79,6 +81,7 @@ pub fn extract_acb_to_memory<R: Read + Seek>(
|
|
|
79
81
|
};
|
|
80
82
|
outputs.push(ExtractedAcbTrack {
|
|
81
83
|
name: track.name.clone(),
|
|
84
|
+
cue_id: track.cue_id,
|
|
82
85
|
extension,
|
|
83
86
|
data,
|
|
84
87
|
subkey,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
mod afs;
|
|
4
4
|
mod builder;
|
|
5
5
|
mod consts;
|
|
6
|
+
mod decode;
|
|
6
7
|
mod extractor;
|
|
7
8
|
mod track;
|
|
8
9
|
mod utf;
|
|
@@ -12,6 +13,10 @@ pub use builder::{
|
|
|
12
13
|
AcbBuilder, AfsArchiveBuilder, BuilderError, ColumnDef, TrackInput, UtfTableBuilder,
|
|
13
14
|
};
|
|
14
15
|
pub use consts::*;
|
|
16
|
+
pub use decode::{
|
|
17
|
+
decode_acb_to_wav, decode_acb_to_wav_from_file, decode_acb_to_wav_to_memory, DecodeAcbError,
|
|
18
|
+
DecodedAcbTrack,
|
|
19
|
+
};
|
|
15
20
|
pub use extractor::{
|
|
16
21
|
extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
|
|
17
22
|
extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
|
|
@@ -14,6 +14,10 @@ pub mod usm;
|
|
|
14
14
|
mod python;
|
|
15
15
|
|
|
16
16
|
// ACB/AWB exports
|
|
17
|
+
pub use acb::{
|
|
18
|
+
decode_acb_to_wav, decode_acb_to_wav_from_file, decode_acb_to_wav_to_memory, DecodeAcbError,
|
|
19
|
+
DecodedAcbTrack,
|
|
20
|
+
};
|
|
17
21
|
pub use acb::{
|
|
18
22
|
extract_acb, extract_acb_from_file, extract_acb_to_memory, extract_acb_tracks,
|
|
19
23
|
extract_acb_tracks_from_file, ExtractedAcbTrack, ExtractedTrackFile,
|
|
@@ -101,35 +101,76 @@ fn decode_acb_to_wav(acb_path: &str, output_dir: &str, key: Option<u64>) -> PyRe
|
|
|
101
101
|
fs::create_dir_all(out_dir)
|
|
102
102
|
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create output dir: {}", e)))?;
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
.map_err(|e| PyRuntimeError::new_err(format!("
|
|
104
|
+
acb::decode_acb_to_wav_from_file(Path::new(acb_path), out_dir, key)
|
|
105
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB decode failed: {}", e)))
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
/// Extract audio tracks from in-memory ACB bytes (no disk I/O).
|
|
109
|
+
///
|
|
110
|
+
/// The in-memory counterpart of :func:`extract_acb_tracks`: takes the ACB
|
|
111
|
+
/// bytes directly and returns the waveform bytes per track instead of writing
|
|
112
|
+
/// files. Only the embedded AWB is read — external streaming ``.awb`` archives
|
|
113
|
+
/// can't be resolved without a path, so use :func:`extract_acb` for those.
|
|
114
|
+
///
|
|
115
|
+
/// Args:
|
|
116
|
+
/// acb_data: Raw ACB file bytes
|
|
117
|
+
///
|
|
118
|
+
/// Returns:
|
|
119
|
+
/// List of dicts ``{"name", "cue_id", "extension", "subkey", "data"}``.
|
|
120
|
+
#[pyfunction]
|
|
121
|
+
fn extract_acb_bytes<'py>(
|
|
122
|
+
py: Python<'py>,
|
|
123
|
+
acb_data: &[u8],
|
|
124
|
+
) -> PyResult<Vec<Bound<'py, pyo3::types::PyDict>>> {
|
|
125
|
+
let tracks = acb::extract_acb_to_memory(Cursor::new(acb_data.to_vec()), None)
|
|
108
126
|
.map_err(|e| PyRuntimeError::new_err(format!("ACB extraction failed: {}", e)))?;
|
|
109
127
|
|
|
110
|
-
let mut
|
|
128
|
+
let mut out = Vec::with_capacity(tracks.len());
|
|
111
129
|
for track in tracks {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
131
|
+
dict.set_item("name", track.name)?;
|
|
132
|
+
dict.set_item("cue_id", track.cue_id)?;
|
|
133
|
+
dict.set_item("extension", track.extension)?;
|
|
134
|
+
dict.set_item("subkey", track.subkey)?;
|
|
135
|
+
dict.set_item("data", pyo3::types::PyBytes::new(py, &track.data))?;
|
|
136
|
+
out.push(dict);
|
|
137
|
+
}
|
|
138
|
+
Ok(out)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Decode an in-memory ACB straight to WAV bytes (no disk I/O).
|
|
142
|
+
///
|
|
143
|
+
/// The in-memory counterpart of :func:`decode_acb_to_wav`: each AWB's subkey is
|
|
144
|
+
/// applied automatically, so encrypted ACBs only need the global ``key``.
|
|
145
|
+
/// Non-HCA tracks are returned verbatim (``extension`` reflects this).
|
|
146
|
+
///
|
|
147
|
+
/// Args:
|
|
148
|
+
/// acb_data: Raw ACB file bytes
|
|
149
|
+
/// key: Global HCA keycode (omit/None for unencrypted ACBs)
|
|
150
|
+
///
|
|
151
|
+
/// Returns:
|
|
152
|
+
/// List of dicts ``{"name", "cue_id", "extension", "data"}`` where ``data``
|
|
153
|
+
/// is WAV bytes for HCA tracks (``extension == "wav"``).
|
|
154
|
+
#[pyfunction]
|
|
155
|
+
#[pyo3(signature = (acb_data, key=None))]
|
|
156
|
+
fn decode_acb_to_wav_bytes<'py>(
|
|
157
|
+
py: Python<'py>,
|
|
158
|
+
acb_data: &[u8],
|
|
159
|
+
key: Option<u64>,
|
|
160
|
+
) -> PyResult<Vec<Bound<'py, pyo3::types::PyDict>>> {
|
|
161
|
+
let tracks = acb::decode_acb_to_wav_to_memory(Cursor::new(acb_data.to_vec()), None, key)
|
|
162
|
+
.map_err(|e| PyRuntimeError::new_err(format!("ACB decode failed: {}", e)))?;
|
|
163
|
+
|
|
164
|
+
let mut out = Vec::with_capacity(tracks.len());
|
|
165
|
+
for track in tracks {
|
|
166
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
167
|
+
dict.set_item("name", track.name)?;
|
|
168
|
+
dict.set_item("cue_id", track.cue_id)?;
|
|
169
|
+
dict.set_item("extension", track.extension)?;
|
|
170
|
+
dict.set_item("data", pyo3::types::PyBytes::new(py, &track.data))?;
|
|
171
|
+
out.push(dict);
|
|
131
172
|
}
|
|
132
|
-
Ok(
|
|
173
|
+
Ok(out)
|
|
133
174
|
}
|
|
134
175
|
|
|
135
176
|
/// Build an ACB file from track data.
|
|
@@ -525,6 +566,42 @@ fn extract_usm(
|
|
|
525
566
|
.collect())
|
|
526
567
|
}
|
|
527
568
|
|
|
569
|
+
/// Extract USM streams from in-memory bytes (no disk I/O).
|
|
570
|
+
///
|
|
571
|
+
/// The in-memory counterpart of :func:`extract_usm`: takes the USM bytes and
|
|
572
|
+
/// returns each stream's bytes instead of writing files.
|
|
573
|
+
///
|
|
574
|
+
/// Args:
|
|
575
|
+
/// usm_data: Raw USM file bytes
|
|
576
|
+
/// key: Optional decryption key (u64)
|
|
577
|
+
/// export_audio: Whether to include audio streams (default: false)
|
|
578
|
+
///
|
|
579
|
+
/// Returns:
|
|
580
|
+
/// List of dicts ``{"name", "extension", "data"}`` (video, and audio when
|
|
581
|
+
/// ``export_audio`` is set).
|
|
582
|
+
#[pyfunction]
|
|
583
|
+
#[pyo3(signature = (usm_data, key=None, export_audio=false))]
|
|
584
|
+
fn extract_usm_bytes<'py>(
|
|
585
|
+
py: Python<'py>,
|
|
586
|
+
usm_data: &[u8],
|
|
587
|
+
key: Option<u64>,
|
|
588
|
+
export_audio: bool,
|
|
589
|
+
) -> PyResult<Vec<Bound<'py, pyo3::types::PyDict>>> {
|
|
590
|
+
let streams =
|
|
591
|
+
usm::extract_usm_to_memory(Cursor::new(usm_data.to_vec()), b"", key, export_audio)
|
|
592
|
+
.map_err(|e| PyRuntimeError::new_err(format!("USM extraction failed: {}", e)))?;
|
|
593
|
+
|
|
594
|
+
let mut out = Vec::with_capacity(streams.len());
|
|
595
|
+
for stream in streams {
|
|
596
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
597
|
+
dict.set_item("name", stream.name)?;
|
|
598
|
+
dict.set_item("extension", stream.extension)?;
|
|
599
|
+
dict.set_item("data", pyo3::types::PyBytes::new(py, &stream.data))?;
|
|
600
|
+
out.push(dict);
|
|
601
|
+
}
|
|
602
|
+
Ok(out)
|
|
603
|
+
}
|
|
604
|
+
|
|
528
605
|
/// Build a USM file from video data.
|
|
529
606
|
///
|
|
530
607
|
/// Args:
|
|
@@ -611,7 +688,9 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
611
688
|
// ACB functions
|
|
612
689
|
m.add_function(wrap_pyfunction!(extract_acb, m)?)?;
|
|
613
690
|
m.add_function(wrap_pyfunction!(extract_acb_tracks, m)?)?;
|
|
691
|
+
m.add_function(wrap_pyfunction!(extract_acb_bytes, m)?)?;
|
|
614
692
|
m.add_function(wrap_pyfunction!(decode_acb_to_wav, m)?)?;
|
|
693
|
+
m.add_function(wrap_pyfunction!(decode_acb_to_wav_bytes, m)?)?;
|
|
615
694
|
m.add_function(wrap_pyfunction!(build_acb, m)?)?;
|
|
616
695
|
m.add_function(wrap_pyfunction!(build_acb_bytes, m)?)?;
|
|
617
696
|
m.add_function(wrap_pyfunction!(build_music_acb_bytes, m)?)?;
|
|
@@ -624,6 +703,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
624
703
|
|
|
625
704
|
// USM functions
|
|
626
705
|
m.add_function(wrap_pyfunction!(extract_usm, m)?)?;
|
|
706
|
+
m.add_function(wrap_pyfunction!(extract_usm_bytes, m)?)?;
|
|
627
707
|
m.add_function(wrap_pyfunction!(build_usm, m)?)?;
|
|
628
708
|
m.add_function(wrap_pyfunction!(build_usm_bytes, m)?)?;
|
|
629
709
|
m.add_function(wrap_pyfunction!(read_usm_metadata, m)?)?;
|
|
@@ -596,6 +596,36 @@ fn test_acb_extract_tracks_metadata() {
|
|
|
596
596
|
assert_eq!(&data[0..4], b"HCA\x00");
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
+
/// Test extract_acb_to_memory returns track bytes with cue id and subkey
|
|
600
|
+
#[test]
|
|
601
|
+
fn test_acb_extract_to_memory() {
|
|
602
|
+
use cridecoder::{extract_acb_to_memory, AcbBuilder, TrackInput};
|
|
603
|
+
use std::io::Cursor;
|
|
604
|
+
|
|
605
|
+
let mut builder = AcbBuilder::new();
|
|
606
|
+
builder.add_track(TrackInput::new("mem_a", 0, create_minimal_hca_header()));
|
|
607
|
+
builder.add_track(TrackInput::new("mem_b", 1, create_minimal_hca_header()));
|
|
608
|
+
|
|
609
|
+
let mut output = Vec::new();
|
|
610
|
+
builder
|
|
611
|
+
.build(&mut Cursor::new(&mut output), None)
|
|
612
|
+
.expect("ACB build should succeed");
|
|
613
|
+
|
|
614
|
+
let tracks =
|
|
615
|
+
extract_acb_to_memory(Cursor::new(output), None).expect("in-memory extract should succeed");
|
|
616
|
+
|
|
617
|
+
assert_eq!(tracks.len(), 2);
|
|
618
|
+
assert_eq!(tracks[0].name, "mem_a");
|
|
619
|
+
assert_eq!(tracks[0].cue_id, 0);
|
|
620
|
+
assert_eq!(tracks[1].name, "mem_b");
|
|
621
|
+
assert_eq!(tracks[1].cue_id, 1);
|
|
622
|
+
for track in &tracks {
|
|
623
|
+
assert_eq!(track.extension, "hca");
|
|
624
|
+
assert_eq!(track.subkey, 0); // builder produces unencrypted AWBs
|
|
625
|
+
assert_eq!(&track.data[0..4], b"HCA\x00");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
599
629
|
/// Test ACB builder keeps Waveform AWB ids aligned with non-zero cue ids
|
|
600
630
|
#[test]
|
|
601
631
|
fn test_acb_builder_nonzero_cue_id() {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|