PyCriCodecsEx 0.0.1__cp310-cp310-musllinux_1_2_i686.whl → 0.0.3__cp310-cp310-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/cpk.py CHANGED
@@ -8,7 +8,7 @@ from concurrent.futures import ProcessPoolExecutor, as_completed
8
8
  from tempfile import NamedTemporaryFile
9
9
  import CriCodecsEx
10
10
 
11
- def worker_do_compression(src : str, dst: str):
11
+ def _worker_do_compression(src : str, dst: str):
12
12
  with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
13
13
  data = fsrc.read()
14
14
  compressed = CriCodecsEx.CriLaylaCompress(data)
@@ -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
@@ -55,7 +56,12 @@ class CPK:
55
56
  stream: BinaryIO
56
57
  tables: dict
57
58
  filename: str
58
- def __init__(self, filename) -> None:
59
+ def __init__(self, filename : str | BinaryIO) -> None:
60
+ """Loads a CPK archive's table-of-content and ready for file reading.
61
+
62
+ Args:
63
+ filename (str | BinaryIO): The path to the CPK file or a BinaryIO stream containing the CPK data.
64
+ """
59
65
  if type(filename) == str:
60
66
  self.filename = filename
61
67
  self.stream = FileIO(filename)
@@ -184,7 +190,7 @@ class CPKBuilder:
184
190
  ContentSize: int
185
191
  EnabledDataSize: int
186
192
  EnabledPackedSize: int
187
- outfile: str
193
+ outfile: BinaryIO
188
194
  init_toc_len: int # This is a bit of a redundancy, but some CPK's need it.
189
195
 
190
196
  in_files : list[tuple[str, str, bool]] # (source path, dest filename, compress or not)
@@ -249,14 +255,13 @@ class CPKBuilder:
249
255
 
250
256
  self.in_files.append((src, dst, compress))
251
257
 
252
- def _writetofile(self, header) -> None:
253
- with open(self.outfile, "wb") as out:
254
- out.write(header)
255
- for i, ((path, _), (filename, file_size, pack_size)) in enumerate(zip(self.os_files, self.files)):
256
- src = open(path, 'rb').read()
257
- out.write(src)
258
- out.write(bytes(0x800 - pack_size % 0x800))
259
- 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))
260
265
 
261
266
  def _populate_files(self, parallel : bool):
262
267
  self.files = []
@@ -271,7 +276,7 @@ class CPKBuilder:
271
276
  futures = []
272
277
  for (src, _, _), (dst, compress) in zip(self.in_files,self.os_files):
273
278
  if compress:
274
- futures.append(exec.submit(worker_do_compression, src, dst))
279
+ futures.append(exec.submit(_worker_do_compression, src, dst))
275
280
  for i, fut in as_completed(futures):
276
281
  try:
277
282
  fut.result()
@@ -281,7 +286,7 @@ class CPKBuilder:
281
286
  else:
282
287
  for i, ((src, _, _), (dst, compress)) in enumerate(zip(self.in_files,self.os_files)):
283
288
  if compress:
284
- worker_do_compression(src, dst)
289
+ _worker_do_compression(src, dst)
285
290
  self.progress_cb("Compress %s" % os.path.basename(src), i + 1, len(self.in_files))
286
291
  for (src, filename, _) , (dst, _) in zip(self.in_files,self.os_files):
287
292
  file_size = os.stat(src).st_size
@@ -299,12 +304,12 @@ class CPKBuilder:
299
304
  pass
300
305
  self.os_files = []
301
306
 
302
- def save(self, outfile : str, parallel : bool = False):
307
+ def save(self, outfile : str | BinaryIO, parallel : bool = False):
303
308
  """Build and save the bundle into a file
304
309
 
305
310
 
306
311
  Args:
307
- outfile (str): The output file path.
312
+ outfile (str | BinaryIO): The output file path or a writable binary stream.
308
313
  parallel (bool, optional): Whether to use parallel processing for file compression (if at all used). Defaults to False.
309
314
 
310
315
  NOTE:
@@ -313,6 +318,8 @@ class CPKBuilder:
313
318
  """
