PyCriCodecsEx 0.0.2__cp311-cp311-macosx_11_0_arm64.whl → 0.0.4__cp311-cp311-macosx_11_0_arm64.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-darwin.so +0 -0
- PyCriCodecsEx/__init__.py +1 -1
- PyCriCodecsEx/acb.py +57 -26
- 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 +31 -261
- PyCriCodecsEx/utf.py +8 -24
- pycricodecsex-0.0.4.dist-info/METADATA +35 -0
- pycricodecsex-0.0.4.dist-info/RECORD +15 -0
- pycricodecsex-0.0.2.dist-info/METADATA +0 -86
- pycricodecsex-0.0.2.dist-info/RECORD +0 -15
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.4.dist-info}/WHEEL +0 -0
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.4.dist-info}/licenses/LICENSE +0 -0
- {pycricodecsex-0.0.2.dist-info → pycricodecsex-0.0.4.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): 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) -> 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:
|
|
@@ -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
|
|
@@ -151,6 +150,7 @@ class USMCrypt:
|
|
|
151
150
|
# 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
151
|
# seems like it could be random values and the USM would still work.
|
|
153
152
|
class FFmpegCodec:
|
|
153
|
+
"""Base codec for FFMpeg-based Video streams"""
|
|
154
154
|
filename: str
|
|
155
155
|
filesize: int
|
|
156
156
|
|
|
@@ -162,6 +162,13 @@ class FFmpegCodec:
|
|
|
162
162
|
avbps: int
|
|
163
163
|
|
|
164
164
|
def __init__(self, stream: str | bytes):
|
|
165
|
+
"""Initialize FFmpegCodec with a media stream, gathering metadata and frame info.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
stream (str | bytes): The media stream to process.
|
|
169
|
+
NOTE:
|
|
170
|
+
A temp file maybe created for probing only. Which will be deleted after use.
|
|
171
|
+
"""
|
|
165
172
|
if type(stream) == str:
|
|
166
173
|
self.filename = stream
|
|
167
174
|
else:
|
|
@@ -226,7 +233,7 @@ class FFmpegCodec:
|
|
|
226
233
|
return len(self.packets)
|
|
227
234
|
|
|
228
235
|
def frames(self):
|
|
229
|
-
"""frame data, frame dict, is keyframe, duration"""
|
|
236
|
+
"""Generator of [frame data, frame dict, is keyframe, duration]"""
|
|
230
237
|
offsets = [int(packet["pos"]) for packet in self.packets] + [self.filesize]
|
|
231
238
|
for i, frame in enumerate(self.packets):
|
|
232
239
|
frame_size = offsets[i + 1] - offsets[i]
|
|
@@ -284,13 +291,16 @@ class FFmpegCodec:
|
|
|
284
291
|
return SFV_list
|
|
285
292
|
|
|
286
293
|
def save(self, filepath: str):
|
|
287
|
-
'''Saves the
|
|
294
|
+
'''Saves the underlying video stream to a file.'''
|
|
288
295
|
tell = self.file.tell()
|
|
289
296
|
self.file.seek(0)
|
|
290
297
|
shutil.copyfileobj(self.file, open(filepath, 'wb'))
|
|
291
298
|
self.file.seek(tell)
|
|
292
299
|
|
|
293
300
|
class VP9Codec(FFmpegCodec):
|
|
301
|
+
"""VP9 Video stream codec.
|
|
302
|
+
|
|
303
|
+
Only streams with `.ivf` containers are supported."""
|
|
294
304
|
MPEG_CODEC = 9
|
|
295
305
|
MPEG_DCPREC = 0
|
|
296
306
|
VERSION = 16777984
|
|
@@ -299,6 +309,9 @@ class VP9Codec(FFmpegCodec):
|
|
|
299
309
|
super().__init__(filename)
|
|
300
310
|
assert self.format == "ivf", "must be ivf format."
|
|
301
311
|
class H264Codec(FFmpegCodec):
|
|
312
|
+
"""H264 Video stream codec.
|
|
313
|
+
|
|
314
|
+
Only streams with `.h264` containers are supported."""
|
|
302
315
|
MPEG_CODEC = 5
|
|
303
316
|
MPEG_DCPREC = 11
|
|
304
317
|
VERSION = 0
|
|
@@ -309,6 +322,9 @@ class H264Codec(FFmpegCodec):
|
|
|
309
322
|
self.format == "h264"
|
|
310
323
|
), "must be raw h264 data. transcode with '.h264' suffix as output"
|
|
311
324
|
class MPEG1Codec(FFmpegCodec):
|
|
325
|
+
"""MPEG1 Video stream codec.
|
|
326
|
+
|
|
327
|
+
Only streams with `.mpeg1` containers are supported."""
|
|
312
328
|
MPEG_CODEC = 1
|
|
313
329
|
MPEG_DCPREC = 11
|
|
314
330
|
VERSION = 0
|
|
@@ -317,260 +333,11 @@ class MPEG1Codec(FFmpegCodec):
|
|
|
317
333
|
super().__init__(stream)
|
|
318
334
|
assert self.format == "mpegvideo", "must be m1v format (mpegvideo)."
|
|
319
335
|
|
|
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
|
-
|
|
336
|
+
from PyCriCodecsEx.hca import HCACodec
|
|
337
|
+
from PyCriCodecsEx.adx import ADXCodec
|
|
571
338
|
|
|
572
339
|
class USM(USMCrypt):
|
|
573
|
-
"""
|
|
340
|
+
"""Use this class to extract infromation and data from a USM file."""
|
|
574
341
|
|
|
575
342
|
filename: str
|
|
576
343
|
decrypt: bool
|
|
@@ -585,7 +352,7 @@ class USM(USMCrypt):
|
|
|
585
352
|
|
|
586
353
|
metadata: list
|
|
587
354
|
|
|
588
|
-
def __init__(self, filename, key: str | int = None):
|
|
355
|
+
def __init__(self, filename : str | BinaryIO, key: str | int = None):
|
|
589
356
|
"""Loads a USM file into memory and prepares it for processing.
|
|
590
357
|
|
|
591
358
|
Args:
|
|
@@ -601,7 +368,10 @@ class USM(USMCrypt):
|
|
|
601
368
|
self._load_file()
|
|
602
369
|
|
|
603
370
|
def _load_file(self):
|
|
604
|
-
|
|
371
|
+
if type(self.filename) == str:
|
|
372
|
+
self.stream = open(self.filename, "rb")
|
|
373
|
+
else:
|
|
374
|
+
self.stream = self.filename
|
|
605
375
|
self.stream.seek(0, 2)
|
|
606
376
|
self.size = self.stream.tell()
|
|
607
377
|
self.stream.seek(0)
|
|
@@ -716,13 +486,13 @@ class USM(USMCrypt):
|
|
|
716
486
|
|
|
717
487
|
@property
|
|
718
488
|
def streams(self):
|
|
719
|
-
"""[Type (@SFV, @SFA),
|
|
489
|
+
"""Generator of Tuple[Stream Type ("@SFV", "@SFA"), File name, Raw stream data]"""
|
|
720
490
|
for stream in self.CRIDObj.dictarray[1:]:
|
|
721
491
|
filename, stmid, chno = stream["filename"][1], stream["stmid"][1], stream["chno"][1]
|
|
722
492
|
stmid = int.to_bytes(stmid, 4, 'big', signed='False')
|
|
723
493
|
yield stmid, str(filename), self.output.get(f'{stmid.decode()}_{chno}', None)
|
|
724
494
|
|
|
725
|
-
def get_video(self):
|
|
495
|
+
def get_video(self) -> VP9Codec | H264Codec | MPEG1Codec:
|
|
726
496
|
"""Create a video codec from the available streams.
|
|
727
497
|
|
|
728
498
|
NOTE: A temporary file may be created with this process to determine the stream information."""
|
|
@@ -751,7 +521,7 @@ class USM(USMCrypt):
|
|
|
751
521
|
return []
|
|
752
522
|
|
|
753
523
|
class USMBuilder(USMCrypt):
|
|
754
|
-
"""
|
|
524
|
+
"""Use this class to build USM files."""
|
|
755
525
|
video_stream: VP9Codec | H264Codec | MPEG1Codec
|
|
756
526
|
audio_streams: List[HCACodec | ADXCodec]
|
|
757
527
|
|
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)
|