PyCriCodecsEx 0.0.2__cp311-cp311-win_amd64.whl → 0.0.3__cp311-cp311-win_amd64.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.
Binary file
PyCriCodecsEx/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.0.2"
1
+ __version__ = "0.0.3"
PyCriCodecsEx/acb.py CHANGED
@@ -3,10 +3,11 @@
3
3
  # - Original work by https://github.com/Youjose/PyCriCodecs
4
4
  # See Research/ACBSchema.py for more details.
5
5
 
6
- from typing import Generator, List, Tuple
6
+ from typing import Generator, List, Tuple, BinaryIO
7
7
  from PyCriCodecsEx.chunk import *
8
8
  from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
9
- from PyCriCodecsEx.usm import HCACodec, ADXCodec
9
+ from PyCriCodecsEx.hca import HCACodec
10
+ from PyCriCodecsEx.adx import ADXCodec
10
11
  from PyCriCodecsEx.awb import AWB, AWBBuilder
11
12
  from dataclasses import dataclass
12
13
  from copy import deepcopy
@@ -136,9 +137,14 @@ class CueItem:
136
137
  Waveforms: list[int] # List of waveform IDs
137
138
 
138
139
  class ACB(UTF):
139
- """An ACB is basically a giant @UTF table. Use this class to extract any ACB, and potentially modifiy it in place."""
140
- def __init__(self, filename) -> None:
141
- super().__init__(filename,recursive=True)
140
+ """Use this class to read, and modify ACB files in memory."""
141
+ def __init__(self, stream : str | BinaryIO) -> None:
142
+ """Loads an ACB file from the given stream.
143
+
144
+ Args:
145
+ stream (str | BinaryIO): The path to the ACB file or a BinaryIO stream containing the ACB data.
146
+ """
147
+ super().__init__(stream, recursive=True)
142
148
 
143
149
  @property
144
150
  def payload(self) -> dict:
@@ -239,17 +245,22 @@ class ACB(UTF):
239
245
  yield CueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
240
246
 
241
247
  class ACBBuilder:
248
+ """Use this class to build ACB files from an existing ACB object."""
242
249
  acb: ACB
243
250
 
244
251
  def __init__(self, acb: ACB) -> None:
245
252
  """Initializes the ACBBuilder with an existing ACB object.
246
253
 
254
+ Args:
255
+ acb (ACB): The ACB object to build from.
256
+
247
257
  Building ACB from scratch isn't planned for now since:
248
- * We don't know how SeqCommandTable TLVs work. This is the biggest issue.
249
- * Many fields are unknown or not well understood
250
- - Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
251
- with their own ACF table. Missing these is not a fun debugging experience.
252
- * ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
258
+
259
+ * We don't know how SeqCommandTable TLVs work. This is the biggest issue.
260
+ * Many fields are unknown or not well understood
261
+ - Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
262
+ with their own ACF table. Missing these is not a fun debugging experience.
263
+ * ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
253
264
 
254
265
  Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
255
266
  """
PyCriCodecsEx/adx.py CHANGED
@@ -1,8 +1,13 @@
1
+ from typing import BinaryIO
2
+ from io import BytesIO
3
+ from PyCriCodecsEx.chunk import *
1
4
  import CriCodecsEx
2
-
3
5
  class ADX:
4
- """ADX Module for decoding and encoding ADX files, pass the either `adx file` or `wav file` in bytes to either `decode` or `encode` respectively."""
5
-
6
+ """ADX class for decoding and encoding ADX files, pass the either `adx file` or `wav file` in bytes to either `decode` or `encode` respectively.
7
+
8
+ **NOTE:** Direct usage of this class is not recommended, use the `ADXCodec` wrapper instead.
9
+ """
10
+
6
11
  # Decodes ADX to WAV.
7
12
  @staticmethod
8
13
  def decode(data: bytes) -> bytes:
@@ -13,4 +18,138 @@ class ADX:
13
18
  @staticmethod