314
319
  assert self.in_files, "cannot save empty bundle"
315
320
  self.outfile = outfile
321
+ if type(outfile) == str:
322
+ self.outfile = open(outfile, "wb")
316
323
  self._populate_files(parallel)
317
324
  if self.encrypt:
318
325
  encflag = 0
@@ -358,7 +365,9 @@ class CPKBuilder:
358
365
  data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.ITOCdata
359
366
  self._writetofile(data)
360
367
  self._cleanup_files()
361
-
368
+ if type(outfile) == str:
369
+ self.outfile.close()
370
+
362
371
  def _generate_GTOC(self) -> bytearray:
363
372
  # NOTE: Practically useless
364
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
@@ -46,7 +50,14 @@ class HCA:
46
50
  table: array
47
51
  looping: bool
48
52
 
49
- def __init__(self, stream: BinaryIO, key: int = 0, subkey: int = 0) -> None:
53
+ def __init__(self, stream: str | BinaryIO, key: int = 0, subkey: int = 0) -> None:
54
+ """Initializes the HCA encoder/decoder
55
+
56
+ Args:
57
+ stream (str | BinaryIO): Path to the HCA or WAV file, or a BinaryIO stream.
58
+ key (int, optional): HCA key. Defaults to 0.
59
+ subkey (int, optional): HCA subkey. Defaults to 0.
60
+ """
50
61
  if type(stream) == str:
51
62
  self.stream = FileIO(stream)
52
63
  self.hcastream = FileIO(stream)
@@ -66,10 +77,10 @@ class HCA:
66
77
  self.hcabytes: bytearray = b''
67
78
  self.enc_table: array = b''
68
79
  self.table: array = b''
69
- self.Pyparse_header()
80
+ self._Pyparse_header()
70
81
 
71
82
 
72
- def Pyparse_header(self) -> None:
83
+ def _Pyparse_header(self) -> None:
73
84
  self.HcaSig, self.version, self.header_size = HcaHeaderStruct.unpack(
74
85
  self.hcastream.read(HcaHeaderStruct.size)
75
86
  )
@@ -198,39 +209,35 @@ class HCA:
198
209
  raise ValueError(f"WAV bitdepth of {self.fmtBitCount} is not supported, only 16 bit WAV files are supported.")
199
210
  elif self.fmtSize != 16:
200
211
  raise ValueError(f"WAV file has an FMT chunk of an unsupported size: {self.fmtSize}, the only supported size is 16.")
201
- if self.stream.read(4) == b"smpl":
202
- self.stream.seek(-4, 1)
203
- self.looping = True
204
- # Will just be naming the important things here.
205
- smplsig, smplesize, _, _, _, _, _, _, _, self.LoopCount, _, _, _, self.LoopStartSample, self.LoopEndSample, _, _ = WavSmplHeaderStruct.unpack(
206
- self.stream.read(WavSmplHeaderStruct.size)
207
- )
208
- if self.LoopCount != 1:
209
- self.looping = False # Unsupported multiple looping points, so backtracks, and ignores looping data.
210
- self.stream.seek(-WavSmplHeaderStruct.size, 1)
211
- self.stream.seek(8 + smplesize, 1)
212
- else:
213
- self.stream.seek(-4, 1)
214
- self.looping = False
215
- if self.stream.read(4) == b"note": # There's no use for this on ADX.
216
- len = self.stream.read(4)
217
- self.stream.seek(len+4) # + 1? + padding maybe?
218
- else:
219
- self.stream.seek(-4, 1)
220
- if self.stream.read(4) == b"data":
221
- self.stream.seek(-4, 1)
222
- self.dataSig, self.dataSize = WavDataHeaderStruct.unpack(
223
- self.stream.read(WavDataHeaderStruct.size)
224
- )
225
- else:
226
- 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)
227
234
  else:
228
235
  raise ValueError("Invalid HCA or WAV file.")
