PyCriCodecsEx 0.0.2__cp312-cp312-musllinux_1_2_i686.whl → 0.0.3__cp312-cp312-musllinux_1_2_i686.whl
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.
- PyCriCodecsEx/__init__.py +1 -1
- PyCriCodecsEx/acb.py +21 -10
- PyCriCodecsEx/adx.py +143 -4
- PyCriCodecsEx/awb.py +1 -0
- PyCriCodecsEx/chunk.py +0 -5
- PyCriCodecsEx/cpk.py +16 -12
- PyCriCodecsEx/hca.py +167 -27
- PyCriCodecsEx/usm.py +18 -258
- PyCriCodecsEx/utf.py +8 -24
- pycricodecsex-0.0.3.dist-info/METADATA +35 -0
- pycricodecsex-0.0.3.dist-info/RECORD +17 -0
- pycricodecsex-0.0.2.dist-info/METADATA +0 -86
- pycricodecsex-0.0.2.dist-info/RECORD +0 -17
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.3.dist-info}/WHEEL +0 -0
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.3.dist-info}/top_level.txt +0 -0
PyCriCodecsEx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.3"
|
PyCriCodecsEx/acb.py
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
# - Original work by https://github.com/Youjose/PyCriCodecs
|
|
4
4
|
# See Research/ACBSchema.py for more details.
|
|
5
5
|
|
|
6
|
-
from typing import Generator, List, Tuple
|
|
6
|
+
from typing import Generator, List, Tuple, BinaryIO
|
|
7
7
|
from PyCriCodecsEx.chunk import *
|
|
8
8
|
from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
|
|
9
|
-
from PyCriCodecsEx.
|
|
9
|
+
from PyCriCodecsEx.hca import HCACodec
|
|
10
|
+
from PyCriCodecsEx.adx import ADXCodec
|
|
10
11
|
from PyCriCodecsEx.awb import AWB, AWBBuilder
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from copy import deepcopy
|
|
@@ -136,9 +137,14 @@ class CueItem:
|
|
|
136
137
|
Waveforms: list[int] # List of waveform IDs
|
|
137
138
|
|
|
138
139
|
class ACB(UTF):
|
|
139
|
-
"""
|
|
140
|
-
def __init__(self,
|
|
141
|
-
|
|
140
|
+
"""Use this class to read, and modify ACB files in memory."""
|
|
141
|
+
def __init__(self, stream : str | BinaryIO) -> None:
|
|
142
|
+
"""Loads an ACB file from the given stream.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
stream (str | BinaryIO): The path to the ACB file or a BinaryIO stream containing the ACB data.
|
|
146
|
+
"""
|
|
147
|
+
super().__init__(stream, recursive=True)
|
|
142
148
|
|
|
143
149
|
@property
|
|
144
150
|
def payload(self) -> dict:
|
|
@@ -239,17 +245,22 @@ class ACB(UTF):
|
|
|
239
245
|
yield CueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
|
|
240
246
|
|
|
241
247
|
class ACBBuilder:
|
|
248
|
+
"""Use this class to build ACB files from an existing ACB object."""
|
|
242
249
|
acb: ACB
|
|
243
250
|
|
|
244
251
|
def __init__(self, acb: ACB) -> None:
|
|
245
252
|
"""Initializes the ACBBuilder with an existing ACB object.
|
|
246
253
|
|
|
254
|
+
Args:
|
|
255
|
+
acb (ACB): The ACB object to build from.
|
|
256
|
+
|
|
247
257
|
Building ACB from scratch isn't planned for now since:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
|
|
259
|
+
* We don't know how SeqCommandTable TLVs work. This is the biggest issue.
|
|
260
|
+
* Many fields are unknown or not well understood
|
|
261
|
+
- Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
|
|
262
|
+
with their own ACF table. Missing these is not a fun debugging experience.
|
|
263
|
+
* ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
|
|
253
264
|
|
|
254
265
|
Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
|
|
255
266
|
"""
|
PyCriCodecsEx/adx.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
from typing import BinaryIO
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from PyCriCodecsEx.chunk import *
|
|
1
4
|
import CriCodecsEx
|
|
2
|
-
|
|
3
5
|
class ADX:
|
|
4
|
-
"""ADX
|
|
5
|
-
|
|
6
|
+
"""ADX class for decoding and encoding ADX files, pass the either `adx file` or `wav file` in bytes to either `decode` or `encode` respectively.
|
|
7
|
+
|
|
8
|
+
**NOTE:** Direct usage of this class is not recommended, use the `ADXCodec` wrapper instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
6
11
|
# Decodes ADX to WAV.
|
|
7
12
|
@staticmethod
|
|
8
13
|
def decode(data: bytes) -> bytes:
|
|
@@ -13,4 +18,138 @@ class ADX:
|
|
|
13
18
|
@staticmethod
|
|
14
19
|
def encode(data: bytes, BitDepth = 0x4, Blocksize = 0x12, Encoding = 3, AdxVersion = 0x4, Highpass_Frequency = 0x1F4, Filter = 0, force_not_looping = False) -> bytes:
|
|
15
20
|
""" Encodes WAV to ADX. """
|
|
16
|
-
return CriCodecsEx.AdxEncode(bytes(data), BitDepth, Blocksize, Encoding, Highpass_Frequency, Filter, AdxVersion, force_not_looping)
|
|
21
|
+
return CriCodecsEx.AdxEncode(bytes(data), BitDepth, Blocksize, Encoding, Highpass_Frequency, Filter, AdxVersion, force_not_looping)
|
|
22
|
+
|
|
23
|
+
class ADXCodec(ADX):
|
|
24
|
+
"""Use this class for encoding and decoding ADX files, from and to WAV."""
|
|
25
|
+
|
|
26
|
+
CHUNK_INTERVAL = 99.9
|
|
27
|
+
BASE_FRAMERATE = 2997
|
|
28
|
+
# TODO: Move these to an enum
|
|
29
|
+
AUDIO_CODEC = 2
|
|
30
|
+
METADATA_COUNT = 0
|
|
31
|
+
|
|
32
|
+
filename : str
|
|
33
|
+
filesize : int
|
|
34
|
+
|
|
35
|
+
adx : bytes
|
|
36
|
+
header : bytes
|
|
37
|
+
sfaStream: BinaryIO
|
|
38
|
+
|
|
39
|
+
AdxDataOffset: int
|
|
40
|
+
AdxEncoding: int
|
|
41
|
+
AdxBlocksize: int
|
|
42
|
+
AdxSampleBitdepth: int
|
|
43
|
+
AdxChannelCount: int
|
|
44
|
+
AdxSamplingRate: int
|
|
45
|
+
AdxSampleCount: int
|
|
46
|
+
AdxHighpassFrequency: int
|
|
47
|
+
AdxVersion: int
|
|
48
|
+
AdxFlags: int
|
|
49
|
+
|
|
50
|
+
chnls: int
|
|
51
|
+
sampling_rate: int
|
|
52
|
+
total_samples: int
|
|
53
|
+
avbps: int
|
|
54
|
+
|
|
55
|
+
def __init__(self, stream: str | bytes, filename: str = "default.adx", bitdepth: int = 4, **kwargs):
|
|
56
|
+
"""Initializes the ADX encoder/decoder
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
stream (str | bytes): Path to the ADX or WAV file, or a BinaryIO stream. WAV files will be automatically encoded with the given settings first.
|
|
60
|
+
filename (str, optional): Output filename. Defaults to "default.adx".
|
|
61
|
+
bitdepth (int, optional): Audio bit depth within [2,15]. Defaults to 4.
|
|
62
|
+
"""
|
|
63
|
+
if type(stream) == str:
|
|
64
|
+
self.adx = open(stream, "rb").read()
|
|
65
|
+
else:
|
|
66
|
+
self.adx = stream
|
|
67
|
+
self.filename = filename
|
|
68
|
+
self.filesize = len(self.adx)
|
|
69
|
+
magic = self.adx[:4]
|
|
70
|
+
if magic == b"RIFF":
|
|
71
|
+
self.adx = self.encode(self.adx, bitdepth, force_not_looping=True)
|
|
72
|
+
self.sfaStream = BytesIO(self.adx)
|
|
73
|
+
header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
|
|
74
|
+
FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
|
|
75
|
+
assert FourCC == 0x8000, "either ADX or WAV is supported"
|
|
76
|
+
assert self.AdxVersion in {3,4}, "unsupported ADX version"
|
|
77
|
+
if self.AdxVersion == 4:
|
|
78
|
+
self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
|
|
79
|
+
self.sfaStream.seek(0)
|
|
80
|
+
self.chnls = self.AdxChannelCount
|
|
81
|
+
self.sampling_rate = self.AdxSamplingRate
|
|
82
|
+
self.total_samples = self.AdxSampleCount
|
|
83
|
+
self.avbps = int(self.filesize * 8 * self.chnls) - self.filesize
|
|
84
|
+
|
|
85
|
+
def generate_SFA(self, index: int, builder):
|
|
86
|
+
# USMBuilder usage
|
|
87
|
+
current_interval = 0
|
|
88
|
+
stream_size = len(self.adx) - self.AdxBlocksize
|
|
89
|
+
chunk_size = int(self.AdxSamplingRate // (self.BASE_FRAMERATE / 100) // 32) * (self.AdxBlocksize * self.AdxChannelCount)
|
|
90
|
+
self.sfaStream.seek(0)
|
|
91
|
+
res = []
|
|
92
|
+
while self.sfaStream.tell() < stream_size:
|
|
93
|
+
if self.sfaStream.tell() > 0:
|
|
94
|
+
if self.sfaStream.tell() + chunk_size < stream_size:
|
|
95
|
+
datalen = chunk_size
|
|
96
|
+
else:
|
|
97
|
+
datalen = (stream_size - (self.AdxDataOffset + 4) - chunk_size) % chunk_size
|
|
98
|
+
else:
|
|
99
|
+
datalen = self.AdxDataOffset + 4
|
|
100
|
+
if not datalen:
|
|
101
|
+
break
|
|
102
|
+
padding = (0x20 - (datalen % 0x20) if datalen % 0x20 != 0 else 0)
|
|
103
|
+
SFA_chunk = USMChunkHeader.pack(
|
|
104
|
+
USMChunckHeaderType.SFA.value,
|
|
105
|
+
datalen + 0x18 + padding,
|
|
106
|
+
0,
|
|
107
|
+
0x18,
|
|
108
|
+
padding,
|
|
109
|
+
index,
|
|
110
|
+
0,
|
|
111
|
+
0,
|
|
112
|
+
0,
|
|
113
|
+
round(current_interval),
|
|
114
|
+
self.BASE_FRAMERATE,
|
|
115
|
+
0,
|
|
116
|
+
0
|
|
117
|
+
)
|
|
118
|
+
chunk_data = self.sfaStream.read(datalen)
|
|
119
|
+
if builder.encrypt_audio:
|
|
120
|
+
SFA_chunk = builder.AudioMask(chunk_data)
|
|
121
|
+
SFA_chunk += chunk_data.ljust(datalen + padding, b"\x00")
|
|
122
|
+
current_interval += self.CHUNK_INTERVAL
|
|
123
|
+
res.append(SFA_chunk)
|
|
124
|
+
# ---
|
|
125
|
+
SFA_chunk = USMChunkHeader.pack(
|
|
126
|
+
USMChunckHeaderType.SFA.value,
|
|
127
|
+
0x38,
|
|
128
|
+
0,
|
|
129
|
+
0x18,
|
|
130
|
+
0,
|
|
131
|
+
index,
|
|
132
|
+
0,
|
|
133
|
+
0,
|
|
134
|
+
2,
|
|
135
|
+
0,
|
|
136
|
+
30,
|
|
137
|
+
0,
|
|
138
|
+
0
|
|
139
|
+
)
|
|
140
|
+
SFA_chunk += b"#CONTENTS END ===============\x00"
|
|
141
|
+
res[-1] += SFA_chunk
|
|
142
|
+
return res
|
|
143
|
+
|
|
144
|
+
def get_metadata(self):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def get_encoded(self):
|
|
148
|
+
"""Gets the encoded ADX audio data."""
|
|
149
|
+
return self.adx
|
|
150
|
+
|
|
151
|
+
def save(self, filepath: str):
|
|
152
|
+
"""Saves the encoded ADX audio to filepath"""
|
|
153
|
+
with open(filepath, "wb") as f:
|
|
154
|
+
f.write(self.decode(self.adx))
|
|
155
|
+
|
PyCriCodecsEx/awb.py
CHANGED
|
@@ -82,6 +82,7 @@ class AWB:
|
|
|
82
82
|
raise ValueError("Unknown int size.")
|
|
83
83
|
|
|
84
84
|
class AWBBuilder:
|
|
85
|
+
"""Use this class to build AWB files from a list of bytes."""
|
|
85
86
|
def __init__(self, infiles: list[bytes], subkey: int = 0, version: int = 2, id_intsize = 0x2, align: int = 0x20) -> None:
|
|
86
87
|
"""Initializes the AWB builder.
|
|
87
88
|
|
PyCriCodecsEx/chunk.py
CHANGED
|
@@ -46,11 +46,6 @@ class HCAType(Enum):
|
|
|
46
46
|
HCA = b"HCA\x00" # Header.
|
|
47
47
|
EHCA = b"\xC8\xC3\xC1\x00" # Encrypted HCA header.
|
|
48
48
|
|
|
49
|
-
class VideoType(Enum):
|
|
50
|
-
IVF = b"DKIF" # Header.
|
|
51
|
-
# H264 = b"" # Header.
|
|
52
|
-
# MPEG = b"" # Header.
|
|
53
|
-
|
|
54
49
|
# I saw some devs swap the unsigned/signed indexes. So I am not sure what's correct or not.
|
|
55
50
|
# In my own experience, swapping those results in an incorrect signed values (should be unsigned) in ACB's/CPK's.
|
|
56
51
|
# If someone were to change this, they must change 'stringtypes' function in UTF/UTFBuilder classes.
|
PyCriCodecsEx/cpk.py
CHANGED
|
@@ -48,6 +48,7 @@ class _TOC():
|
|
|
48
48
|
self.table = UTF(self.stream.read()).table
|
|
49
49
|
|
|
50
50
|
class CPK:
|
|
51
|
+
"""Use this class to load CPK file table-of-content, and read files from them on-demand."""
|
|
51
52
|
magic: bytes
|
|
52
53
|
encflag: int
|
|
53
54
|
packet_size: int
|
|
@@ -189,7 +190,7 @@ class CPKBuilder:
|
|
|
189
190
|
ContentSize: int
|
|
190
191
|
EnabledDataSize: int
|
|
191
192
|
EnabledPackedSize: int
|
|
192
|
-
outfile:
|
|
193
|
+
outfile: BinaryIO
|
|
193
194
|
init_toc_len: int # This is a bit of a redundancy, but some CPK's need it.
|
|
194
195
|
|
|
195
196
|
in_files : list[tuple[str, str, bool]] # (source path, dest filename, compress or not)
|
|
@@ -254,14 +255,13 @@ class CPKBuilder:
|
|
|
254
255
|
|
|
255
256
|
self.in_files.append((src, dst, compress))
|
|
256
257
|
|
|
257
|
-
def _writetofile(self, header) -> None:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
self.progress_cb("Write %s" % os.path.basename(filename), i + 1, len(self.files))
|
|
258
|
+
def _writetofile(self, header) -> None:
|
|
259
|
+
self.outfile.write(header)
|
|
260
|
+
for i, ((path, _), (filename, file_size, pack_size)) in enumerate(zip(self.os_files, self.files)):
|
|
261
|
+
src = open(path, 'rb').read()
|
|
262
|
+
self.outfile.write(src)
|
|
263
|
+
self.outfile.write(bytes(0x800 - pack_size % 0x800))
|
|
264
|
+
self.progress_cb("Write %s" % os.path.basename(filename), i + 1, len(self.files))
|
|
265
265
|
|
|
266
266
|
def _populate_files(self, parallel : bool):
|
|
267
267
|
self.files = []
|
|
@@ -304,12 +304,12 @@ class CPKBuilder:
|
|
|
304
304
|
pass
|
|
305
305
|
self.os_files = []
|
|
306
306
|
|
|
307
|
-
def save(self, outfile : str, parallel : bool = False):
|
|
307
|
+
def save(self, outfile : str | BinaryIO, parallel : bool = False):
|
|
308
308
|
"""Build and save the bundle into a file
|
|
309
309
|
|
|
310
310
|
|
|
311
311
|
Args:
|
|
312
|
-
outfile (str): The output file path.
|
|
312
|
+
outfile (str | BinaryIO): The output file path or a writable binary stream.
|
|
313
313
|
parallel (bool, optional): Whether to use parallel processing for file compression (if at all used). Defaults to False.
|
|
314
314
|
|
|
315
315
|
NOTE:
|
|
@@ -318,6 +318,8 @@ class CPKBuilder:
|
|
|
318
318
|
"""
|
|
319
319
|
assert self.in_files, "cannot save empty bundle"
|
|
320
320
|
self.outfile = outfile
|
|
321
|
+
if type(outfile) == str:
|
|
322
|
+
self.outfile = open(outfile, "wb")
|
|
321
323
|
self._populate_files(parallel)
|
|
322
324
|
if self.encrypt:
|
|
323
325
|
encflag = 0
|
|
@@ -363,7 +365,9 @@ class CPKBuilder:
|
|
|
363
365
|
data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.ITOCdata
|
|
364
366
|
self._writetofile(data)
|
|
365
367
|
self._cleanup_files()
|
|
366
|
-
|
|
368
|
+
if type(outfile) == str:
|
|
369
|
+
self.outfile.close()
|
|
370
|
+
|
|
367
371
|
def _generate_GTOC(self) -> bytearray:
|
|
368
372
|
# NOTE: Practically useless
|
|
369
373
|
# I have no idea why are those numbers here.
|
PyCriCodecsEx/hca.py
CHANGED
|
@@ -5,7 +5,7 @@ from array import array
|
|
|
5
5
|
import CriCodecsEx
|
|
6
6
|
|
|
7
7
|
from PyCriCodecsEx.chunk import *
|
|
8
|
-
|
|
8
|
+
from PyCriCodecsEx.utf import UTFTypeValues, UTFBuilder
|
|
9
9
|
HcaHeaderStruct = Struct(">4sHH")
|
|
10
10
|
HcaFmtHeaderStruct = Struct(">4sIIHH")
|
|
11
11
|
HcaCompHeaderStruct = Struct(">4sHBBBBBBBBBB")
|
|
@@ -17,6 +17,10 @@ HcaCiphHeaderStruct = Struct(">4sH")
|
|
|
17
17
|
HcaRvaHeaderStruct = Struct(">4sf")
|
|
18
18
|
|
|
19
19
|
class HCA:
|
|
20
|
+
"""HCA class for decoding and encoding HCA files
|
|
21
|
+
|
|
22
|
+
**NOTE:** Direct usage of this class is not recommended, use the `HCACodec` wrapper instead.
|
|
23
|
+
"""
|
|
20
24
|
stream: BinaryIO
|
|
21
25
|
hcastream: BinaryIO
|
|
22
26
|
HcaSig: bytes
|
|
@@ -205,32 +209,28 @@ class HCA:
|
|
|
205
209
|
raise ValueError(f"WAV bitdepth of {self.fmtBitCount} is not supported, only 16 bit WAV files are supported.")
|
|
206
210
|
elif self.fmtSize != 16:
|
|
207
211
|
raise ValueError(f"WAV file has an FMT chunk of an unsupported size: {self.fmtSize}, the only supported size is 16.")
|
|
208
|
-
|
|
209
|
-
self.stream.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
self.
|
|
230
|
-
self.stream.read(WavDataHeaderStruct.size)
|
|
231
|
-
)
|
|
232
|
-
else:
|
|
233
|
-
raise ValueError("Invalid or an unsupported wav file.")
|
|
212
|
+
while (hdr := self.stream.read(4)):
|
|
213
|
+
size = int.from_bytes(self.stream.read(4), 'little')
|
|
214
|
+
size += (size & 1) # padding
|
|
215
|
+
offset = self.stream.tell()
|
|
216
|
+
match hdr:
|
|
217
|
+
case b"smpl":
|
|
218
|
+
self.stream.seek(-4, 1)
|
|
219
|
+
self.looping = True
|
|
220
|
+
# Will just be naming the important things here.
|
|
221
|
+
smplsig, smplesize, _, _, _, _, _, _, _, self.LoopCount, _, _, _, self.LoopStartSample, self.LoopEndSample, _, _ = WavSmplHeaderStruct.unpack(
|
|
222
|
+
self.stream.read(WavSmplHeaderStruct.size)
|
|
223
|
+
)
|
|
224
|
+
if self.LoopCount != 1:
|
|
225
|
+
self.looping = False # Unsupported multiple looping points, so backtracks, and ignores looping data.
|
|
226
|
+
self.stream.seek(-WavSmplHeaderStruct.size, 1)
|
|
227
|
+
self.stream.seek(8 + smplesize, 1)
|
|
228
|
+
case b"data":
|
|
229
|
+
self.stream.seek(-4, 1)
|
|
230
|
+
self.dataSig, self.dataSize = WavDataHeaderStruct.unpack(
|
|
231
|
+
self.stream.read(WavDataHeaderStruct.size)
|
|
232
|
+
)
|
|
233
|
+
self.stream.seek(offset + size, 0)
|
|
234
234
|
else:
|
|
235
235
|
raise ValueError("Invalid HCA or WAV file.")
|
|
236
236
|
self.stream.seek(0)
|
|
@@ -309,3 +309,143 @@ class HCA:
|
|
|
309
309
|
header = self.hcastream.read(self.header_size)
|
|
310
310
|
self.hcastream.seek(0)
|
|
311
311
|
return header
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class HCACodec(HCA):
|
|
315
|
+
"""Use this class for encoding and decoding HCA files, from and to WAV."""
|
|
316
|
+
CHUNK_INTERVAL = 64
|
|
317
|
+
BASE_FRAMERATE = 2997 # dt = CHUNK_INTERVAL / BASE_FRAMERATE
|
|
318
|
+
AUDIO_CODEC = 4
|
|
319
|
+
METADATA_COUNT = 1
|
|
320
|
+
|
|
321
|
+
filename: str
|
|
322
|
+
|
|
323
|
+
chnls: int
|
|
324
|
+
sampling_rate: int
|
|
325
|
+
total_samples: int
|
|
326
|
+
avbps: int
|
|
327
|
+
|
|
328
|
+
filesize: int
|
|
329
|
+
|
|
330
|
+
def __init__(self, stream: str | bytes, filename: str = "default.hca", quality: CriHcaQuality = CriHcaQuality.High, key=0, subkey=0, **kwargs):
|
|
331
|
+
"""Initializes the HCA encoder/decoder
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
stream (str | bytes): Path to the HCA or WAV file, or a BinaryIO stream. WAV files will be automatically encoded with the given settings first.
|
|
335
|
+
filename (str, optional): USM filename. Defaults to "default.hca".
|
|
336
|
+
quality (CriHcaQuality, optional): Encoding quality. Defaults to CriHcaQuality.High.
|
|
337
|
+
key (int, optional): HCA key. Defaults to 0.
|
|
338
|
+
subkey (int, optional): HCA subkey. Defaults to 0.
|
|
339
|
+
"""
|
|
340
|
+
self.filename = filename
|
|
341
|
+
super().__init__(stream, key, subkey)
|
|
342
|
+
if self.filetype == "wav":
|
|
343
|
+
self.encode(
|
|
344
|
+
force_not_looping=True,
|
|
345
|
+
encrypt=key != 0,
|
|
346
|
+
keyless=False,
|
|
347
|
+
quality_level=quality
|
|
348
|
+
)
|
|
349
|
+
self.hcastream.seek(0, 2)
|
|
350
|
+
self.filesize = self.hcastream.tell()
|
|
351
|
+
self.hcastream.seek(0)
|
|
352
|
+
|
|
353
|
+
if self.filetype == "wav":
|
|
354
|
+
self.chnls = self.fmtChannelCount
|
|
355
|
+
self.sampling_rate = self.fmtSamplingRate
|
|
356
|
+
self.total_samples = int(self.dataSize // self.fmtSamplingSize)
|
|
357
|
+
else:
|
|
358
|
+
self.chnls = self.hca["ChannelCount"]
|
|
359
|
+
self.sampling_rate = self.hca["SampleRate"]
|
|
360
|
+
self.total_samples = self.hca["FrameCount"]
|
|
361
|
+
# I don't know how this is derived so I am putting my best guess here. TODO
|
|
362
|
+
self.avbps = int(self.filesize / self.chnls)
|
|
363
|
+
|
|
364
|
+
def generate_SFA(self, index: int, builder):
|
|
365
|
+
# USMBuilder usage
|
|
366
|
+
current_interval = 0
|
|
367
|
+
padding = (
|
|
368
|
+
0x20 - (self.hca["HeaderSize"] % 0x20)
|
|
369
|
+
if self.hca["HeaderSize"] % 0x20 != 0
|
|
370
|
+
else 0
|
|
371
|
+
)
|
|
372
|
+
SFA_chunk = USMChunkHeader.pack(
|
|
373
|
+
USMChunckHeaderType.SFA.value,
|
|
374
|
+
self.hca["HeaderSize"] + 0x18 + padding,
|
|
375
|
+
0,
|
|
376
|
+
0x18,
|
|
377
|
+
padding,
|
|
378
|
+
index,
|
|
379
|
+
0,
|
|
380
|
+
0,
|
|
381
|
+
0,
|
|
382
|
+
current_interval,
|
|
383
|
+
self.BASE_FRAMERATE,
|
|
384
|
+
0,
|
|
385
|
+
0,
|
|
386
|
+
)
|
|
387
|
+
SFA_chunk += self.get_header().ljust(self.hca["HeaderSize"] + padding, b"\x00")
|
|
388
|
+
res = []
|
|
389
|
+
res.append(SFA_chunk)
|
|
390
|
+
for i, frame in enumerate(self.get_frames(), start=1):
|
|
391
|
+
padding = (
|
|
392
|
+
0x20 - (self.hca["FrameSize"] % 0x20)
|
|
393
|
+
if self.hca["FrameSize"] % 0x20 != 0
|
|
394
|
+
else 0
|
|
395
|
+
)
|
|
396
|
+
SFA_chunk = USMChunkHeader.pack(
|
|
397
|
+
USMChunckHeaderType.SFA.value,
|
|
398
|
+
self.hca["FrameSize"] + 0x18 + padding,
|
|
399
|
+
0,
|
|
400
|
+
0x18,
|
|
401
|
+
padding,
|
|
402
|
+
index,
|
|
403
|
+
0,
|
|
404
|
+
0,
|
|
405
|
+
0,
|
|
406
|
+
current_interval,
|
|
407
|
+
self.BASE_FRAMERATE,
|
|
408
|
+
0,
|
|
409
|
+
0,
|
|
410
|
+
)
|
|
411
|
+
SFA_chunk += frame[1].ljust(self.hca["FrameSize"] + padding, b"\x00")
|
|
412
|
+
current_interval = round(i * self.CHUNK_INTERVAL)
|
|
413
|
+
res.append(SFA_chunk)
|
|
414
|
+
else:
|
|
415
|
+
SFA_chunk = USMChunkHeader.pack(
|
|
416
|
+
USMChunckHeaderType.SFA.value,
|
|
417
|
+
0x38,
|
|
418
|
+
0,
|
|
419
|
+
0x18,
|
|
420
|
+
0,
|
|
421
|
+
index,
|
|
422
|
+
0,
|
|
423
|
+
0,
|
|
424
|
+
2,
|
|
425
|
+
0,
|
|
426
|
+
30,
|
|
427
|
+
0,
|
|
428
|
+
0,
|
|
429
|
+
)
|
|
430
|
+
SFA_chunk += b"#CONTENTS END ===============\x00"
|
|
431
|
+
res[-1] += SFA_chunk
|
|
432
|
+
|
|
433
|
+
return res
|
|
434
|
+
|
|
435
|
+
def get_metadata(self):
|
|
436
|
+
payload = [dict(hca_header=(UTFTypeValues.bytes, self.get_header()))]
|
|
437
|
+
p = UTFBuilder(payload, table_name="AUDIO_HEADER")
|
|
438
|
+
p.strings = b"<NULL>\x00" + p.strings
|
|
439
|
+
return p.bytes()
|
|
440
|
+
|
|
441
|
+
def get_encoded(self):
|
|
442
|
+
"""Gets the encoded HCA audio data."""
|
|
443
|
+
self.hcastream.seek(0)
|
|
444
|
+
res = self.hcastream.read()
|
|
445
|
+
self.hcastream.seek(0)
|
|
446
|
+
return res
|
|
447
|
+
|
|
448
|
+
def save(self, filepath: str):
|
|
449
|
+
"""Saves the decoded WAV audio to filepath"""
|
|
450
|
+
with open(filepath, "wb") as f:
|
|
451
|
+
f.write(self.decode())
|
PyCriCodecsEx/usm.py
CHANGED
|
@@ -6,8 +6,6 @@ from functools import cached_property
|
|
|
6
6
|
|
|
7
7
|
from PyCriCodecsEx.chunk import *
|
|
8
8
|
from PyCriCodecsEx.utf import UTF, UTFBuilder
|
|
9
|
-
from PyCriCodecsEx.adx import ADX
|
|
10
|
-
from PyCriCodecsEx.hca import HCA
|
|
11
9
|
try:
|
|
12
10
|
import ffmpeg
|
|
13
11
|
except ImportError:
|
|
@@ -22,6 +20,7 @@ import tempfile
|
|
|
22
20
|
# code for a complete breakdown of the USM format.
|
|
23
21
|
|
|
24
22
|
class USMCrypt:
|
|
23
|
+
"""USM related crypto functions"""
|
|
25
24
|
videomask1: bytearray
|
|
26
25
|
videomask2: bytearray
|
|
27
26
|
audiomask: bytearray
|
|
@@ -162,6 +161,13 @@ class FFmpegCodec:
|
|
|
162
161
|
avbps: int
|
|
163
162
|
|
|
164
163
|
def __init__(self, stream: str | bytes):
|
|
164
|
+
"""Initialize FFmpegCodec with a media stream, gathering metadata and frame info.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
stream (str | bytes): The media stream to process.
|
|
168
|
+
NOTE:
|
|
169
|
+
A temp file maybe created for probing only. Which will be deleted after use.
|
|
170
|
+
"""
|
|
165
171
|
if type(stream) == str:
|
|
166
172
|
self.filename = stream
|
|
167
173
|
else:
|
|
@@ -317,260 +323,11 @@ class MPEG1Codec(FFmpegCodec):
|
|
|
317
323
|
super().__init__(stream)
|
|
318
324
|
assert self.format == "mpegvideo", "must be m1v format (mpegvideo)."
|
|
319
325
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
BASE_FRAMERATE = 2997 # dt = CHUNK_INTERVAL / BASE_FRAMERATE
|
|
323
|
-
AUDIO_CODEC = 4
|
|
324
|
-
METADATA_COUNT = 1
|
|
325
|
-
|
|
326
|
-
filename: str
|
|
327
|
-
|
|
328
|
-
chnls: int
|
|
329
|
-
sampling_rate: int
|
|
330
|
-
total_samples: int
|
|
331
|
-
avbps: int
|
|
332
|
-
|
|
333
|
-
filesize: int
|
|
334
|
-
|
|
335
|
-
def __init__(self, stream: str | bytes, filename: str = "default.hca", quality: CriHcaQuality = CriHcaQuality.High, key=0, subkey=0, **kwargs):
|
|
336
|
-
self.filename = filename
|
|
337
|
-
super().__init__(stream, key, subkey)
|
|
338
|
-
if self.filetype == "wav":
|
|
339
|
-
self.encode(
|
|
340
|
-
force_not_looping=True,
|
|
341
|
-
encrypt=key != 0,
|
|
342
|
-
keyless=False,
|
|
343
|
-
quality_level=quality
|
|
344
|
-
)
|
|
345
|
-
self.hcastream.seek(0, 2)
|
|
346
|
-
self.filesize = self.hcastream.tell()
|
|
347
|
-
self.hcastream.seek(0)
|
|
348
|
-
|
|
349
|
-
if self.filetype == "wav":
|
|
350
|
-
self.chnls = self.fmtChannelCount
|
|
351
|
-
self.sampling_rate = self.fmtSamplingRate
|
|
352
|
-
self.total_samples = int(self.dataSize // self.fmtSamplingSize)
|
|
353
|
-
else:
|
|
354
|
-
self.chnls = self.hca["ChannelCount"]
|
|
355
|
-
self.sampling_rate = self.hca["SampleRate"]
|
|
356
|
-
self.total_samples = self.hca["FrameCount"]
|
|
357
|
-
# I don't know how this is derived so I am putting my best guess here. TODO
|
|
358
|
-
self.avbps = int(self.filesize / self.chnls)
|
|
359
|
-
|
|
360
|
-
def generate_SFA(self, index: int, builder: "USMBuilder"):
|
|
361
|
-
current_interval = 0
|
|
362
|
-
padding = (
|
|
363
|
-
0x20 - (self.hca["HeaderSize"] % 0x20)
|
|
364
|
-
if self.hca["HeaderSize"] % 0x20 != 0
|
|
365
|
-
else 0
|
|
366
|
-
)
|
|
367
|
-
SFA_chunk = USMChunkHeader.pack(
|
|
368
|
-
USMChunckHeaderType.SFA.value,
|
|
369
|
-
self.hca["HeaderSize"] + 0x18 + padding,
|
|
370
|
-
0,
|
|
371
|
-
0x18,
|
|
372
|
-
padding,
|
|
373
|
-
index,
|
|
374
|
-
0,
|
|
375
|
-
0,
|
|
376
|
-
0,
|
|
377
|
-
current_interval,
|
|
378
|
-
self.BASE_FRAMERATE,
|
|
379
|
-
0,
|
|
380
|
-
0,
|
|
381
|
-
)
|
|
382
|
-
SFA_chunk += self.get_header().ljust(self.hca["HeaderSize"] + padding, b"\x00")
|
|
383
|
-
res = []
|
|
384
|
-
res.append(SFA_chunk)
|
|
385
|
-
for i, frame in enumerate(self.get_frames(), start=1):
|
|
386
|
-
padding = (
|
|
387
|
-
0x20 - (self.hca["FrameSize"] % 0x20)
|
|
388
|
-
if self.hca["FrameSize"] % 0x20 != 0
|
|
389
|
-
else 0
|
|
390
|
-
)
|
|
391
|
-
SFA_chunk = USMChunkHeader.pack(
|
|
392
|
-
USMChunckHeaderType.SFA.value,
|
|
393
|
-
self.hca["FrameSize"] + 0x18 + padding,
|
|
394
|
-
0,
|
|
395
|
-
0x18,
|
|
396
|
-
padding,
|
|
397
|
-
index,
|
|
398
|
-
0,
|
|
399
|
-
0,
|
|
400
|
-
0,
|
|
401
|
-
current_interval,
|
|
402
|
-
self.BASE_FRAMERATE,
|
|
403
|
-
0,
|
|
404
|
-
0,
|
|
405
|
-
)
|
|
406
|
-
SFA_chunk += frame[1].ljust(self.hca["FrameSize"] + padding, b"\x00")
|
|
407
|
-
current_interval = round(i * self.CHUNK_INTERVAL)
|
|
408
|
-
res.append(SFA_chunk)
|
|
409
|
-
else:
|
|
410
|
-
SFA_chunk = USMChunkHeader.pack(
|
|
411
|
-
USMChunckHeaderType.SFA.value,
|
|
412
|
-
0x38,
|
|
413
|
-
0,
|
|
414
|
-
0x18,
|
|
415
|
-
0,
|
|
416
|
-
index,
|
|
417
|
-
0,
|
|
418
|
-
0,
|
|
419
|
-
2,
|
|
420
|
-
0,
|
|
421
|
-
30,
|
|
422
|
-
0,
|
|
423
|
-
0,
|
|
424
|
-
)
|
|
425
|
-
SFA_chunk += b"#CONTENTS END ===============\x00"
|
|
426
|
-
res[-1] += SFA_chunk
|
|
427
|
-
|
|
428
|
-
return res
|
|
429
|
-
|
|
430
|
-
def get_metadata(self):
|
|
431
|
-
payload = [dict(hca_header=(UTFTypeValues.bytes, self.get_header()))]
|
|
432
|
-
p = UTFBuilder(payload, table_name="AUDIO_HEADER")
|
|
433
|
-
p.strings = b"<NULL>\x00" + p.strings
|
|
434
|
-
return p.bytes()
|
|
435
|
-
|
|
436
|
-
def get_encoded(self):
|
|
437
|
-
"""Gets the encoded HCA audio data."""
|
|
438
|
-
self.hcastream.seek(0)
|
|
439
|
-
res = self.hcastream.read()
|
|
440
|
-
self.hcastream.seek(0)
|
|
441
|
-
return res
|
|
442
|
-
|
|
443
|
-
def save(self, filepath: str):
|
|
444
|
-
"""Saves the decoded WAV audio to filepath"""
|
|
445
|
-
with open(filepath, "wb") as f:
|
|
446
|
-
f.write(self.decode())
|
|
447
|
-
|
|
448
|
-
class ADXCodec(ADX):
|
|
449
|
-
CHUNK_INTERVAL = 99.9
|
|
450
|
-
BASE_FRAMERATE = 2997
|
|
451
|
-
# TODO: Move these to an enum
|
|
452
|
-
AUDIO_CODEC = 2
|
|
453
|
-
METADATA_COUNT = 0
|
|
454
|
-
|
|
455
|
-
filename : str
|
|
456
|
-
filesize : int
|
|
457
|
-
|
|
458
|
-
adx : bytes
|
|
459
|
-
header : bytes
|
|
460
|
-
sfaStream: BinaryIO
|
|
461
|
-
|
|
462
|
-
AdxDataOffset: int
|
|
463
|
-
AdxEncoding: int
|
|
464
|
-
AdxBlocksize: int
|
|
465
|
-
AdxSampleBitdepth: int
|
|
466
|
-
AdxChannelCount: int
|
|
467
|
-
AdxSamplingRate: int
|
|
468
|
-
AdxSampleCount: int
|
|
469
|
-
AdxHighpassFrequency: int
|
|
470
|
-
AdxVersion: int
|
|
471
|
-
AdxFlags: int
|
|
472
|
-
|
|
473
|
-
chnls: int
|
|
474
|
-
sampling_rate: int
|
|
475
|
-
total_samples: int
|
|
476
|
-
avbps: int
|
|
477
|
-
|
|
478
|
-
def __init__(self, stream: str | bytes, filename: str = "default.adx", bitdepth: int = 4, **kwargs):
|
|
479
|
-
if type(stream) == str:
|
|
480
|
-
self.adx = open(stream, "rb").read()
|
|
481
|
-
else:
|
|
482
|
-
self.adx = stream
|
|
483
|
-
self.filename = filename
|
|
484
|
-
self.filesize = len(self.adx)
|
|
485
|
-
magic = self.adx[:4]
|
|
486
|
-
if magic == b"RIFF":
|
|
487
|
-
self.adx = self.encode(self.adx, bitdepth, force_not_looping=True)
|
|
488
|
-
self.sfaStream = BytesIO(self.adx)
|
|
489
|
-
header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
|
|
490
|
-
FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
|
|
491
|
-
assert FourCC == 0x8000, "either ADX or WAV is supported"
|
|
492
|
-
assert self.AdxVersion in {3,4}, "unsupported ADX version"
|
|
493
|
-
if self.AdxVersion == 4:
|
|
494
|
-
self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
|
|
495
|
-
self.sfaStream.seek(0)
|
|
496
|
-
self.chnls = self.AdxChannelCount
|
|
497
|
-
self.sampling_rate = self.AdxSamplingRate
|
|
498
|
-
self.total_samples = self.AdxSampleCount
|
|
499
|
-
self.avbps = int(self.filesize * 8 * self.chnls) - self.filesize
|
|
500
|
-
|
|
501
|
-
def generate_SFA(self, index: int, builder: "USMBuilder"):
|
|
502
|
-
current_interval = 0
|
|
503
|
-
stream_size = len(self.adx) - self.AdxBlocksize
|
|
504
|
-
chunk_size = int(self.AdxSamplingRate // (self.BASE_FRAMERATE / 100) // 32) * (self.AdxBlocksize * self.AdxChannelCount)
|
|
505
|
-
self.sfaStream.seek(0)
|
|
506
|
-
res = []
|
|
507
|
-
while self.sfaStream.tell() < stream_size:
|
|
508
|
-
if self.sfaStream.tell() > 0:
|
|
509
|
-
if self.sfaStream.tell() + chunk_size < stream_size:
|
|
510
|
-
datalen = chunk_size
|
|
511
|
-
else:
|
|
512
|
-
datalen = (stream_size - (self.AdxDataOffset + 4) - chunk_size) % chunk_size
|
|
513
|
-
else:
|
|
514
|
-
datalen = self.AdxDataOffset + 4
|
|
515
|
-
if not datalen:
|
|
516
|
-
break
|
|
517
|
-
padding = (0x20 - (datalen % 0x20) if datalen % 0x20 != 0 else 0)
|
|
518
|
-
SFA_chunk = USMChunkHeader.pack(
|
|
519
|
-
USMChunckHeaderType.SFA.value,
|
|
520
|
-
datalen + 0x18 + padding,
|
|
521
|
-
0,
|
|
522
|
-
0x18,
|
|
523
|
-
padding,
|
|
524
|
-
index,
|
|
525
|
-
0,
|
|
526
|
-
0,
|
|
527
|
-
0,
|
|
528
|
-
round(current_interval),
|
|
529
|
-
self.BASE_FRAMERATE,
|
|
530
|
-
0,
|
|
531
|
-
0
|
|
532
|
-
)
|
|
533
|
-
chunk_data = self.sfaStream.read(datalen)
|
|
534
|
-
if builder.encrypt_audio:
|
|
535
|
-
SFA_chunk = builder.AudioMask(chunk_data)
|
|
536
|
-
SFA_chunk += chunk_data.ljust(datalen + padding, b"\x00")
|
|
537
|
-
current_interval += self.CHUNK_INTERVAL
|
|
538
|
-
res.append(SFA_chunk)
|
|
539
|
-
# ---
|
|
540
|
-
SFA_chunk = USMChunkHeader.pack(
|
|
541
|
-
USMChunckHeaderType.SFA.value,
|
|
542
|
-
0x38,
|
|
543
|
-
0,
|
|
544
|
-
0x18,
|
|
545
|
-
0,
|
|
546
|
-
index,
|
|
547
|
-
0,
|
|
548
|
-
0,
|
|
549
|
-
2,
|
|
550
|
-
0,
|
|
551
|
-
30,
|
|
552
|
-
0,
|
|
553
|
-
0
|
|
554
|
-
)
|
|
555
|
-
SFA_chunk += b"#CONTENTS END ===============\x00"
|
|
556
|
-
res[-1] += SFA_chunk
|
|
557
|
-
return res
|
|
558
|
-
|
|
559
|
-
def get_metadata(self):
|
|
560
|
-
return None
|
|
561
|
-
|
|
562
|
-
def get_encoded(self):
|
|
563
|
-
"""Gets the encoded ADX audio data."""
|
|
564
|
-
return self.adx
|
|
565
|
-
|
|
566
|
-
def save(self, filepath: str):
|
|
567
|
-
"""Saves the encoded ADX audio to filepath"""
|
|
568
|
-
with open(filepath, "wb") as f:
|
|
569
|
-
f.write(self.decode(self.adx))
|
|
570
|
-
|
|
326
|
+
from PyCriCodecsEx.hca import HCACodec
|
|
327
|
+
from PyCriCodecsEx.adx import ADXCodec
|
|
571
328
|
|
|
572
329
|
class USM(USMCrypt):
|
|
573
|
-
"""
|
|
330
|
+
"""Use this class to extract infromation and data from a USM file."""
|
|
574
331
|
|
|
575
332
|
filename: str
|
|
576
333
|
decrypt: bool
|
|
@@ -585,7 +342,7 @@ class USM(USMCrypt):
|
|
|
585
342
|
|
|
586
343
|
metadata: list
|
|
587
344
|
|
|
588
|
-
def __init__(self, filename, key: str | int = None):
|
|
345
|
+
def __init__(self, filename : str | BinaryIO, key: str | int = None):
|
|
589
346
|
"""Loads a USM file into memory and prepares it for processing.
|
|
590
347
|
|
|
591
348
|
Args:
|
|
@@ -601,7 +358,10 @@ class USM(USMCrypt):
|
|
|
601
358
|
self._load_file()
|
|
602
359
|
|
|
603
360
|
def _load_file(self):
|
|
604
|
-
|
|
361
|
+
if type(self.filename) == str:
|
|
362
|
+
self.stream = open(self.filename, "rb")
|
|
363
|
+
else:
|
|
364
|
+
self.stream = self.filename
|
|
605
365
|
self.stream.seek(0, 2)
|
|
606
366
|
self.size = self.stream.tell()
|
|
607
367
|
self.stream.seek(0)
|
|
@@ -716,7 +476,7 @@ class USM(USMCrypt):
|
|
|
716
476
|
|
|
717
477
|
@property
|
|
718
478
|
def streams(self):
|
|
719
|
-
"""[Type (@SFV, @SFA),
|
|
479
|
+
"""Generator of Tuple[Stream Type ("@SFV", "@SFA"), File name, Raw stream data]"""
|
|
720
480
|
for stream in self.CRIDObj.dictarray[1:]:
|
|
721
481
|
filename, stmid, chno = stream["filename"][1], stream["stmid"][1], stream["chno"][1]
|
|
722
482
|
stmid = int.to_bytes(stmid, 4, 'big', signed='False')
|
|
@@ -751,7 +511,7 @@ class USM(USMCrypt):
|
|
|
751
511
|
return []
|
|
752
512
|
|
|
753
513
|
class USMBuilder(USMCrypt):
|
|
754
|
-
"""
|
|
514
|
+
"""Use this class to build USM files."""
|
|
755
515
|
video_stream: VP9Codec | H264Codec | MPEG1Codec
|
|
756
516
|
audio_streams: List[HCACodec | ADXCodec]
|
|
757
517
|
|
PyCriCodecsEx/utf.py
CHANGED
|
@@ -8,6 +8,7 @@ from struct import unpack, calcsize, pack
|
|
|
8
8
|
from PyCriCodecsEx.chunk import *
|
|
9
9
|
|
|
10
10
|
class UTF:
|
|
11
|
+
"""Use this class to unpack @UTF table binary payload."""
|
|
11
12
|
|
|
12
13
|
_dictarray: list
|
|
13
14
|
|
|
@@ -23,11 +24,11 @@ class UTF:
|
|
|
23
24
|
recursive: bool
|
|
24
25
|
encoding : str = 'utf-8'
|
|
25
26
|
|
|
26
|
-
def __init__(self, stream, recursive=False):
|
|
27
|
+
def __init__(self, stream : str | BinaryIO, recursive=False):
|
|
27
28
|
"""Unpacks UTF table binary payload
|
|
28
29
|
|
|
29
30
|
Args:
|
|
30
|
-
stream (Union[str
|
|
31
|
+
stream (Union[str | BinaryIO]): The input stream or file path to read the UTF table from.
|
|
31
32
|
recursive (bool): Whether to recursively unpack nested UTF tables.
|
|
32
33
|
"""
|
|
33
34
|
if type(stream) == str:
|
|
@@ -290,6 +291,7 @@ class UTF:
|
|
|
290
291
|
return self._dictarray
|
|
291
292
|
|
|
292
293
|
class UTFBuilder:
|
|
294
|
+
"""Use this class to build UTF table binary payloads from a `dictarray`."""
|
|
293
295
|
|
|
294
296
|
encoding: str
|
|
295
297
|
dictarray: list
|
|
@@ -591,34 +593,16 @@ class UTFBuilder:
|
|
|
591
593
|
return dataarray
|
|
592
594
|
|
|
593
595
|
class UTFViewer:
|
|
596
|
+
"""Use this class to create dataclass-like access to `dictarray`s."""
|
|
597
|
+
|
|
594
598
|
_payload: dict
|
|
595
599
|
|
|
596
600
|
def __init__(self, payload):
|
|
597
601
|
"""Construct a non-owning read-write, deletable view of a UTF table dictarray.
|
|
602
|
+
|
|
598
603
|
Nested classes are supported.
|
|
599
|
-
Sorting (using .sort()) is done in-place and affects the original payload.
|
|
600
604
|
|
|
601
|
-
|
|
602
|
-
```python
|
|
603
|
-
class CueNameTable(UTFViewer):
|
|
604
|
-
CueName : str
|
|
605
|
-
CueIndex : int
|
|
606
|
-
class ACBTable(UTFViewer):
|
|
607
|
-
CueNameTable : List[CueNameTable]
|
|
608
|
-
Awb : AWB
|
|
609
|
-
src = ACB(ACB_sample)
|
|
610
|
-
payload = ACBTable(src.payload)
|
|
611
|
-
>>> Referencing items through Python is allowed
|
|
612
|
-
name = payload.CueNameTable
|
|
613
|
-
>>> Lists can be indexed
|
|
614
|
-
name_str = name[0].CueName
|
|
615
|
-
>>> Deleting items from lists is also allowed
|
|
616
|
-
src.view.CueNameTable.pop(1)
|
|
617
|
-
src.view.CueTable.pop(1)
|
|
618
|
-
>>> The changes will be reflected in the original UTF payload
|
|
619
|
-
|
|
620
|
-
See __new__ for the actual constructor.
|
|
621
|
-
```
|
|
605
|
+
Sorting (using .sort()) is done in-place and affects the original payload.
|
|
622
606
|
"""
|
|
623
607
|
assert isinstance(payload, dict), "payload must be a dictionary."
|
|
624
608
|
super().__setattr__("_payload", payload)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyCriCodecsEx
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Criware formats library for Python
|
|
5
|
+
Home-page: https://mos9527.github.io/PyCriCodecsEx/
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: usm
|
|
13
|
+
Requires-Dist: ffmpeg-python; extra == "usm"
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: provides-extra
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
PyCriCodecsEx
|
|
24
|
+
---
|
|
25
|
+
A continuation of @Youjose's work on Criware formats. Feautres are still in flux and subject to change. When in doubt, Refer to the [original repo](https://github.com/Youjose/PyCriCodecs) for more information.
|
|
26
|
+
|
|
27
|
+
Detailed documentation, installation instructions are available at https://mos9527.com/PyCriCodecsEx
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Credits
|
|
31
|
+
- https://github.com/Youjose/PyCriCodecs
|
|
32
|
+
- https://github.com/Mikewando/PyCriCodecs ([PR#1 on USM](https://github.com/mos9527/PyCriCodecsEx/pull/1))
|
|
33
|
+
- https://github.com/donmai-me/WannaCRI
|
|
34
|
+
- https://github.com/vgmstream/vgmstream
|
|
35
|
+
- https://github.com/K0lb3/UnityPy (For CI script)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CriCodecsEx.cpython-312-i386-linux-musl.so,sha256=BFNR6EisI_-4HhrIBCoUVARsahMUbTm8B95jhsBmRkU,298493
|
|
2
|
+
pycricodecsex-0.0.3.dist-info/RECORD,,
|
|
3
|
+
pycricodecsex-0.0.3.dist-info/METADATA,sha256=yvFl7vPBIloog0TJ9nUgf7psQbf1IrL2URvIXdDoU90,1256
|
|
4
|
+
pycricodecsex-0.0.3.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
|
|
5
|
+
pycricodecsex-0.0.3.dist-info/WHEEL,sha256=Vuwis_LWaa45QUuJ2qqSgABfO0elv3qQohyjTF3DnPc,110
|
|
6
|
+
pycricodecsex-0.0.3.dist-info/licenses/LICENSE,sha256=GyZb4dzF2tM3LU5I_tJXpHtUDz8297zeUuCJ5qz8io0,1063
|
|
7
|
+
pycricodecsex.libs/libgcc_s-f3fb5a36.so.1,sha256=SrjjCCuY7RHj-T9JLrY9XFMgCCpYD9Qmezr4uoJGVEQ,168321
|
|
8
|
+
pycricodecsex.libs/libstdc++-d2a021ba.so.6.0.32,sha256=1zr_iwGwEBe95gyKdgiw7C4Y1RR9ijV40j66rk4elzg,3537349
|
|
9
|
+
PyCriCodecsEx/cpk.py,sha256=3ymJe_2kRBDll0C4SAjQBEROXSbVigDGU_QTa31kw-g,37466
|
|
10
|
+
PyCriCodecsEx/hca.py,sha256=YrG1uMj4penM5m1Qm7cIfGUcWdcRCPGgUAEjH-cqLcs,18994
|
|
11
|
+
PyCriCodecsEx/utf.py,sha256=ORSoyRrgBbdaGP6cQQejWfqkdWz6vEGG_sTo_fbO48Y,27555
|
|
12
|
+
PyCriCodecsEx/adx.py,sha256=2oRgzwKL74pvQyHgX2zWckSGM0vpRrN7dB7KjdeqGhw,5812
|
|
13
|
+
PyCriCodecsEx/awb.py,sha256=2R3xK8YBXOGygn_kBflzuk3DY1hQHiF-hLaPcrzj1sk,6198
|
|
14
|
+
PyCriCodecsEx/chunk.py,sha256=ZYyeqqcBK6dDDj12YvrhpZDzW0iyOhysRlYuCD3rgQ8,2719
|
|
15
|
+
PyCriCodecsEx/acb.py,sha256=CgZabDx1t19DP6fWR6pSgLVF9n1Gp3owAjx0pOU4_Bw,10444
|
|
16
|
+
PyCriCodecsEx/usm.py,sha256=TsJNBaPul3EYGQJwqLUcbMOSs_oGw8gvjz7mexosURc,35977
|
|
17
|
+
PyCriCodecsEx/__init__.py,sha256=4GZKi13lDTD25YBkGakhZyEQZWTER_OWQMNPoH_UM2c,22
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: PyCriCodecsEx
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: Criware formats library for Python
|
|
5
|
-
Home-page: https://github.com/mos9527/PyCriCodecsEx
|
|
6
|
-
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Provides-Extra: usm
|
|
13
|
-
Requires-Dist: ffmpeg-python; extra == "usm"
|
|
14
|
-
Dynamic: classifier
|
|
15
|
-
Dynamic: description
|
|
16
|
-
Dynamic: description-content-type
|
|
17
|
-
Dynamic: home-page
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
Dynamic: provides-extra
|
|
20
|
-
Dynamic: requires-python
|
|
21
|
-
Dynamic: summary
|
|
22
|
-
|
|
23
|
-
# PyCriCodecsEx
|
|
24
|
-
A continuation of @Youjose's work on Criware formats. Feautres are still in flux and subject to change. When in doubt, Refer to the [original repo](https://github.com/Youjose/PyCriCodecs) for more information.
|
|
25
|
-
|
|
26
|
-
# Installation
|
|
27
|
-
```bash
|
|
28
|
-
pip install PyCriCodecsEx
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
For USM features, you need `ffmpeg` installed and available in your PATH. See also https://github.com/kkroening/ffmpeg-python?tab=readme-ov-file#installing-ffmpeg
|
|
32
|
-
|
|
33
|
-
## Features
|
|
34
|
-
If not otherwise mentioned, all features marked with [x] are considered working, and has been verified with official tools.
|
|
35
|
-
|
|
36
|
-
Examples are available in [Tests](https://github.com/mos9527/PyCriCodecsEx/tree/main/Tests)
|
|
37
|
-
|
|
38
|
-
### ACB Cue sheets (also AWB)
|
|
39
|
-
- [x] Cue extraction support for most ACBs
|
|
40
|
-
- [x] Cue waveform(s) encoding with ADX/HCA support
|
|
41
|
-
- [x] Comprehensive Cue metadata editing support (via Python API)
|
|
42
|
-
|
|
43
|
-
### USM Sofdec2 (Encode & Decode)
|
|
44
|
-
#### Audio Stream
|
|
45
|
-
For audio to be muxed in, you need a PCM WAV sample with NO metadata, which can be produced with e.g.:
|
|
46
|
-
```bash
|
|
47
|
-
ffmpeg -i input.mp3 -vn -c:a pcm_s16le -map_metadata -1 output.wav
|
|
48
|
-
```
|
|
49
|
-
Decoding and Encoded format can be the following:
|
|
50
|
-
- [x] HCA
|
|
51
|
-
- [x] ADX
|
|
52
|
-
#### Video Stream
|
|
53
|
-
**NOTE**: You definitely want to tweak these encode settings a bit.
|
|
54
|
-
- [x] Sofdec Prime (MPEG1, from `.m1v` container)
|
|
55
|
-
- Prepare source file with: `ffmpeg -i <input_file> -c:v mpeg1video -an <output_file>.m1v`
|
|
56
|
-
- [x] H264 (from `.h264` raw container)
|
|
57
|
-
- Prepare source file with: `ffmpeg -i <input_file> -c:v libx264 -an <output_file>.h264`
|
|
58
|
-
- [x] VP9 (from `.ivf` container)
|
|
59
|
-
- Prepare source file with: `ffmpeg -i <input_file> -c:v libvpx -an <output_file>.ivf`
|
|
60
|
-
### HCA Audio Codec
|
|
61
|
-
- [x] Decoding (up to version 3.0)
|
|
62
|
-
- [x] Encoding (up to version 3.0)
|
|
63
|
-
### ADX Audio Codec
|
|
64
|
-
- [x] Decoding
|
|
65
|
-
- [x] Encoding
|
|
66
|
-
### CPK
|
|
67
|
-
- [x] Unpacking
|
|
68
|
-
- [x] Packing
|
|
69
|
-
|
|
70
|
-
## Roadmap
|
|
71
|
-
- [x] ACB Extraction (Massive TODO. see also https://github.com/mos9527/PyCriCodecsEx/blob/main/Research/ACBSchema.py)
|
|
72
|
-
- [ ] Interface for encode tasks (CLI then maybe GUI?)
|
|
73
|
-
- [ ] Documentation
|
|
74
|
-
- [ ] C/C++ port + FFI
|
|
75
|
-
## Currently Known Bugs
|
|
76
|
-
- USM seeking does not work. Though most games don't use it anyways.
|
|
77
|
-
- Not important, and might not fix: ADX encoding and decoding at higher bitdepths (11-15) adds popping noise.
|
|
78
|
-
- Some CPK's that has the same filename for every file in the entry will overwrite each other.
|
|
79
|
-
- Probably many more I am unaware of, report if you find any.
|
|
80
|
-
|
|
81
|
-
# Credits
|
|
82
|
-
- https://github.com/Youjose/PyCriCodecs
|
|
83
|
-
- https://github.com/Mikewando/PyCriCodecs ([PR#1 on USM](https://github.com/mos9527/PyCriCodecsEx/pull/1))
|
|
84
|
-
- https://github.com/donmai-me/WannaCRI
|
|
85
|
-
- https://github.com/vgmstream/vgmstream
|
|
86
|
-
- https://github.com/K0lb3/UnityPy (For CI script)
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
CriCodecsEx.cpython-312-i386-linux-musl.so,sha256=BFNR6EisI_-4HhrIBCoUVARsahMUbTm8B95jhsBmRkU,298493
|
|
2
|
-
pycricodecsex.libs/libgcc_s-f3fb5a36.so.1,sha256=SrjjCCuY7RHj-T9JLrY9XFMgCCpYD9Qmezr4uoJGVEQ,168321
|
|
3
|
-
pycricodecsex.libs/libstdc++-d2a021ba.so.6.0.32,sha256=1zr_iwGwEBe95gyKdgiw7C4Y1RR9ijV40j66rk4elzg,3537349
|
|
4
|
-
pycricodecsex-0.0.2.dist-info/RECORD,,
|
|
5
|
-
pycricodecsex-0.0.2.dist-info/METADATA,sha256=gdddHtzOzTWQJUszhu_mCanshANM_6j61_DfggTnpc8,3352
|
|
6
|
-
pycricodecsex-0.0.2.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
|
|
7
|
-
pycricodecsex-0.0.2.dist-info/WHEEL,sha256=Vuwis_LWaa45QUuJ2qqSgABfO0elv3qQohyjTF3DnPc,110
|
|
8
|
-
pycricodecsex-0.0.2.dist-info/licenses/LICENSE,sha256=GyZb4dzF2tM3LU5I_tJXpHtUDz8297zeUuCJ5qz8io0,1063
|
|
9
|
-
PyCriCodecsEx/cpk.py,sha256=-SbUua1vMQ6OR-WN0TOv0iieLluy4fExueQYkRlM7IY,37196
|
|
10
|
-
PyCriCodecsEx/hca.py,sha256=OlcDJS8BSaa_g4LH2KS8bOHS_SfGrzwKH5tuCihBMmo,14239
|
|
11
|
-
PyCriCodecsEx/utf.py,sha256=xUAG0fHZjznUuBMwf6sXlfw-GHHvt2GtoHNrg9T4BFY,28111
|
|
12
|
-
PyCriCodecsEx/adx.py,sha256=dKv1HcBoe4Wkfot4Y2bfViBV2wJe1rrTzM325VA6RKk,762
|
|
13
|
-
PyCriCodecsEx/awb.py,sha256=NSm3eia1MR4xCLuAUJC-82uzbPYk7PO6kQBYOsNRWW8,6132
|
|
14
|
-
PyCriCodecsEx/chunk.py,sha256=MbajWDE_6g9JF254aZw-tyWcKpGVmQgIbGISpC7ootQ,2829
|
|
15
|
-
PyCriCodecsEx/acb.py,sha256=j5u4mcv4Jz9uYxjpFCbvh9oxXcSpigb8ulB0wlvr4Ig,10084
|
|
16
|
-
PyCriCodecsEx/usm.py,sha256=MT_Gl-6x25PMy4G0t3RipgPS5Pz-XyqpQGdQIhkK_7E,43872
|
|
17
|
-
PyCriCodecsEx/__init__.py,sha256=QvlVh4JTl3JL7jQAja76yKtT-IvF4631ASjWY1wS6AQ,22
|
|
File without changes
|
|
File without changes
|
|
File without changes
|