PyCriCodecsEx 0.0.2__cp311-cp311-win32.whl → 0.0.5__cp311-cp311-win32.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/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
- if self.stream.read(4) == b"smpl":
209
- self.stream.seek(-4, 1)
210
- self.looping = True
211
- # Will just be naming the important things here.
212
- smplsig, smplesize, _, _, _, _, _, _, _, self.LoopCount, _, _, _, self.LoopStartSample, self.LoopEndSample, _, _ = WavSmplHeaderStruct.unpack(
213
- self.stream.read(WavSmplHeaderStruct.size)
214
- )
215
- if self.LoopCount != 1:
216
- self.looping = False # Unsupported multiple looping points, so backtracks, and ignores looping data.
217
- self.stream.seek(-WavSmplHeaderStruct.size, 1)
218
- self.stream.seek(8 + smplesize, 1)
219
- else:
220
- self.stream.seek(-4, 1)
221
- self.looping = False
222
- if self.stream.read(4) == b"note": # There's no use for this on ADX.
223
- len = self.stream.read(4)
224
- self.stream.seek(len+4) # + 1? + padding maybe?
225
- else:
226
- self.stream.seek(-4, 1)
227
- if self.stream.read(4) == b"data":
228
- self.stream.seek(-4, 1)
229
- self.dataSig, self.dataSize = WavDataHeaderStruct.unpack(
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 raw, underlying video stream to a file.'''
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
- 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 = "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
- """USM class for extracting infromation and data from a USM file."""
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
- self.stream = open(self.filename, "rb")
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), Filename, Raw stream data]"""
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
- """USM class for building USM files."""
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, bytes]): The input stream or file path to read the UTF table from.
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
- Example:
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)