PyCriCodecsEx 0.0.1__cp312-cp312-musllinux_1_2_i686.whl → 0.0.3__cp312-cp312-musllinux_1_2_i686.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PyCriCodecsEx/usm.py CHANGED
@@ -6,8 +6,6 @@ from functools import cached_property
6
6
 
7
7
  from PyCriCodecsEx.chunk import *
8
8
  from PyCriCodecsEx.utf import UTF, UTFBuilder
9
- from PyCriCodecsEx.adx import ADX
10
- from PyCriCodecsEx.hca import HCA
11
9
  try:
12
10
  import ffmpeg
13
11
  except ImportError:
@@ -22,6 +20,7 @@ import tempfile
22
20
  # code for a complete breakdown of the USM format.
23
21
 
24
22
  class USMCrypt:
23
+ """USM related crypto functions"""
25
24
  videomask1: bytearray
26
25
  videomask2: bytearray
27
26
  audiomask: bytearray
@@ -162,6 +161,13 @@ class FFmpegCodec:
162
161
  avbps: int
163
162
 
164
163
  def __init__(self, stream: str | bytes):
164
+ """Initialize FFmpegCodec with a media stream, gathering metadata and frame info.
165
+
166
+ Args:
167
+ stream (str | bytes): The media stream to process.
168
+ NOTE:
169
+ A temp file maybe created for probing only. Which will be deleted after use.
170
+ """
165
171
  if type(stream) == str:
166
172
  self.filename = stream
167
173
  else:
@@ -317,249 +323,11 @@ class MPEG1Codec(FFmpegCodec):
317
323
  super().__init__(stream)
318
324
  assert self.format == "mpegvideo", "must be m1v format (mpegvideo)."
319
325
 
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
-
326
+ from PyCriCodecsEx.hca import HCACodec
327
+ from PyCriCodecsEx.adx import ADXCodec
560
328
 
561
329
  class USM(USMCrypt):
562
- """USM class for extracting infromation and data from a USM file."""
330
+ """Use this class to extract infromation and data from a USM file."""
563
331
 
564
332
  filename: str
565
333
  decrypt: bool
@@ -574,7 +342,7 @@ class USM(USMCrypt):
574
342
 
575
343
  metadata: list
576
344
 
577
- def __init__(self, filename, key: str | int = None):
345
+ def __init__(self, filename : str | BinaryIO, key: str | int = None):
578
346
  """Loads a USM file into memory and prepares it for processing.
579
347
 
580
348
  Args:
@@ -590,7 +358,10 @@ class USM(USMCrypt):
590
358
  self._load_file()
591
359
 
592
360
  def _load_file(self):
593
- self.stream = open(self.filename, "rb")
361
+ if type(self.filename) == str:
362
+ self.stream = open(self.filename, "rb")
363
+ else:
364
+ self.stream = self.filename
594
365
  self.stream.seek(0, 2)
595
366
  self.size = self.stream.tell()
596
367
  self.stream.seek(0)
@@ -705,7 +476,7 @@ class USM(USMCrypt):
705
476
 
706
477
  @property
707
478
  def streams(self):
708
- """[Type (@SFV, @SFA), Filename, Raw stream data]"""
479
+ """Generator of Tuple[Stream Type ("@SFV", "@SFA"), File name, Raw stream data]"""
709
480
  for stream in self.CRIDObj.dictarray[1:]:
710
481
  filename, stmid, chno = stream["filename"][1], stream["stmid"][1], stream["chno"][1]
711
482
  stmid = int.to_bytes(stmid, 4, 'big', signed='False')
@@ -729,7 +500,7 @@ class USM(USMCrypt):
729
500
  stream.filename = sfname
730
501
  return stream
731
502
 
732
- def get_audios(self) -> List[HCACodec]:
503
+ def get_audios(self) -> List[ADXCodec | HCACodec]:
733
504
  """Create a list of audio codecs from the available streams."""
734
505
  match self.audio_codec:
735
506
  case ADXCodec.AUDIO_CODEC:
@@ -740,115 +511,73 @@ class USM(USMCrypt):
740
511
  return []
741
512
 
742
513
  class USMBuilder(USMCrypt):
743
- """USM class for building USM files."""
514
+ """Use this class to build USM files."""
744
515
  video_stream: VP9Codec | H264Codec | MPEG1Codec
745
-
746
- enable_audio: bool
747
516
  audio_streams: List[HCACodec | ADXCodec]
748
517
 
749
- key: int
750
- encrypt: bool
751
- encrypt_audio: bool
518
+ key: int = None
519
+ encrypt: bool = False
520
+ encrypt_audio: bool = False
752
521
 
753
- audio_codec: int
754
- # !!: TODO Quality settings
755
522
  def __init__(
756
523
  self,
757
- video: str,
758
- audio: List[str] | str = None,
759
524
  key = None,
760
- audio_codec=HCACodec.AUDIO_CODEC,
761
- encrypt_audio: bool = False,
525
+ encrypt_audio = False
762
526
  ) -> None:
763
527
  """Initialize the USMBuilder from set source files.
764
528
 
765
529
  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
530
  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.
