PyCriCodecsEx 0.0.1__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.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/usm.py ADDED
@@ -0,0 +1,1266 @@
1
+ import os
2
+ import itertools, shutil
3
+ from typing import BinaryIO, List
4
+ from io import FileIO, BytesIO
5
+ from functools import cached_property
6
+
7
+ from PyCriCodecsEx.chunk import *
8
+ from PyCriCodecsEx.utf import UTF, UTFBuilder
9
+ from PyCriCodecsEx.adx import ADX
10
+ from PyCriCodecsEx.hca import HCA
11
+ try:
12
+ import ffmpeg
13
+ except ImportError:
14
+ raise ImportError("ffmpeg-python is required for USM support. Install via PyCriCodecsEx[usm] extra.")
15
+ import tempfile
16
+
17
+ # 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.
23
+
24
+ class USMCrypt:
25
+ videomask1: bytearray
26
+ videomask2: bytearray
27
+ audiomask: bytearray
28
+
29
+ def init_key(self, key: str):
30
+ if type(key) == str:
31
+ if len(key) <= 16:
32
+ key = key.rjust(16, "0")
33
+ key1 = bytes.fromhex(key[8:])
34
+ key2 = bytes.fromhex(key[:8])
35
+ else:
36
+ raise ValueError("Invalid input key.")
37
+ elif type(key) == int:
38
+ key1 = int.to_bytes(key & 0xFFFFFFFF, 4, "big")
39
+ key2 = int.to_bytes(key >> 32, 4, "big")
40
+ else:
41
+ raise ValueError(
42
+ "Invalid key format, must be either a string or an integer."
43
+ )
44
+ t = bytearray(0x20)
45
+ t[0x00:0x09] = [
46
+ key1[3],
47
+ key1[2],
48
+ key1[1],
49
+ (key1[0] - 0x34) % 0x100,
50
+ (key2[3] + 0xF9) % 0x100,
51
+ (key2[2] ^ 0x13) % 0x100,
52
+ (key2[1] + 0x61) % 0x100,
53
+ (key1[3] ^ 0xFF) % 0x100,
54
+ (key1[1] + key1[2]) % 0x100,
55
+ ]
56
+ t[0x09:0x0C] = [
57
+ (t[0x01] - t[0x07]) % 0x100,
58
+ (t[0x02] ^ 0xFF) % 0x100,
59
+ (t[0x01] ^ 0xFF) % 0x100,
60
+ ]
61
+ t[0x0C:0x0E] = [
62
+ (t[0x0B] + t[0x09]) % 0x100,
63
+ (t[0x08] - t[0x03]) % 0x100,
64
+ ]
65
+ t[0x0E:0x10] = [
66
+ (t[0x0D] ^ 0xFF) % 0x100,
67
+ (t[0x0A] - t[0x0B]) % 0x100,
68
+ ]
69
+ t[0x10] = (t[0x08] - t[0x0F]) % 0x100
70
+ t[0x11:0x17] = [
71
+ (t[0x10] ^ t[0x07]) % 0x100,
72
+ (t[0x0F] ^ 0xFF) % 0x100,
73
+ (t[0x03] ^ 0x10) % 0x100,
74
+ (t[0x04] - 0x32) % 0x100,
75
+ (t[0x05] + 0xED) % 0x100,
76
+ (t[0x06] ^ 0xF3) % 0x100,
77
+ ]
78
+ t[0x17:0x1A] = [
79
+ (t[0x13] - t[0x0F]) % 0x100,
80
+ (t[0x15] + t[0x07]) % 0x100,
81
+ (0x21 - t[0x13]) % 0x100,
82
+ ]
83
+ t[0x1A:0x1C] = [
84
+ (t[0x14] ^ t[0x17]) % 0x100,
85
+ (t[0x16] + t[0x16]) % 0x100,
86
+ ]
87
+ t[0x1C:0x1F] = [
88
+ (t[0x17] + 0x44) % 0x100,
89
+ (t[0x03] + t[0x04]) % 0x100,
90
+ (t[0x05] - t[0x16]) % 0x100,
91
+ ]
92
+ t[0x1F] = (t[0x1D] ^ t[0x13]) % 0x100
93
+ t2 = [b"U", b"R", b"U", b"C"]
94
+ self.videomask1 = t
95
+ self.videomask2 = bytearray(map(lambda x: x ^ 0xFF, t))
96
+ self.audiomask = bytearray(0x20)
97
+ for x in range(0x20):
98
+ if (x & 1) == 1:
99
+ self.audiomask[x] = ord(t2[(x >> 1) & 3])
100
+ else:
101
+ self.audiomask[x] = self.videomask2[x]
102
+
103
+ # Decrypt SFV chunks or ALP chunks, should only be used if the video data is encrypted.
104
+ def VideoMask(self, memObj: bytearray) -> bytearray:
105
+ head = memObj[:0x40]
106
+ memObj = memObj[0x40:]
107
+ size = len(memObj)
108
+ # memObj len is a cached property, very fast to lookup
109
+ if size <= 0x200:
110
+ return head + memObj
111
+ data_view = memoryview(memObj).cast("Q")
112
+
113
+ # mask 2
114
+ mask = bytearray(self.videomask2)
115
+ mask_view = memoryview(mask).cast("Q")
116
+ vmask = self.videomask2
117
+ vmask_view = memoryview(vmask).cast("Q")
118
+
119
+ mask_index = 0
120
+
121
+ for i in range(32, size // 8):
122
+ data_view[i] ^= mask_view[mask_index]
123
+ mask_view[mask_index] = data_view[i] ^ vmask_view[mask_index]
124
+ mask_index = (mask_index + 1) % 4
125
+
126
+ # mask 1
127
+ mask = bytearray(self.videomask1)
128
+ mask_view = memoryview(mask).cast("Q")
129
+ mask_index = 0
130
+ for i in range(32):
131
+ mask_view[mask_index] ^= data_view[i + 32]
132
+ data_view[i] ^= mask_view[mask_index]
133
+ mask_index = (mask_index + 1) % 4
134
+
135
+ return head + memObj
136
+
137
+ # Decrypts SFA chunks, should just be used with ADX files.
138
+ def AudioMask(self, memObj: bytearray) -> bytearray:
139
+ head = memObj[:0x140]
140
+ memObj = memObj[0x140:]
141
+ size = len(memObj)
142
+ data_view = memoryview(memObj).cast("Q")
143
+ mask = bytearray(self.audiomask)
144
+ mask_view = memoryview(mask).cast("Q")
145
+ for i in range(size // 8):
146
+ data_view[i] ^= mask_view[i % 4]
147
+ return head + memObj
148
+
149
+
150
+ # There are a lot of unknowns, minbuf(minimum buffer of what?) and avbps(average bitrate per second)
151
+ # 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
+ # seems like it could be random values and the USM would still work.
153
+ class FFmpegCodec:
154
+ filename: str
155
+ filesize: int
156
+
157
+ info: dict
158
+ file: FileIO
159
+
160
+ minchk: int
161
+ minbuf: int
162
+ avbps: int
163
+
164
+ def __init__(self, stream: str | bytes):
165
+ if type(stream) == str:
166
+ self.filename = stream
167
+ else:
168
+ self.tempfile = tempfile.NamedTemporaryFile(delete=False)
169
+ self.tempfile.write(stream)
170
+ self.tempfile.close()
171
+ self.filename = self.tempfile.name
172
+ self.info = ffmpeg.probe(
173
+ self.filename, show_entries="packet=dts,pts_time,pos,flags,duration_time"
174
+ )
175
+ if type(stream) == str:
176
+ self.file = open(self.filename, "rb")
177
+ self.filesize = os.path.getsize(self.filename)
178
+ else:
179
+ os.unlink(self.tempfile.name)
180
+ self.file = BytesIO(stream)
181
+ self.filesize = len(stream)
182
+
183
+ @property
184
+ def format(self):
185
+ return self.info["format"]["format_name"]
186
+
187
+ @property
188
+ def stream(self) -> dict:
189
+ return self.info["streams"][0]
190
+
191
+ @property
192
+ def codec(self):
193
+ return self.stream["codec_name"]
194
+
195
+ @cached_property
196
+ def framerate(self):
197
+ """Running framerate (max frame rate)"""
198
+ # Lesson learned. Do NOT trust the metadata.
199
+ # num, denom = self.stream["r_frame_rate"].split("/")
200
+ # return int(int(num) / int(denom))
201
+ return 1 / min((dt for _, _, _, dt in self.frames()))
202
+
203
+ @cached_property
204
+ def avg_framerate(self):
205
+ """Average framerate"""
206
+ # avg_frame_rate = self.stream.get("avg_frame_rate", None)
207
+ # if avg_frame_rate:
208
+ # num, denom = avg_frame_rate.split("/")
209
+ # return int(int(num) / int(denom))
210
+ return self.frame_count / sum((dt for _, _, _, dt in self.frames()))
211
+
212
+ @property
213
+ def packets(self):
214
+ return self.info["packets"]
215
+
216
+ @property
217
+ def width(self):
218
+ return self.stream["width"]
219
+
220
+ @property
221
+ def height(self):
222
+ return self.stream["height"]
223
+
224
+ @property
225
+ def frame_count(self):
226
+ return len(self.packets)
227
+
228
+ def frames(self):
229
+ """frame data, frame dict, is keyframe, duration"""
230
+ offsets = [int(packet["pos"]) for packet in self.packets] + [self.filesize]
231
+ for i, frame in enumerate(self.packets):
232
+ frame_size = offsets[i + 1] - offsets[i]
233
+ self.file.seek(offsets[i])
234
+ raw_frame = self.file.read(frame_size)
235
+ yield raw_frame, frame, frame["flags"][0] == "K", float(frame["duration_time"])
236
+
237
+ def generate_SFV(self, builder: "USMBuilder"):
238
+ v_framerate = int(self.framerate)
239
+ current_interval = 0
240
+ SFV_list = []
241
+ SFV_chunk = b""
242
+ count = 0
243
+ self.minchk = 0
244
+ self.minbuf = 0
245
+ bitrate = 0
246
+ for data, _, is_keyframe, dt in self.frames():
247
+ # SFV has priority in chunks, it comes first.
248
+ datalen = len(data)
249
+ padlen = 0x20 - (datalen % 0x20) if datalen % 0x20 != 0 else 0
250
+ SFV_chunk = USMChunkHeader.pack(
251
+ USMChunckHeaderType.SFV.value,
252
+ datalen + 0x18 + padlen,
253
+ 0,
254
+ 0x18,
255
+ padlen,
256
+ 0,
257
+ 0,
258
+ 0,
259
+ 0,
260
+ int(current_interval),
261
+ v_framerate,
262
+ 0,
263
+ 0,
264
+ )
265
+ if builder.encrypt:
266
+ data = builder.VideoMask(data)
267
+ SFV_chunk += data
268
+ SFV_chunk = SFV_chunk.ljust(datalen + 0x18 + padlen + 0x8, b"\x00")
269
+ SFV_list.append(SFV_chunk)
270
+ count += 1
271
+ current_interval += 2997 * dt # 29.97 as base
272
+ if is_keyframe:
273
+ self.minchk += 1
274
+ if self.minbuf < datalen:
275
+ self.minbuf = datalen
276
+ bitrate += datalen * 8 * v_framerate
277
+ else:
278
+ self.avbps = int(bitrate / count)
279
+ SFV_chunk = USMChunkHeader.pack(
280
+ USMChunckHeaderType.SFV.value, 0x38, 0, 0x18, 0, 0, 0, 0, 2, 0, 30, 0, 0
281
+ )
282
+ SFV_chunk += b"#CONTENTS END ===============\x00"
283
+ SFV_list.append(SFV_chunk)
284
+ return SFV_list
285
+
286
+ def save(self, filepath: str):
287
+ '''Saves the raw, underlying video stream to a file.'''
288
+ tell = self.file.tell()
289
+ self.file.seek(0)
290
+ shutil.copyfileobj(self.file, open(filepath, 'wb'))
291
+ self.file.seek(tell)
292
+
293
+ class VP9Codec(FFmpegCodec):
294
+ MPEG_CODEC = 9
295
+ MPEG_DCPREC = 0
296
+ VERSION = 16777984
297
+
298
+ def __init__(self, filename: str | bytes):
299
+ super().__init__(filename)
300
+ assert self.format == "ivf", "must be ivf format."
301
+ class H264Codec(FFmpegCodec):
302
+ MPEG_CODEC = 5
303
+ MPEG_DCPREC = 11
304
+ VERSION = 0
305
+
306
+ def __init__(self, filename : str | bytes):
307
+ super().__init__(filename)
308
+ assert (
309
+ self.format == "h264"
310
+ ), "must be raw h264 data. transcode with '.h264' suffix as output"
311
+ class MPEG1Codec(FFmpegCodec):
312
+ MPEG_CODEC = 1
313
+ MPEG_DCPREC = 11
314
+ VERSION = 0
315
+
316
+ def __init__(self, stream : str | bytes):
317
+ super().__init__(stream)
318
+ assert self.format == "mpegvideo", "must be m1v format (mpegvideo)."
319
+
320
+ class HCACodec(HCA):
321
+ CHUNK_INTERVAL = 64
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, 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 save(self, filepath: str):
437
+ """Saves the decoded WAV audio to filepath"""
438
+ with open(filepath, "wb") as f:
439
+ f.write(self.decode())
440
+
441
+ class ADXCodec(ADX):
442
+ CHUNK_INTERVAL = 99.9
443
+ BASE_FRAMERATE = 2997
444
+ # TODO: Move these to an enum
445
+ AUDIO_CODEC = 2
446
+ METADATA_COUNT = 0
447
+
448
+ filename : str
449
+ filesize : int
450
+
451
+ adx : bytes
452
+ header : bytes
453
+ sfaStream: BinaryIO
454
+
455
+ AdxDataOffset: int
456
+ AdxEncoding: int
457
+ AdxBlocksize: int
458
+ AdxSampleBitdepth: int
459
+ AdxChannelCount: int
460
+ AdxSamplingRate: int
461
+ AdxSampleCount: int
462
+ AdxHighpassFrequency: int
463
+ AdxVersion: int
464
+ AdxFlags: int
465
+
466
+ chnls: int
467
+ sampling_rate: int
468
+ total_samples: int
469
+ avbps: int
470
+
471
+ def __init__(self, stream: str | bytes, filename: str, bitdepth: int = 4, **kwargs):
472
+ if type(stream) == str:
473
+ self.adx = open(stream, "rb").read()
474
+ else:
475
+ self.adx = stream
476
+ self.filename = filename
477
+ self.filesize = len(self.adx)
478
+ magic = self.adx[:4]
479
+ if magic == b"RIFF":
480
+ self.adx = self.encode(self.adx, bitdepth, force_not_looping=True)
481
+ self.sfaStream = BytesIO(self.adx)
482
+ header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
483
+ FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
484
+ assert FourCC == 0x8000, "Either ADX or WAV is supported"
485
+ assert self.AdxVersion in {3,4}, "Unsupported ADX version"
486
+ if self.AdxVersion == 4:
487
+ self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
488
+ self.sfaStream.seek(0)
489
+ self.chnls = self.AdxChannelCount
490
+ self.sampling_rate = self.AdxSamplingRate
491
+ self.total_samples = self.AdxSampleCount
492
+ self.avbps = int(self.filesize * 8 * self.chnls) - self.filesize
493
+
494
+ def generate_SFA(self, index: int, builder: "USMBuilder"):
495
+ current_interval = 0
496
+ stream_size = len(self.adx) - self.AdxBlocksize
497
+ chunk_size = int(self.AdxSamplingRate // (self.BASE_FRAMERATE / 100) // 32) * (self.AdxBlocksize * self.AdxChannelCount)
498
+ self.sfaStream.seek(0)
499
+ res = []
500
+ while self.sfaStream.tell() < stream_size:
501
+ if self.sfaStream.tell() > 0:
502
+ if self.sfaStream.tell() + chunk_size < stream_size:
503
+ datalen = chunk_size
504
+ else:
505
+ datalen = (stream_size - (self.AdxDataOffset + 4) - chunk_size) % chunk_size
506
+ else:
507
+ datalen = self.AdxDataOffset + 4
508
+ if not datalen:
509
+ break
510
+ padding = (0x20 - (datalen % 0x20) if datalen % 0x20 != 0 else 0)
511
+ SFA_chunk = USMChunkHeader.pack(
512
+ USMChunckHeaderType.SFA.value,
513
+ datalen + 0x18 + padding,
514
+ 0,
515
+ 0x18,
516
+ padding,
517
+ index,
518
+ 0,
519
+ 0,
520
+ 0,
521
+ round(current_interval),
522
+ self.BASE_FRAMERATE,
523
+ 0,
524
+ 0
525
+ )
526
+ chunk_data = self.sfaStream.read(datalen)
527
+ if builder.encrypt_audio:
528
+ SFA_chunk = builder.AudioMask(chunk_data)
529
+ SFA_chunk += chunk_data.ljust(datalen + padding, b"\x00")
530
+ current_interval += self.CHUNK_INTERVAL
531
+ res.append(SFA_chunk)
532
+ # ---
533
+ SFA_chunk = USMChunkHeader.pack(
534
+ USMChunckHeaderType.SFA.value,
535
+ 0x38,
536
+ 0,
537
+ 0x18,
538
+ 0,
539
+ index,
540
+ 0,
541
+ 0,
542
+ 2,
543
+ 0,
544
+ 30,
545
+ 0,
546
+ 0
547
+ )
548
+ SFA_chunk += b"#CONTENTS END ===============\x00"
549
+ res[-1] += SFA_chunk
550
+ return res
551
+
552
+ def get_metadata(self):
553
+ return None
554
+
555
+ def save(self, filepath: str):
556
+ """Saves the encoded ADX audio to filepath"""
557
+ with open(filepath, "wb") as f:
558
+ f.write(self.decode(self.adx))
559
+
560
+
561
+ class USM(USMCrypt):
562
+ """USM class for extracting infromation and data from a USM file."""
563
+
564
+ filename: str
565
+ decrypt: bool
566
+ stream: BinaryIO
567
+ CRIDObj: UTF
568
+ output: dict[str, bytes]
569
+ size: int
570
+ demuxed: bool
571
+
572
+ audio_codec: int
573
+ video_codec: int
574
+
575
+ metadata: list
576
+
577
+ def __init__(self, filename, key: str | int = None):
578
+ """Loads a USM file into memory and prepares it for processing.
579
+
580
+ Args:
581
+ filename (str): The path to the USM file.
582
+ key (str, optional): The decryption key. Either int64 or a hex string. Defaults to None.
583
+ """
584
+ self.filename = filename
585
+ self.decrypt = False
586
+
587
+ if key:
588
+ self.decrypt = True
589
+ self.init_key(key)
590
+ self._load_file()
591
+
592
+ def _load_file(self):
593
+ self.stream = open(self.filename, "rb")
594
+ self.stream.seek(0, 2)
595
+ self.size = self.stream.tell()
596
+ self.stream.seek(0)
597
+ header = self.stream.read(4)
598
+ if header != USMChunckHeaderType.CRID.value:
599
+ raise NotImplementedError(f"Unsupported file type: {header}")
600
+ self.stream.seek(0)
601
+ self._demux()
602
+
603
+ def _demux(self) -> None:
604
+ """Gets data from USM chunks and assignes them to output."""
605
+ self.stream.seek(0)
606
+ self.metadata = list()
607
+ (
608
+ header,
609
+ chuncksize,
610
+ unk08,
611
+ offset,
612
+ padding,
613
+ chno,
614
+ unk0D,
615
+ unk0E,
616
+ type,
617
+ frametime,
618
+ framerate,
619
+ unk18,
620
+ unk1C,
621
+ ) = USMChunkHeader.unpack(self.stream.read(USMChunkHeader.size))
622
+ chuncksize -= 0x18
623
+ offset -= 0x18
624
+ self.CRIDObj = UTF(self.stream.read(chuncksize))
625
+ CRID_payload = self.CRIDObj.dictarray
626
+ headers = [
627
+ (int.to_bytes(x["stmid"][1], 4, "big")).decode() for x in CRID_payload[1:]
628
+ ]
629
+ chnos = [x["chno"][1] for x in CRID_payload[1:]]
630
+ output = dict()
631
+ for i in range(len(headers)):
632
+ output[headers[i] + "_" + str(chnos[i])] = bytearray()
633
+ while self.stream.tell() < self.size:
634
+ header: bytes
635
+ (
636
+ header,
637
+ chuncksize,
638
+ unk08,
639
+ offset,
640
+ padding,
641
+ chno,
642
+ unk0D,
643
+ unk0E,
644
+ type,
645
+ frametime,
646
+ framerate,
647
+ unk18,
648
+ unk1C,
649
+ ) = USMChunkHeader.unpack(self.stream.read(USMChunkHeader.size))
650
+ chuncksize -= 0x18
651
+ offset -= 0x18
652
+ if header.decode() in headers:
653
+ if type == 0:
654
+ data = self._reader(chuncksize, offset, padding, header)
655
+ output[header.decode() + "_" + str(chno)].extend(data)
656
+ elif type == 1 or type == 3:
657
+ ChunkObj = UTF(self.stream.read(chuncksize))
658
+ self.metadata.append(ChunkObj)
659
+ if type == 1:
660
+ if header == USMChunckHeaderType.SFA.value:
661
+ codec = ChunkObj.dictarray[0]
662
+ self.audio_codec = codec["audio_codec"][1]
663
+ # So far, audio_codec of 2, means ADX, while audio_codec 4 means HCA.
664
+ if header == USMChunckHeaderType.SFV.value:
665
+ self.video_codec = ChunkObj.dictarray[0]['mpeg_codec'][1]
666
+ else:
667
+ self.stream.seek(chuncksize, 1)
668
+ else:
669
+ # It is likely impossible for the code to reach here, since the code right now is suitable
670
+ # for any chunk type specified in the CRID header.
671
+ # But just incase somehow there's an extra chunk, this code might handle it.
672
+ if header in [chunk.value for chunk in USMChunckHeaderType]:
673
+ if type == 0:
674
+ output[header.decode() + "_0"] = bytearray()
675
+ data = self._reader(chuncksize, offset, padding, header)
676
+ output[header.decode() + "_0"].extend(
677
+ data
678
+ ) # No channel number info, code here assumes it's a one channel data type.
679
+ elif type == 1 or type == 3:
680
+ ChunkObj = UTF(self.stream.read(chuncksize))
681
+ self.metadata.append(ChunkObj)
682
+ if type == 1 and header == USMChunckHeaderType.SFA.value:
683
+ codec = ChunkObj.dictarray[0]
684
+ self.audio_codec = codec["audio_codec"][1]
685
+ else:
686
+ self.stream.seek(chuncksize, 1)
687
+ else:
688
+ raise NotImplementedError(f"Unsupported chunk type: {header}")
689
+ self.output = output
690
+ self.demuxed = True
691
+
692
+ def _reader(self, chuncksize, offset, padding, header) -> bytearray:
693
+ """Chunks reader function, reads all data in a chunk and returns a bytearray."""
694
+ data = bytearray(self.stream.read(chuncksize)[offset:])
695
+ if (
696
+ header == USMChunckHeaderType.SFV.value
697
+ or header == USMChunckHeaderType.ALP.value
698
+ ):
699
+ data = self.VideoMask(data) if self.decrypt else data
700
+ elif header == USMChunckHeaderType.SFA.value:
701
+ data = self.AudioMask(data) if (self.audio_codec == 2 and self.decrypt) else data
702
+ if padding:
703
+ data = data[:-padding]
704
+ return data
705
+
706
+ @property
707
+ def streams(self):
708
+ """[Type (@SFV, @SFA), Filename, Raw stream data]"""
709
+ for stream in self.CRIDObj.dictarray[1:]:
710
+ filename, stmid, chno = stream["filename"][1], stream["stmid"][1], stream["chno"][1]
711
+ stmid = int.to_bytes(stmid, 4, 'big', signed='False')
712
+ yield stmid, str(filename), self.output.get(f'{stmid.decode()}_{chno}', None)
713
+
714
+ def get_video(self):
715
+ """Create a video codec from the available streams.
716
+
717
+ NOTE: A temporary file may be created with this process to determine the stream information."""
718
+ stype, sfname, sraw = next(filter(lambda x: x[0] == USMChunckHeaderType.SFV.value, self.streams), (None, None, None))
719
+ stream = None
720
+ match self.video_codec:
721
+ case MPEG1Codec.MPEG_CODEC:
722
+ stream = MPEG1Codec(sraw)
723
+ case H264Codec.MPEG_CODEC:
724
+ stream = H264Codec(sraw)
725
+ case VP9Codec.MPEG_CODEC:
726
+ stream = VP9Codec(sraw)
727
+ case _:
728
+ raise NotImplementedError(f"Unsupported video codec: {self.video_codec}")
729
+ stream.filename = sfname
730
+ return stream
731
+
732
+ def get_audios(self) -> List[HCACodec]:
733
+ """Create a list of audio codecs from the available streams."""
734
+ match self.audio_codec:
735
+ case ADXCodec.AUDIO_CODEC:
736
+ return [ADXCodec(s[2], s[1]) for s in self.streams if s[0] == USMChunckHeaderType.SFA.value]
737
+ case HCACodec.AUDIO_CODEC:
738
+ return [HCACodec(s[2], s[1]) for s in self.streams if s[0] == USMChunckHeaderType.SFA.value] # HCAs are never encrypted in USM
739
+ case _:
740
+ return []
741
+
742
+ class USMBuilder(USMCrypt):
743
+ """USM class for building USM files."""
744
+ video_stream: VP9Codec | H264Codec | MPEG1Codec
745
+
746
+ enable_audio: bool
747
+ audio_streams: List[HCACodec | ADXCodec]
748
+
749
+ key: int
750
+ encrypt: bool
751
+ encrypt_audio: bool
752
+
753
+ audio_codec: int
754
+ # !!: TODO Quality settings
755
+ def __init__(
756
+ self,
757
+ video: str,
758
+ audio: List[str] | str = None,
759
+ key = None,
760
+ audio_codec=HCACodec.AUDIO_CODEC,
761
+ encrypt_audio: bool = False,
762
+ ) -> None:
763
+ """Initialize the USMBuilder from set source files.
764
+
765
+ Args:
766
+ video (str): The path to the video file. The video source format will be used to map accordingly to the ones Sofdec use.
767
+ - MPEG1 (with M1V container): MPEG1 Codec (Sofdec Prime)
768
+ - H264 (with H264 container): H264 Codec
769
+ - VP9 (with IVF container): VP9 Codec
770
+ audio (List[str] | str, optional): The path(s) to the audio file(s). Defaults to None.
771
+ key (str | int, optional): The encryption key. Either int64 or a hex string. Defaults to None.
772
+ audio_codec (int, optional): The audio codec to use. Defaults to HCACodec.AUDIO_CODEC.
773
+ encrypt_audio (bool, optional): Whether to encrypt the audio. Defaults to False.
774
+ """
775
+ self.audio_codec = audio_codec
776
+ self.encrypt = False
777
+ self.enable_audio = False
778
+ self.encrypt_audio = encrypt_audio
779
+ self.key = 0
780
+ if encrypt_audio and not key:
781
+ raise ValueError("Cannot encrypt Audio without key.")
782
+ if key:
783
+ self.init_key(key)
784
+ self.encrypt = True
785
+ self.load_video(video)
786
+ self.audio_streams = []
787
+ if audio:
788
+ self.load_audio(audio)
789
+ self.enable_audio = True
790
+
791
+ def load_video(self, video):
792
+ temp_stream = FFmpegCodec(video)
793
+ self.video_stream = None
794
+ match temp_stream.stream["codec_name"]:
795
+ case "h264":
796
+ self.video_stream = H264Codec(video)
797
+ case "vp9":
798
+ self.video_stream = VP9Codec(video)
799
+ case "mpeg1video":
800
+ self.video_stream = MPEG1Codec(video)
801
+ assert self.video_stream, (
802
+ "fail to match suitable video codec. Codec=%s"
803
+ % temp_stream.stream["codec_name"]
804
+ )
805
+
806
+ def load_audio(self, audio):
807
+ self.audio_filenames = []
808
+ if type(audio) == list:
809
+ count = 0
810
+ for track in audio:
811
+ if type(track) == str:
812
+ self.audio_filenames.append(os.path.basename(track))
813
+ else:
814
+ self.audio_filenames.append("{:02d}.sfa".format(count))
815
+ count += 1
816
+ else:
817
+ if type(audio) == str:
818
+ self.audio_filenames.append(os.path.basename(audio))
819
+ else:
820
+ self.audio_filenames.append("00.sfa")
821
+
822
+ self.audio_streams = []
823
+ codec = None
824
+ match self.audio_codec:
825
+ case HCACodec.AUDIO_CODEC:
826
+ codec = HCACodec
827
+ case ADXCodec.AUDIO_CODEC:
828
+ codec = ADXCodec
829
+ assert codec, (
830
+ "fail to match suitable audio codec given option: %s" % self.audio_codec
831
+ )
832
+ if type(audio) == list:
833
+ for track in audio:
834
+ if type(track) == str:
835
+ fn = os.path.basename(track)
836
+ else:
837
+ fn = "{:02d}.sfa".format(count)
838
+ hcaObj = codec(track, fn, key=self.key)
839
+ self.audio_streams.append(hcaObj)
840
+ else:
841
+ if type(audio) == str:
842
+ fn = os.path.basename(audio)
843
+ else:
844
+ fn = "00.sfa"
845
+ hcaObj = codec(audio, fn, key=self.key)
846
+ self.audio_streams.append(hcaObj)
847
+
848
+
849
+ def build(self) -> bytes:
850
+ SFV_list = self.video_stream.generate_SFV(self)
851
+ if self.enable_audio:
852
+ SFA_chunks = [s.generate_SFA(i, self) for i, s in enumerate(self.audio_streams) ]
853
+ else:
854
+ SFA_chunks = []
855
+ SBT_chunks = [] # TODO: Subtitles
856
+ header = self._build_header(SFV_list, SFA_chunks, SBT_chunks)
857
+ chunks = list(itertools.chain(SFV_list, *SFA_chunks))
858
+
859
+ def chunk_key_sort(chunk):
860
+ (
861
+ header,
862
+ chuncksize,
863
+ unk08,
864
+ offset,
865
+ padding,
866
+ chno,
867
+ unk0D,
868
+ unk0E,
869
+ type,
870
+ frametime,
871
+ framerate,
872
+ unk18,
873
+ unk1C,
874
+ ) = USMChunkHeader.unpack(chunk[: USMChunkHeader.size])
875
+ prio = 0 if header == USMChunckHeaderType.SFV else 1
876
+ # all stream chunks before section_end chunks, then sort by frametime, with SFV chunks before SFA chunks
877
+ return (type, frametime, prio)
878
+
879
+ chunks.sort(key=chunk_key_sort)
880
+ self.usm = header
881
+ chunks = b''.join(chunks)
882
+ self.usm += chunks
883
+ return self.usm
884
+
885
+ def _build_header(
886
+ self, SFV_list: list, SFA_chunks: list, SBT_chunks: list # TODO: Not used
887
+ ) -> bytes:
888
+ # Main USM file
889
+ CRIUSF_DIR_STREAM = [
890
+ dict(
891
+ fmtver=(UTFTypeValues.uint, self.video_stream.VERSION),
892
+ filename=(
893
+ UTFTypeValues.string,
894
+ os.path.splitext(os.path.basename(self.video_stream.filename))[0]
895
+ + ".usm",
896
+ ),
897
+ filesize=(UTFTypeValues.uint, -1), # Will be updated later.
898
+ datasize=(UTFTypeValues.uint, 0),
899
+ stmid=(UTFTypeValues.uint, 0),
900
+ chno=(UTFTypeValues.ushort, 0xFFFF),
901
+ minchk=(UTFTypeValues.ushort, 1),
902
+ minbuf=(UTFTypeValues.uint, -1), # Will be updated later.
903
+ avbps=(UTFTypeValues.uint, -1), # Will be updated later.
904
+ )
905
+ ]
906
+
907
+ total_avbps = self.video_stream.avbps
908
+ minbuf = 4 + self.video_stream.minbuf
909
+
910
+ v_filesize = self.video_stream.filesize
911
+
912
+ video_dict = dict(
913
+ fmtver=(UTFTypeValues.uint, self.video_stream.VERSION),
914
+ filename=(
915
+ UTFTypeValues.string,
916
+ os.path.basename(self.video_stream.filename),
917
+ ),
918
+ filesize=(UTFTypeValues.uint, v_filesize),
919
+ datasize=(UTFTypeValues.uint, 0),
920
+ stmid=(
921
+ UTFTypeValues.uint,
922
+ int.from_bytes(USMChunckHeaderType.SFV.value, "big"),
923
+ ),
924
+ chno=(UTFTypeValues.ushort, 0),
925
+ minchk=(UTFTypeValues.ushort, self.video_stream.minchk),
926
+ minbuf=(UTFTypeValues.uint, self.video_stream.minbuf),
927
+ avbps=(UTFTypeValues.uint, self.video_stream.avbps),
928
+ )
929
+ CRIUSF_DIR_STREAM.append(video_dict)
930
+
931
+ if self.enable_audio:
932
+ chno = 0
933
+ for stream in self.audio_streams:
934
+ avbps = stream.avbps
935
+ total_avbps += avbps
936
+ minbuf += 27860
937
+ audio_dict = dict(
938
+ fmtver=(UTFTypeValues.uint, 0),
939
+ filename=(UTFTypeValues.string, self.audio_filenames[chno]),
940
+ filesize=(UTFTypeValues.uint, stream.filesize),
941
+ datasize=(UTFTypeValues.uint, 0),
942
+ stmid=(
943
+ UTFTypeValues.uint,
944
+ int.from_bytes(USMChunckHeaderType.SFA.value, "big"),
945
+ ),
946
+ chno=(UTFTypeValues.ushort, chno),
947
+ minchk=(UTFTypeValues.ushort, 1),
948
+ minbuf=(
949
+ UTFTypeValues.uint,
950
+ 27860,
951
+ ), # minbuf is fixed at that for audio.
952
+ avbps=(UTFTypeValues.uint, avbps),
953
+ )
954
+ CRIUSF_DIR_STREAM.append(audio_dict)
955
+ chno += 1
956
+
957
+ CRIUSF_DIR_STREAM[0]["avbps"] = (UTFTypeValues.uint, total_avbps)
958
+ CRIUSF_DIR_STREAM[0]["minbuf"] = (
959
+ UTFTypeValues.uint,
960
+ minbuf,
961
+ ) # Wrong. TODO Despite being fixed per SFA stream, seems to change internally before summation.
962
+
963
+ def gen_video_hdr_info(metadata_size: int):
964
+ hdr = [
965
+ {
966
+ "width": (UTFTypeValues.uint, self.video_stream.width),
967
+ "height": (UTFTypeValues.uint, self.video_stream.height),
968
+ "mat_width": (UTFTypeValues.uint, self.video_stream.width),
969
+ "mat_height": (UTFTypeValues.uint, self.video_stream.height),
970
+ "disp_width": (UTFTypeValues.uint, self.video_stream.width),
971
+ "disp_height": (UTFTypeValues.uint, self.video_stream.height),
972
+ "scrn_width": (UTFTypeValues.uint, 0),
973
+ "mpeg_dcprec": (UTFTypeValues.uchar, self.video_stream.MPEG_DCPREC),
974
+ "mpeg_codec": (UTFTypeValues.uchar, self.video_stream.MPEG_CODEC),
975
+ "alpha_type": (UTFTypeValues.uint, 0),
976
+ "total_frames": (UTFTypeValues.uint, self.video_stream.frame_count),
977
+ "framerate_n": (
978
+ UTFTypeValues.uint,
979
+ int(self.video_stream.framerate * 1000),
980
+ ),
981
+ "framerate_d": (UTFTypeValues.uint, 1000), # Denominator
982
+ "metadata_count": (
983
+ UTFTypeValues.uint,
984
+ 1,
985
+ ), # Could be 0 and ignore metadata?
986
+ "metadata_size": (
987
+ UTFTypeValues.uint,
988
+ metadata_size,
989
+ ),
990
+ "ixsize": (UTFTypeValues.uint, self.video_stream.minbuf),
991
+ "pre_padding": (UTFTypeValues.uint, 0),
992
+ "max_picture_size": (UTFTypeValues.uint, 0),
993
+ "color_space": (UTFTypeValues.uint, 0),
994
+ "picture_type": (UTFTypeValues.uint, 0),
995
+ }
996
+ ]
997
+ v = UTFBuilder(hdr, table_name="VIDEO_HDRINFO")
998
+ v.strings = b"<NULL>\x00" + v.strings
999
+ hdr = v.bytes()
1000
+ padding = 0x20 - (len(hdr) % 0x20) if (len(hdr) % 0x20) != 0 else 0
1001
+ chk = USMChunkHeader.pack(
1002
+ USMChunckHeaderType.SFV.value,
1003
+ len(hdr) + 0x18 + padding,
1004
+ 0,
1005
+ 0x18,
1006
+ padding,
1007
+ 0,
1008
+ 0,
1009
+ 0,
1010
+ 1,
1011
+ 0,
1012
+ 30,
1013
+ 0,
1014
+ 0,
1015
+ )
1016
+ chk += hdr.ljust(len(hdr) + padding, b"\x00")
1017
+ return chk
1018
+
1019
+ audio_metadata = []
1020
+ audio_headers = []
1021
+ if self.enable_audio:
1022
+ chno = 0
1023
+ for stream in self.audio_streams:
1024
+ metadata = stream.get_metadata()
1025
+ if not metadata:
1026
+ break
1027
+ else:
1028
+ padding = (
1029
+ 0x20 - (len(metadata) % 0x20)
1030
+ if len(metadata) % 0x20 != 0
1031
+ else 0
1032
+ )
1033
+ chk = USMChunkHeader.pack(
1034
+ USMChunckHeaderType.SFA.value,
1035
+ len(metadata) + 0x18 + padding,
1036
+ 0,
1037
+ 0x18,
1038
+ padding,
1039
+ chno,
1040
+ 0,
1041
+ 0,
1042
+ 3,
1043
+ 0,
1044
+ 30,
1045
+ 0,
1046
+ 0,
1047
+ )
1048
+ chk += metadata.ljust(len(metadata) + padding, b"\x00")
1049
+ audio_metadata.append(chk)
1050
+ chno += 1
1051
+
1052
+ chno = 0
1053
+ for stream in self.audio_streams:
1054
+ AUDIO_HDRINFO = [
1055
+ {
1056
+ "audio_codec": (UTFTypeValues.uchar, stream.AUDIO_CODEC),
1057
+ "sampling_rate": (UTFTypeValues.uint, stream.sampling_rate),
1058
+ "total_samples": (UTFTypeValues.uint, stream.total_samples),
1059
+ "num_channels": (UTFTypeValues.uchar, stream.chnls),
1060
+ "metadata_count": (UTFTypeValues.uint, stream.METADATA_COUNT),
1061
+ "metadat_size": (UTFTypeValues.uint, len(audio_metadata[chno]) if audio_metadata else 0),
1062
+ "ixsize": (UTFTypeValues.uint, 27860),
1063
+ "ambisonics": (UTFTypeValues.uint, 0)
1064
+ }
1065
+ ]
1066
+ p = UTFBuilder(AUDIO_HDRINFO, table_name="AUDIO_HDRINFO")
1067
+ p.strings = b"<NULL>\x00" + p.strings
1068
+ header = p.bytes()
1069
+ padding = (
1070
+ 0x20 - (len(header) % 0x20) if (len(header) % 0x20) != 0 else 0
1071
+ )
1072
+ chk = USMChunkHeader.pack(
1073
+ USMChunckHeaderType.SFA.value,
1074
+ len(header) + 0x18 + padding,
1075
+ 0,
1076
+ 0x18,
1077
+ padding,
1078
+ chno,
1079
+ 0,
1080
+ 0,
1081
+ 1,
1082
+ 0,
1083
+ 30,
1084
+ 0,
1085
+ 0,
1086
+ )
1087
+ chk += header.ljust(len(header) + padding, b"\x00")
1088
+ audio_headers.append(chk)
1089
+ chno += 1
1090
+
1091
+ keyframes = [
1092
+ (data["pos"], i)
1093
+ for i, (frame, data, is_keyframe, duration) in enumerate(self.video_stream.frames())
1094
+ if is_keyframe
1095
+ ]
1096
+
1097
+ def comp_seek_info(first_chk_ofs):
1098
+ seek = [
1099
+ {
1100
+ "ofs_byte": (UTFTypeValues.ullong, first_chk_ofs + int(pos)),
1101
+ "ofs_frmid": (UTFTypeValues.int, i),
1102
+ "num_skip": (UTFTypeValues.short, 0),
1103
+ "resv": (UTFTypeValues.short, 0),
1104
+ }
1105
+ for pos, i in keyframes
1106
+ ]
1107
+ seek = UTFBuilder(seek, table_name="VIDEO_SEEKINFO")
1108
+ seek.strings = b"<NULL>\x00" + seek.strings
1109
+ seek = seek.bytes()
1110
+ padding = 0x20 - len(seek) % 0x20 if len(seek) % 0x20 != 0 else 0
1111
+ seekinf = USMChunkHeader.pack(
1112
+ USMChunckHeaderType.SFV.value,
1113
+ len(seek) + 0x18 + padding,
1114
+ 0,
1115
+ 0x18,
1116
+ padding,
1117
+ 0,
1118
+ 0,
1119
+ 0,
1120
+ 3,
1121
+ 0,
1122
+ 30,
1123
+ 0,
1124
+ 0,
1125
+ )
1126
+ seekinf += seek.ljust(len(seek) + padding, b"\x00")
1127
+ return seekinf
1128
+
1129
+ len_seek = len(comp_seek_info(0))
1130
+ len_audio_headers = sum([len(x) + 0x40 for x in audio_headers])
1131
+ len_audio_metadata = sum([len(x) + 0x40 for x in audio_metadata])
1132
+ first_chk_ofs = (
1133
+ 0x800 # CRID
1134
+ + 512 # VIDEO_HDRINFO
1135
+ + len_seek
1136
+ + 128 # SFV_END * 2
1137
+ + len_audio_headers
1138
+ + len_audio_metadata
1139
+ )
1140
+ VIDEO_SEEKINFO = comp_seek_info(first_chk_ofs)
1141
+ VIDEO_HDRINFO = gen_video_hdr_info(len(VIDEO_SEEKINFO))
1142
+
1143
+ total_len = sum([len(x) for x in SFV_list]) + first_chk_ofs
1144
+ if self.enable_audio:
1145
+ sum_len = 0
1146
+ for stream in SFA_chunks:
1147
+ for x in stream:
1148
+ sum_len += len(x)
1149
+ total_len += sum_len
1150
+
1151
+ CRIUSF_DIR_STREAM[0]["filesize"] = (UTFTypeValues.uint, total_len)
1152
+ CRIUSF_DIR_STREAM = UTFBuilder(
1153
+ CRIUSF_DIR_STREAM, table_name="CRIUSF_DIR_STREAM"
1154
+ )
1155
+ CRIUSF_DIR_STREAM.strings = b"<NULL>\x00" + CRIUSF_DIR_STREAM.strings
1156
+ CRIUSF_DIR_STREAM = CRIUSF_DIR_STREAM.bytes()
1157
+
1158
+ ##############################################
1159
+ # Parsing everything.
1160
+ ##############################################
1161
+ header = bytes()
1162
+ # CRID
1163
+ padding = 0x800 - len(CRIUSF_DIR_STREAM)
1164
+ CRID = USMChunkHeader.pack(
1165
+ USMChunckHeaderType.CRID.value,
1166
+ 0x800 - 0x8,
1167
+ 0,
1168
+ 0x18,
1169
+ padding - 0x20,
1170
+ 0,
1171
+ 0,
1172
+ 0,
1173
+ 1,
1174
+ 0,
1175
+ 30,
1176
+ 0,
1177
+ 0,
1178
+ )
1179
+ CRID += CRIUSF_DIR_STREAM.ljust(0x800 - 0x20, b"\x00")
1180
+ header += CRID
1181
+
1182
+ # Header chunks
1183
+ header += VIDEO_HDRINFO
1184
+ if self.enable_audio:
1185
+ header += b''.join(audio_headers)
1186
+ SFV_END = USMChunkHeader.pack(
1187
+ USMChunckHeaderType.SFV.value,
1188
+ 0x38,
1189
+ 0,
1190
+ 0x18,
1191
+ 0x0,
1192
+ 0x0,
1193
+ 0x0,
1194
+ 0x0,
1195
+ 2,
1196
+ 0,
1197
+ 30,
1198
+ 0,
1199
+ 0,
1200
+ )
1201
+ SFV_END += b"#HEADER END ===============\x00"
1202
+ header += SFV_END
1203
+
1204
+ SFA_chk_END = b'' # Maybe reused
1205
+ if self.enable_audio:
1206
+ SFA_chk_END = b''.join([
1207
+ USMChunkHeader.pack(
1208
+ USMChunckHeaderType.SFA.value,
1209
+ 0x38,
1210
+ 0,
1211
+ 0x18,
1212
+ 0x0,
1213
+ i,
1214
+ 0x0,
1215
+ 0x0,
1216
+ 2,
1217
+ 0,
1218
+ 30,
1219
+ 0,
1220
+ 0,
1221
+ ) + b"#HEADER END ===============\x00" for i in range(len(audio_headers))
1222
+ ])
1223
+ header += SFA_chk_END # Ends audio_headers
1224
+ header += VIDEO_SEEKINFO
1225
+
1226
+ if self.enable_audio:
1227
+ header += b''.join(audio_metadata)
1228
+ SFV_END = USMChunkHeader.pack(
1229
+ USMChunckHeaderType.SFV.value,
1230
+ 0x38,
1231
+ 0,
1232
+ 0x18,
1233
+ 0x0,
1234
+ 0x0,
1235
+ 0x0,
1236
+ 0x0,
1237
+ 2,
1238
+ 0,
1239
+ 30,
1240
+ 0,
1241
+ 0,
1242
+ )
1243
+ SFV_END += b"#METADATA END ===============\x00"
1244
+ header += SFV_END
1245
+
1246
+ if audio_metadata:
1247
+ SFA_chk_END = b''.join([
1248
+ USMChunkHeader.pack(
1249
+ USMChunckHeaderType.SFA.value,
1250
+ 0x38,
1251
+ 0,
1252
+ 0x18,
1253
+ 0x0,
1254
+ i,
1255
+ 0x0,
1256
+ 0x0,
1257
+ 2,
1258
+ 0,
1259
+ 30,
1260
+ 0,
1261
+ 0,
1262
+ ) + b"#METADATA END ===============\x00" for i in range(len(audio_headers))
1263
+ ])
1264
+ header += SFA_chk_END # Ends audio_headers
1265
+
1266
+ return header