PyCriCodecsEx 0.0.2__cp311-cp311-musllinux_1_2_i686.whl → 0.0.5__cp311-cp311-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.
- CriCodecsEx.cpython-311-i386-linux-musl.so +0 -0
- PyCriCodecsEx/__init__.py +1 -1
- PyCriCodecsEx/acb.py +67 -29
- PyCriCodecsEx/adx.py +146 -4
- PyCriCodecsEx/awb.py +3 -3
- PyCriCodecsEx/chunk.py +0 -5
- PyCriCodecsEx/cpk.py +56 -50
- PyCriCodecsEx/hca.py +170 -27
- PyCriCodecsEx/usm.py +32 -266
- PyCriCodecsEx/utf.py +8 -24
- pycricodecsex-0.0.5.dist-info/METADATA +35 -0
- pycricodecsex-0.0.5.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.5.dist-info}/WHEEL +0 -0
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.5.dist-info}/top_level.txt +0 -0
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,146 @@ 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): Filename, used by USMBuilder. 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) -> bytes:
|
|
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 | BinaryIO):
|
|
449
|
+
"""Saves the decoded WAV audio to filepath or a writable stream"""
|
|
450
|
+
if type(filepath) == str:
|
|
451
|
+
with open(filepath, "wb") as f:
|
|
452
|
+
f.write(self.decode())
|
|
453
|
+
else:
|
|
454
|
+
filepath.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:
|
|
@@ -15,13 +13,10 @@ except ImportError:
|
|
|
15
13
|
import tempfile
|
|
16
14
|
|
|
17
15
|
# Big thanks and credit for k0lb3 and 9th helping me write this specific code.
|
|
18
|
-
# Also credit for the original C++ code from Nyagamon/bnnm.
|
|
19
|
-
|
|
20
|
-
# Apparently there is an older USM format called SofDec? This is for SofDec2 though.
|
|
21
|
-
# Extraction working only for now, although check https://github.com/donmai-me/WannaCRI/
|
|
22
|
-
# code for a complete breakdown of the USM format.
|
|
16
|
+
# Also credit for the original C++ code from Nyagamon/bnnm and https://github.com/donmai-me/WannaCRI/
|
|
23
17
|
|
|
24
18
|
class USMCrypt:
|
|
19
|
+
"""USM related crypto functions"""
|
|
25
20
|
videomask1: bytearray
|
|
26
21
|
videomask2: bytearray
|
|
27
22
|
audiomask: bytearray
|
|
@@ -151,6 +146,7 @@ class USMCrypt:
|
|
|
151
146
|
# are still unknown how to derive them, at least video wise it is possible, no idea how it's calculated audio wise nor anything else
|
|
152
147
|
# seems like it could be random values and the USM would still work.
|
|
153
148
|
class FFmpegCodec:
|
|
149
|
+
"""Base codec for FFMpeg-based Video streams"""
|
|
154
150
|
filename: str
|
|
155
151
|
filesize: int
|
|
156
152
|
|
|
@@ -162,6 +158,13 @@ class FFmpegCodec:
|
|
|
162
158
|
avbps: int
|
|
163
159
|
|
|
164
160
|
def __init__(self, stream: str | bytes):
|
|
161
|
+
"""Initialize FFmpegCodec with a media stream, gathering metadata and frame info.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
stream (str | bytes): The media stream to process.
|
|
165
|
+
NOTE:
|
|
166
|
+
A temp file maybe created for probing only. Which will be deleted after use.
|
|
167
|
+
"""
|
|
165
168
|
if type(stream) == str:
|
|
166
169
|
self.filename = stream
|
|
167
170
|
else:
|
|
@@ -226,7 +229,7 @@ class FFmpegCodec:
|
|
|
226
229
|
return len(self.packets)
|
|
227
230
|
|
|
228
231
|
def frames(self):
|
|
229
|
-
"""frame data, frame dict, is keyframe, duration"""
|
|
232
|
+
"""Generator of [frame data, frame dict, is keyframe, duration]"""
|
|
230
233
|
offsets = [int(packet["pos"]) for packet in self.packets] + [self.filesize]
|
|
231
234
|
for i, frame in enumerate(self.packets):
|
|
232
235
|
frame_size = offsets[i + 1] - offsets[i]
|
|
@@ -284,13 +287,16 @@ class FFmpegCodec:
|
|
|
284
287
|
return SFV_list
|
|
285
288
|
|
|
286
289
|
def save(self, filepath: str):
|
|
287
|
-
'''Saves the
|
|
290
|
+
'''Saves the underlying video stream to a file.'''
|
|
288
291
|
tell = self.file.tell()
|
|
289
292
|
self.file.seek(0)
|
|
290
293
|
shutil.copyfileobj(self.file, open(filepath, 'wb'))
|
|
291
294
|
self.file.seek(tell)
|
|
292
295
|
|
|
293
296
|
class VP9Codec(FFmpegCodec):
|
|
297
|
+
"""VP9 Video stream codec.
|
|
298
|
+
|
|
299
|
+
Only streams with `.ivf` containers are supported."""
|
|
294
300
|
MPEG_CODEC = 9
|
|
295
301
|
MPEG_DCPREC = 0
|
|
296
302
|
VERSION = 16777984
|
|
@@ -299,6 +305,9 @@ class VP9Codec(FFmpegCodec):
|
|
|
299
305
|
super().__init__(filename)
|
|
300
306
|
assert self.format == "ivf", "must be ivf format."
|
|
301
307
|
class H264Codec(FFmpegCodec):
|
|
308
|
+
"""H264 Video stream codec.
|
|
309
|
+
|
|
310
|
+
Only streams with `.h264` containers are supported."""
|
|
302
311
|
MPEG_CODEC = 5
|
|
303
312
|
MPEG_DCPREC = 11
|
|
304
313
|
VERSION = 0
|
|
@@ -309,6 +318,9 @@ class H264Codec(FFmpegCodec):
|
|
|
309
318
|
self.format == "h264"
|
|
310
319
|
), "must be raw h264 data. transcode with '.h264' suffix as output"
|
|
311
320
|
class MPEG1Codec(FFmpegCodec):
|
|
321
|
+
"""MPEG1 Video stream codec.
|
|
322
|
+
|
|
323
|
+
Only streams with `.mpeg1` containers are supported."""
|
|
312
324
|
MPEG_CODEC = 1
|
|
313
325
|
MPEG_DCPREC = 11
|
|
314
326
|
VERSION = 0
|
|
@@ -317,260 +329,11 @@ class MPEG1Codec(FFmpegCodec):
|
|
|
317
329
|
super().__init__(stream)
|
|
318
330
|
assert self.format == "mpegvideo", "must be m1v format (mpegvideo)."
|
|
319
331
|
|
|
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
|
-
|
|
332
|
+
from PyCriCodecsEx.hca import HCACodec
|
|
333
|
+
from PyCriCodecsEx.adx import ADXCodec
|
|
571
334
|
|
|
572
335
|
class USM(USMCrypt):
|
|
573
|
-
"""
|
|
336
|
+
"""Use this class to extract infromation and data from a USM file."""
|
|
574
337
|
|
|
575
338
|
filename: str
|
|
576
339
|
decrypt: bool
|
|
@@ -585,7 +348,7 @@ class USM(USMCrypt):
|
|
|
585
348
|
|
|
586
349
|
metadata: list
|
|
587
350
|
|
|
588
|
-
def __init__(self, filename, key: str | int = None):
|
|
351
|
+
def __init__(self, filename : str | BinaryIO, key: str | int = None):
|
|
589
352
|
"""Loads a USM file into memory and prepares it for processing.
|
|
590
353
|
|
|
591
354
|
Args:
|
|
@@ -601,7 +364,10 @@ class USM(USMCrypt):
|
|
|
601
364
|
self._load_file()
|
|
602
365
|
|
|
603
366
|
def _load_file(self):
|
|
604
|
-
|
|
367
|
+
if type(self.filename) == str:
|
|
368
|
+
self.stream = open(self.filename, "rb")
|
|
369
|
+
else:
|
|
370
|
+
self.stream = self.filename
|
|
605
371
|
self.stream.seek(0, 2)
|
|
606
372
|
self.size = self.stream.tell()
|
|
607
373
|
self.stream.seek(0)
|
|
@@ -716,13 +482,13 @@ class USM(USMCrypt):
|
|
|
716
482
|
|
|
717
483
|
@property
|
|
718
484
|
def streams(self):
|
|
719
|
-
"""[Type (@SFV, @SFA),
|
|
485
|
+
"""Generator of Tuple[Stream Type ("@SFV", "@SFA"), File name, Raw stream data]"""
|
|
720
486
|
for stream in self.CRIDObj.dictarray[1:]:
|
|
721
487
|
filename, stmid, chno = stream["filename"][1], stream["stmid"][1], stream["chno"][1]
|
|
722
488
|
stmid = int.to_bytes(stmid, 4, 'big', signed='False')
|
|
723
489
|
yield stmid, str(filename), self.output.get(f'{stmid.decode()}_{chno}', None)
|
|
724
490
|
|
|
725
|
-
def get_video(self):
|
|
491
|
+
def get_video(self) -> VP9Codec | H264Codec | MPEG1Codec:
|
|
726
492
|
"""Create a video codec from the available streams.
|
|
727
493
|
|
|
728
494
|
NOTE: A temporary file may be created with this process to determine the stream information."""
|
|
@@ -751,7 +517,7 @@ class USM(USMCrypt):
|
|
|
751
517
|
return []
|
|
752
518
|
|
|
753
519
|
class USMBuilder(USMCrypt):
|
|
754
|
-
"""
|
|
520
|
+
"""Use this class to build USM files."""
|
|
755
521
|
video_stream: VP9Codec | H264Codec | MPEG1Codec
|
|
756
522
|
audio_streams: List[HCACodec | ADXCodec]
|
|
757
523
|
|
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)
|