531
+ encrypt_audio (bool, optional): Whether to also encrypt the audio. Defaults to False.
774
532
  """
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
533
  if key:
783
534
  self.init_key(key)
784
535
  self.encrypt = True
785
- self.load_video(video)
536
+ self.encrypt_audio = encrypt_audio
786
537
  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
538
 
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")
539
+ def add_video(self, video : str | H264Codec | VP9Codec | MPEG1Codec):
540
+ """Sets the video stream from the specified video file.
821
541
 
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)
542
+ USMs only support one video stream. Consecutive calls to this method will replace the existing video stream.
543
+
544
+ When `video` is str - it will be treated as a file path. The video source format will be used to map accordingly to the ones Sofdec use.
545
+ - MPEG1 (with M1V container): MPEG1 Codec (Sofdec Prime)
546
+ - H264 (with H264 container): H264 Codec
547
+ - VP9 (with IVF container): VP9 Codec
548
+
549
+ Args:
550
+ video (str | FFmpegCodec): The path to the video file or an FFmpegCodec instance.
551
+ """
552
+ if isinstance(video, str):
553
+ temp_stream = FFmpegCodec(video)
554
+ self.video_stream = None
555
+ match temp_stream.stream["codec_name"]:
556
+ case "h264":
557
+ self.video_stream = H264Codec(video)
558
+ case "vp9":
559
+ self.video_stream = VP9Codec(video)
560
+ case "mpeg1video":
561
+ self.video_stream = MPEG1Codec(video)
562
+ assert self.video_stream, (
563
+ "fail to match suitable video codec. Codec=%s"
564
+ % temp_stream.stream["codec_name"]
565
+ )
840
566
  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)
567
+ self.video_stream = video
847
568
 
569
+ def add_audio(self, audio : ADXCodec | HCACodec):
570
+ """Append the audio stream(s) from the specified audio file(s).
571
+
572
+ Args:
573
+ audio (ADXCodec | HCACodec): The path(s) to the audio file(s).
574
+ """
575
+ self.audio_streams.append(audio)
848
576
 
849
577
  def build(self) -> bytes:
578
+ """Build the USM payload"""
850
579
  SFV_list = self.video_stream.generate_SFV(self)
851
- if self.enable_audio:
580
+ if self.audio_streams:
852
581
  SFA_chunks = [s.generate_SFA(i, self) for i, s in enumerate(self.audio_streams) ]
853
582
  else:
854
583
  SFA_chunks = []
@@ -928,7 +657,7 @@ class USMBuilder(USMCrypt):
928
657
  )
929
658
  CRIUSF_DIR_STREAM.append(video_dict)
930
659
 
931
- if self.enable_audio:
660
+ if self.audio_streams:
932
661
  chno = 0
933
662
  for stream in self.audio_streams:
934
663
  avbps = stream.avbps
@@ -936,7 +665,7 @@ class USMBuilder(USMCrypt):
936
665
  minbuf += 27860
937
666
  audio_dict = dict(
938
667
  fmtver=(UTFTypeValues.uint, 0),
939
- filename=(UTFTypeValues.string, self.audio_filenames[chno]),
668
+ filename=(UTFTypeValues.string, stream.filename),
940
669
  filesize=(UTFTypeValues.uint, stream.filesize),
941
670
  datasize=(UTFTypeValues.uint, 0),
942
671
  stmid=(
@@ -1018,7 +747,7 @@ class USMBuilder(USMCrypt):
1018
747
 
1019
748
  audio_metadata = []
1020
749
  audio_headers = []
1021
- if self.enable_audio:
750
+ if self.audio_streams:
1022
751
  chno = 0
1023
752
  for stream in self.audio_streams:
1024
753
  metadata = stream.get_metadata()
@@ -1141,7 +870,7 @@ class USMBuilder(USMCrypt):
1141
870
  VIDEO_HDRINFO = gen_video_hdr_info(len(VIDEO_SEEKINFO))
1142
871
 
1143
872
  total_len = sum([len(x) for x in SFV_list]) + first_chk_ofs
1144
- if self.enable_audio:
873
+ if self.audio_streams:
1145
874
  sum_len = 0
1146
875
  for stream in SFA_chunks:
1147
876
  for x in stream:
@@ -1181,7 +910,7 @@ class USMBuilder(USMCrypt):
1181
910
 
1182
911
  # Header chunks
1183
912
  header += VIDEO_HDRINFO
1184
- if self.enable_audio:
913
+ if self.audio_streams:
1185
914
  header += b''.join(audio_headers)
1186
915
  SFV_END = USMChunkHeader.pack(
1187
916
  USMChunckHeaderType.SFV.value,
@@ -1202,7 +931,7 @@ class USMBuilder(USMCrypt):
1202
931
  header += SFV_END
1203
932
 
1204
933
  SFA_chk_END = b'' # Maybe reused
1205
- if self.enable_audio:
934
+ if self.audio_streams:
1206
935
  SFA_chk_END = b''.join([
1207
936
  USMChunkHeader.pack(
1208
937
  USMChunckHeaderType.SFA.value,
@@ -1223,7 +952,7 @@ class USMBuilder(USMCrypt):
1223
952
  header += SFA_chk_END # Ends audio_headers
1224
953
  header += VIDEO_SEEKINFO
1225
954
 
1226
- if self.enable_audio:
955
+ if self.audio_streams:
1227
956
  header += b''.join(audio_metadata)
1228
957
  SFV_END = USMChunkHeader.pack(
1229
958
  USMChunckHeaderType.SFV.value,