14
19
  def encode(data: bytes, BitDepth = 0x4, Blocksize = 0x12, Encoding = 3, AdxVersion = 0x4, Highpass_Frequency = 0x1F4, Filter = 0, force_not_looping = False) -> bytes:
15
20
  """ Encodes WAV to ADX. """
16
- return CriCodecsEx.AdxEncode(bytes(data), BitDepth, Blocksize, Encoding, Highpass_Frequency, Filter, AdxVersion, force_not_looping)
21
+ return CriCodecsEx.AdxEncode(bytes(data), BitDepth, Blocksize, Encoding, Highpass_Frequency, Filter, AdxVersion, force_not_looping)
22
+
23
+ class ADXCodec(ADX):
24
+ """Use this class for encoding and decoding ADX files, from and to WAV."""
25
+
26
+ CHUNK_INTERVAL = 99.9
27
+ BASE_FRAMERATE = 2997
28
+ # TODO: Move these to an enum
29
+ AUDIO_CODEC = 2
30
+ METADATA_COUNT = 0
31
+
32
+ filename : str
33
+ filesize : int
34
+
35
+ adx : bytes
36
+ header : bytes
37
+ sfaStream: BinaryIO
38
+
39
+ AdxDataOffset: int
40
+ AdxEncoding: int
41
+ AdxBlocksize: int
42
+ AdxSampleBitdepth: int
43
+ AdxChannelCount: int
44
+ AdxSamplingRate: int
45
+ AdxSampleCount: int
46
+ AdxHighpassFrequency: int
47
+ AdxVersion: int
48
+ AdxFlags: int
49
+
50
+ chnls: int
51
+ sampling_rate: int
52
+ total_samples: int
53
+ avbps: int
54
+
55
+ def __init__(self, stream: str | bytes, filename: str = "default.adx", bitdepth: int = 4, **kwargs):
56
+ """Initializes the ADX encoder/decoder
57
+
58
+ Args:
59
+ stream (str | bytes): Path to the ADX or WAV file, or a BinaryIO stream. WAV files will be automatically encoded with the given settings first.
60
+ filename (str, optional): Output filename. Defaults to "default.adx".
61
+ bitdepth (int, optional): Audio bit depth within [2,15]. Defaults to 4.
62
+ """
63
+ if type(stream) == str:
64
+ self.adx = open(stream, "rb").read()
65
+ else:
66
+ self.adx = stream
67
+ self.filename = filename
68
+ self.filesize = len(self.adx)
69
+ magic = self.adx[:4]
70
+ if magic == b"RIFF":
71
+ self.adx = self.encode(self.adx, bitdepth, force_not_looping=True)
72
+ self.sfaStream = BytesIO(self.adx)
73
+ header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
74
+ FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
75
+ assert FourCC == 0x8000, "either ADX or WAV is supported"
76
+ assert self.AdxVersion in {3,4}, "unsupported ADX version"
77
+ if self.AdxVersion == 4:
78
+ self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
79
+ self.sfaStream.seek(0)
80
+ self.chnls = self.AdxChannelCount
81
+ self.sampling_rate = self.AdxSamplingRate
82
+ self.total_samples = self.AdxSampleCount
83
+ self.avbps = int(self.filesize * 8 * self.chnls) - self.filesize
84
+
85
+ def generate_SFA(self, index: int, builder):
86
+ # USMBuilder usage
87
+ current_interval = 0
88
+ stream_size = len(self.adx) - self.AdxBlocksize
89
+ chunk_size = int(self.AdxSamplingRate // (self.BASE_FRAMERATE / 100) // 32) * (self.AdxBlocksize * self.AdxChannelCount)
90
+ self.sfaStream.seek(0)
91
+ res = []
92
+ while self.sfaStream.tell() < stream_size:
93
+ if self.sfaStream.tell() > 0:
94
+ if self.sfaStream.tell() + chunk_size < stream_size:
95
+ datalen = chunk_size
96
+ else:
97
+ datalen = (stream_size - (self.AdxDataOffset + 4) - chunk_size) % chunk_size
98
+ else:
99
+ datalen = self.AdxDataOffset + 4
100
+ if not datalen:
101
+ break
102
+ padding = (0x20 - (datalen % 0x20) if datalen % 0x20 != 0 else 0)
103
+ SFA_chunk = USMChunkHeader.pack(
104
+ USMChunckHeaderType.SFA.value,
105
+ datalen + 0x18 + padding,
106
+ 0,
107
+ 0x18,
108
+ padding,
109
+ index,
110
+ 0,
111
+ 0,
112
+ 0,
113
+ round(current_interval),
114
+ self.BASE_FRAMERATE,
115
+ 0,
116
+ 0
117
+ )
118
+ chunk_data = self.sfaStream.read(datalen)
119
+ if builder.encrypt_audio:
120
+ SFA_chunk = builder.AudioMask(chunk_data)
121
+ SFA_chunk += chunk_data.ljust(datalen + padding, b"\x00")
122
+ current_interval += self.CHUNK_INTERVAL
123
+ res.append(SFA_chunk)
124
+ # ---
125
+ SFA_chunk = USMChunkHeader.pack(
126
+ USMChunckHeaderType.SFA.value,
127
+ 0x38,
128
+ 0,
129
+ 0x18,
130
+ 0,
131
+ index,
132
+ 0,
133
+ 0,
134
+ 2,
135
+ 0,
136
+ 30,
137
+ 0,
138
+ 0
139
+ )
140
+ SFA_chunk += b"#CONTENTS END ===============\x00"
141
+ res[-1] += SFA_chunk
142
+ return res
143
+
144
+ def get_metadata(self):
145
+ return None
146
+
147
+ def get_encoded(self):
148
+ """Gets the encoded ADX audio data."""
149
+ return self.adx
150
+
151
+ def save(self, filepath: str):
152
+ """Saves the encoded ADX audio to filepath"""
153
+ with open(filepath, "wb") as f:
154
+ f.write(self.decode(self.adx))
155
+
PyCriCodecsEx/awb.py CHANGED
@@ -82,6 +82,7 @@ class AWB:
82
82
  raise ValueError("Unknown int size.")
83
83
 
84
84
  class AWBBuilder:
85
+ """Use this class to build AWB files from a list of bytes."""
85
86
  def __init__(self, infiles: list[bytes], subkey: int = 0, version: int = 2, id_intsize = 0x2, align: int = 0x20) -> None:
86
87
  """Initializes the AWB builder.
87
88
 
PyCriCodecsEx/chunk.py CHANGED
@@ -46,11 +46,6 @@ class HCAType(Enum):
46
46
  HCA = b"HCA\x00" # Header.
47
47
  EHCA = b"\xC8\xC3\xC1\x00" # Encrypted HCA header.
48
48
 
49
- class VideoType(Enum):
50
- IVF = b"DKIF" # Header.
51
- # H264 = b"" # Header.
52
- # MPEG = b"" # Header.
53
-
54
49
  # I saw some devs swap the unsigned/signed indexes. So I am not sure what's correct or not.
55
50
  # In my own experience, swapping those results in an incorrect signed values (should be unsigned) in ACB's/CPK's.
56
51
  # If someone were to change this, they must change 'stringtypes' function in UTF/UTFBuilder classes.
PyCriCodecsEx/cpk.py CHANGED
@@ -48,6 +48,7 @@ class _TOC():
48
48
  self.table = UTF(self.stream.read()).table
49
49
 
50
50
  class CPK:
51
+ """Use this class to load CPK file table-of-content, and read files from them on-demand."""
51
52
  magic: bytes
52
53
  encflag: int
53
54
  packet_size: int
@@ -189,7 +190,7 @@ class CPKBuilder:
189
190
  ContentSize: int
190
191
  EnabledDataSize: int
191
192
  EnabledPackedSize: int
192
- outfile: str
193
+ outfile: BinaryIO
193
194
  init_toc_len: int # This is a bit of a redundancy, but some CPK's need it.
194
195
 
195
196
  in_files : list[tuple[str, str, bool]] # (source path, dest filename, compress or not)
@@ -254,14 +255,13 @@ class CPKBuilder:
254
255
 
255
256
  self.in_files.append((src, dst, compress))
256
257
 
257
- def _writetofile(self, header) -> None:
258
- with open(self.outfile, "wb") as out:
259
- out.write(header)
260
- for i, ((path, _), (filename, file_size, pack_size)) in enumerate(zip(self.os_files, self.files)):
261
- src = open(path, 'rb').read()
262
- out.write(src)
263
- out.write(bytes(0x800 - pack_size % 0x800))
264
- self.progress_cb("Write %s" % os.path.basename(filename), i + 1, len(self.files))
258
+ def _writetofile(self, header) -> None:
259
+ self.outfile.write(header)
260
+ for i, ((path, _), (filename, file_size, pack_size)) in enumerate(zip(self.os_files, self.files)):
261
+ src = open(path, 'rb').read()
262
+ self.outfile.write(src)
263
+ self.outfile.write(bytes(0x800 - pack_size % 0x800))
264
+ self.progress_cb("Write %s" % os.path.basename(filename), i + 1, len(self.files))
265
265
 
266
266
  def _populate_files(self, parallel : bool):
267
267
  self.files = []
@@ -304,12 +304,12 @@ class CPKBuilder:
304
304
  pass
305
305
  self.os_files = []
306
306
 
307
- def save(self, outfile : str, parallel : bool = False):
307
+ def save(self, outfile : str | BinaryIO, parallel : bool = False):
308
308
  """Build and save the bundle into a file
309
309
 
310
310
 
311
311
  Args:
312
- outfile (str): The output file path.
312
+ outfile (str | BinaryIO): The output file path or a writable binary stream.
313
313
  parallel (bool, optional): Whether to use parallel processing for file compression (if at all used). Defaults to False.
314
314
 
315
315
  NOTE:
@@ -318,6 +318,8 @@ class CPKBuilder:
318
318
  """
319
319
  assert self.in_files, "cannot save empty bundle"
320
320
  self.outfile = outfile
321
+ if type(outfile) == str:
322
+ self.outfile = open(outfile, "wb")
321
323
  self._populate_files(parallel)
322
324
  if self.encrypt:
323
325
  encflag = 0
@@ -363,7 +365,9 @@ class CPKBuilder:
363
365
  data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.ITOCdata
364
366
  self._writetofile(data)
365
367
  self._cleanup_files()
366
-
368
+ if type(outfile) == str:
369
+ self.outfile.close()
370
+
367
371
  def _generate_GTOC(self) -> bytearray:
368
372
  # NOTE: Practically useless
369
373
  # I have no idea why are those numbers here.
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,143 @@ 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):
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):
449
+ """Saves the decoded WAV audio to filepath"""
450
+ with open(filepath, "wb") as f:
451
+ f.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
@@ -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,260 +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 = "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
-
326
+ from PyCriCodecsEx.hca import HCACodec
327
+ from PyCriCodecsEx.adx import ADXCodec
571
328
 
572
329
  class USM(USMCrypt):
573
- """USM class for extracting infromation and data from a USM file."""
330
+ """Use this class to extract infromation and data from a USM file."""
574
331
 
575
332
  filename: str
576
333
  decrypt: bool
@@ -585,7 +342,7 @@ class USM(USMCrypt):
585
342
 
586
343
  metadata: list
587
344
 
588
- def __init__(self, filename, key: str | int = None):
345
+ def __init__(self, filename : str | BinaryIO, key: str | int = None):
589
346
  """Loads a USM file into memory and prepares it for processing.
590
347
 
591
348
  Args:
@@ -601,7 +358,10 @@ class USM(USMCrypt):
601
358
  self._load_file()
602
359
 
603
360
  def _load_file(self):
604
- 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
605
365
  self.stream.seek(0, 2)
606
366
  self.size = self.stream.tell()
607
367
  self.stream.seek(0)
@@ -716,7 +476,7 @@ class USM(USMCrypt):
716
476
 
717
477
  @property
718
478
  def streams(self):
719
- """[Type (@SFV, @SFA), Filename, Raw stream data]"""
479
+ """Generator of Tuple[Stream Type ("@SFV", "@SFA"), File name, Raw stream data]"""
720
480
  for stream in self.CRIDObj.dictarray[1:]:
721
481
  filename, stmid, chno = stream["filename"][1], stream["stmid"][1], stream["chno"][1]
722
482
  stmid = int.to_bytes(stmid, 4, 'big', signed='False')
@@ -751,7 +511,7 @@ class USM(USMCrypt):
751
511
  return []
752
512
 
753
513
  class USMBuilder(USMCrypt):
754
- """USM class for building USM files."""
514
+ """Use this class to build USM files."""
755
515
  video_stream: VP9Codec | H264Codec | MPEG1Codec
756
516
  audio_streams: List[HCACodec | ADXCodec]
757
517
 
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)
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyCriCodecsEx
3
+ Version: 0.0.3
4
+ Summary: Criware formats library for Python
5
+ Home-page: https://mos9527.github.io/PyCriCodecsEx/
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: usm
13
+ Requires-Dist: ffmpeg-python; extra == "usm"
14
+ Dynamic: classifier
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: license-file
19
+ Dynamic: provides-extra
20
+ Dynamic: requires-python
21
+ Dynamic: summary
22
+
23
+ PyCriCodecsEx
24
+ ---
25
+ A continuation of @Youjose's work on Criware formats. Feautres are still in flux and subject to change. When in doubt, Refer to the [original repo](https://github.com/Youjose/PyCriCodecs) for more information.
26
+
27
+ Detailed documentation, installation instructions are available at https://mos9527.com/PyCriCodecsEx
28
+
29
+
30
+ # Credits
31
+ - https://github.com/Youjose/PyCriCodecs
32
+ - https://github.com/Mikewando/PyCriCodecs ([PR#1 on USM](https://github.com/mos9527/PyCriCodecsEx/pull/1))
33
+ - https://github.com/donmai-me/WannaCRI
34
+ - https://github.com/vgmstream/vgmstream
35
+ - https://github.com/K0lb3/UnityPy (For CI script)
@@ -0,0 +1,15 @@
1
+ CriCodecsEx.cp311-win_amd64.pyd,sha256=c8CuMe4VNMNZobA33rR82bz81GrGEE0-QC8J5Ija7xs,89088
2
+ PyCriCodecsEx/__init__.py,sha256=4dCrvStS1OgE3Vlcv-qx5-TAo377yOMEToEqCi-PwY4,23
3
+ PyCriCodecsEx/acb.py,sha256=nOWggyQTPOBArzRp_7VRO9TY1diLxr2mRPKQd-sd2s0,10723
4
+ PyCriCodecsEx/adx.py,sha256=s3gJBgKrwWjUahrxSJEy1GNHJrdz3iaylgr0TyTIi7M,5967
5
+ PyCriCodecsEx/awb.py,sha256=t6owuEOH3Be31iBuEMwriOh0cPdVtpzsNbTPfLwg_cc,6363
6
+ PyCriCodecsEx/chunk.py,sha256=xmc92tONmXpdbOW2g-KckTeppTXFo8Km0J95UYbbvQg,2810
7
+ PyCriCodecsEx/cpk.py,sha256=bMJgvzPInM95R9dEdhHGW65jR9tFBKUacyOwZS2gOyA,38206
8
+ PyCriCodecsEx/hca.py,sha256=BamujR3tRxMmgnPAYpUOamzOUOBs5hXWhkcBnj7Eeh4,19445
9
+ PyCriCodecsEx/usm.py,sha256=LWSXXkfV1FZpymV0X6UH-lyS5gV6ecBHlLLXXXN9wMc,36972
10
+ PyCriCodecsEx/utf.py,sha256=0LBzpIbJiyL_4KOf_QpEJytr3PbHj8_M4Bd-Lay4wuk,28247
11
+ pycricodecsex-0.0.3.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
12
+ pycricodecsex-0.0.3.dist-info/METADATA,sha256=buywcqSll1E63mnWNtEgwHiyAY8NbvMyP8CDLXAJLH0,1291
13
+ pycricodecsex-0.0.3.dist-info/WHEEL,sha256=JLOMsP7F5qtkAkINx5UnzbFguf8CqZeraV8o04b0I8I,101
14
+ pycricodecsex-0.0.3.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
15
+ pycricodecsex-0.0.3.dist-info/RECORD,,
@@ -1,86 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: PyCriCodecsEx
3
- Version: 0.0.2
4
- Summary: Criware formats library for Python
5
- Home-page: https://github.com/mos9527/PyCriCodecsEx
6
- Classifier: Programming Language :: Python :: 3
7
- Classifier: License :: OSI Approved :: MIT License
8
- Classifier: Operating System :: OS Independent
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Provides-Extra: usm
13
- Requires-Dist: ffmpeg-python; extra == "usm"
14
- Dynamic: classifier
15
- Dynamic: description
16
- Dynamic: description-content-type
17
- Dynamic: home-page
18
- Dynamic: license-file
19
- Dynamic: provides-extra
20
- Dynamic: requires-python
21
- Dynamic: summary
22
-
23
- # PyCriCodecsEx
24
- A continuation of @Youjose's work on Criware formats. Feautres are still in flux and subject to change. When in doubt, Refer to the [original repo](https://github.com/Youjose/PyCriCodecs) for more information.
25
-
26
- # Installation
27
- ```bash
28
- pip install PyCriCodecsEx
29
- ```
30
-
31
- For USM features, you need `ffmpeg` installed and available in your PATH. See also https://github.com/kkroening/ffmpeg-python?tab=readme-ov-file#installing-ffmpeg
32
-
33
- ## Features
34
- If not otherwise mentioned, all features marked with [x] are considered working, and has been verified with official tools.
35
-
36
- Examples are available in [Tests](https://github.com/mos9527/PyCriCodecsEx/tree/main/Tests)
37
-
38
- ### ACB Cue sheets (also AWB)
39
- - [x] Cue extraction support for most ACBs
40
- - [x] Cue waveform(s) encoding with ADX/HCA support
41
- - [x] Comprehensive Cue metadata editing support (via Python API)
42
-
43
- ### USM Sofdec2 (Encode & Decode)
44
- #### Audio Stream
45
- For audio to be muxed in, you need a PCM WAV sample with NO metadata, which can be produced with e.g.:
46
- ```bash
47
- ffmpeg -i input.mp3 -vn -c:a pcm_s16le -map_metadata -1 output.wav
48
- ```
49
- Decoding and Encoded format can be the following:
50
- - [x] HCA
51
- - [x] ADX
52
- #### Video Stream
53
- **NOTE**: You definitely want to tweak these encode settings a bit.
54
- - [x] Sofdec Prime (MPEG1, from `.m1v` container)
55
- - Prepare source file with: `ffmpeg -i <input_file> -c:v mpeg1video -an <output_file>.m1v`
56
- - [x] H264 (from `.h264` raw container)
57
- - Prepare source file with: `ffmpeg -i <input_file> -c:v libx264 -an <output_file>.h264`
58
- - [x] VP9 (from `.ivf` container)
59
- - Prepare source file with: `ffmpeg -i <input_file> -c:v libvpx -an <output_file>.ivf`
60
- ### HCA Audio Codec
61
- - [x] Decoding (up to version 3.0)
62
- - [x] Encoding (up to version 3.0)
63
- ### ADX Audio Codec
64
- - [x] Decoding
65
- - [x] Encoding
66
- ### CPK
67
- - [x] Unpacking
68
- - [x] Packing
69
-
70
- ## Roadmap
71
- - [x] ACB Extraction (Massive TODO. see also https://github.com/mos9527/PyCriCodecsEx/blob/main/Research/ACBSchema.py)
72
- - [ ] Interface for encode tasks (CLI then maybe GUI?)
73
- - [ ] Documentation
74
- - [ ] C/C++ port + FFI
75
- ## Currently Known Bugs
76
- - USM seeking does not work. Though most games don't use it anyways.
77
- - Not important, and might not fix: ADX encoding and decoding at higher bitdepths (11-15) adds popping noise.
78
- - Some CPK's that has the same filename for every file in the entry will overwrite each other.
79
- - Probably many more I am unaware of, report if you find any.
80
-
81
- # Credits
82
- - https://github.com/Youjose/PyCriCodecs
83
- - https://github.com/Mikewando/PyCriCodecs ([PR#1 on USM](https://github.com/mos9527/PyCriCodecsEx/pull/1))
84
- - https://github.com/donmai-me/WannaCRI
85
- - https://github.com/vgmstream/vgmstream
86
- - https://github.com/K0lb3/UnityPy (For CI script)
@@ -1,15 +0,0 @@
1
- CriCodecsEx.cp311-win_amd64.pyd,sha256=5vRlSKo1haEaqS6Bfdr7jZW7iajXmczo4uU6a7jL9BU,89088
2
- PyCriCodecsEx/__init__.py,sha256=j7895T8JfsWQAOvxm8SAdESs5iFmIuAqKpyfjSAfKZ4,23
3
- PyCriCodecsEx/acb.py,sha256=vNPiGW1S0pkdWjcegTWId4C0suUCeX-kfFXUWuRnB-w,10352
4
- PyCriCodecsEx/adx.py,sha256=KnL4wy1ccoc6uHYZVino7cJQB2pMn4BbRhpH3r527TY,777
5
- PyCriCodecsEx/awb.py,sha256=sWUo5ITERYG504YVe9ejDYMM-bpLB31Ny4X-7rmmWlQ,6296
6
- PyCriCodecsEx/chunk.py,sha256=quk9VicOfp-_PgmcjyLL9rCIZQG8DhophXQZWpqfEUw,2925
7
- PyCriCodecsEx/cpk.py,sha256=sLFUGtG1yuwNC4puyp_TOgkhv1qnRGxVQu4ctlqXeUQ,37932
8
- PyCriCodecsEx/hca.py,sha256=ArZAaHogejBRE2rWEkTt8lS993Wf4SkjkjhNTo5Anqs,14550
9
- PyCriCodecsEx/usm.py,sha256=t2V98eno_beVuvU1tbuaFe4p7OohwROiUerMDHhPhEw,45107
10
- PyCriCodecsEx/utf.py,sha256=YWvgpgLy1tdhVWvRe4pbiJZFxnBRHdtQiR6xEetLsGk,28819
11
- pycricodecsex-0.0.2.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
12
- pycricodecsex-0.0.2.dist-info/METADATA,sha256=CAVs16QnoaoIrn8_PJH2w8wM64DTiIXNfxMUvCb-ZN0,3438
13
- pycricodecsex-0.0.2.dist-info/WHEEL,sha256=JLOMsP7F5qtkAkINx5UnzbFguf8CqZeraV8o04b0I8I,101
14
- pycricodecsex-0.0.2.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
15
- pycricodecsex-0.0.2.dist-info/RECORD,,