229
236
  self.stream.seek(0)
230
237
  self.hcastream.seek(0)
231
238
 
232
239
  def info(self) -> dict:
233
- """ Returns info related to the input file. """
240
+ """Returns info related to the input file. """
234
241
  if self.filetype == "hca":
235
242
  return self.hca
236
243
  elif self.filetype == "wav":
@@ -238,6 +245,7 @@ class HCA:
238
245
  return wav
239
246
 
240
247
  def decode(self) -> bytes:
248
+ """Decodes the HCA or WAV file to WAV bytes. """
241
249
  if self.filetype == "wav":
242
250
  raise ValueError("Input type for decoding must be an HCA file.")
243
251
  self.hcastream.seek(0)
@@ -247,6 +255,7 @@ class HCA:
247
255
  return bytes(self.wavbytes)
248
256
 
249
257
  def encode(self, force_not_looping: bool = False, encrypt: bool = False, keyless: bool = False, quality_level: CriHcaQuality = CriHcaQuality.High) -> bytes:
258
+ """Encodes the WAV file to HCA bytes."""
250
259
  if self.filetype == "hca":
251
260
  raise ValueError("Input type for encoding must be a WAV file.")
252
261
  if force_not_looping == False:
@@ -260,21 +269,21 @@ class HCA:
260
269
  self.stream.seek(0)
261
270
  self.hcabytes = CriCodecsEx.HcaEncode(self.stream.read(), force_not_looping, quality_level.value)
262
271
  self.hcastream = BytesIO(self.hcabytes)
263
- self.Pyparse_header()
272
+ self._Pyparse_header()
264
273
  if encrypt:
265
274
  if self.key == 0 and not keyless:
266
275
  self.key = 0xCF222F1FE0748978 # Default key.
267
- self.encrypt(self.key, keyless)
276
+ self._encrypt(self.key, keyless)
268
277
  return self.get_hca()
269
278
 
270
- def encrypt(self, keycode: int, subkey: int = 0, keyless: bool = False) -> None:
279
+ def _encrypt(self, keycode: int, subkey: int = 0, keyless: bool = False) -> None:
271
280
  if(self.encrypted):
272
281
  raise ValueError("HCA is already encrypted.")
273
282
  self.encrypted = True
274
283
  enc = CriCodecsEx.HcaCrypt(self.get_hca(), 1, self.header_size, (1 if keyless else 56), keycode, subkey)
275
284
  self.hcastream = BytesIO(enc)
276
285
 
277
- def decrypt(self, keycode: int, subkey: int = 0) -> None:
286
+ def _decrypt(self, keycode: int, subkey: int = 0) -> None:
278
287
  if(not self.encrypted):
279
288
  raise ValueError("HCA is already decrypted.")
280
289
  self.encrypted = False
@@ -282,21 +291,161 @@ class HCA:
282
291
  self.hcastream = BytesIO(dec)
283
292
 
284
293
  def get_hca(self) -> bytes:
285
- """ Use this function to get the HCA file bytes after encrypting or decrypting. """
294
+ """Get the HCA file bytes after encrypting or decrypting. """
286
295
  self.hcastream.seek(0)
287
296
  fl: bytes = self.hcastream.read()
288
297
  self.hcastream.seek(0)
289
298
  return fl
290
299
 
291
300
  def get_frames(self):
292
- """ Generator function to yield Frame number, and Frame data. """
301
+ """Generator function to yield Frame number, and Frame data. """
293
302
  self.hcastream.seek(self.header_size, 0)
294
303
  for i in range(self.hca['FrameCount']):
295
304
  yield (i, self.hcastream.read(self.hca['FrameSize']))
296
305
 
297
306
  def get_header(self) -> bytes:
298
- """ Use this function to retrieve the HCA Header. """
307
+ """Get the HCA Header. """
299
308
  self.hcastream.seek(0)
300
309
  header = self.hcastream.read(self.header_size)
301
310
  self.hcastream.seek(0)
302
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())