PyCriCodecsEx 0.0.5__cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PyCriCodecsEx/hca.py ADDED
@@ -0,0 +1,454 @@
1
+ from io import BytesIO, FileIO
2
+ from struct import *
3
+ from typing import BinaryIO
4
+ from array import array
5
+ import CriCodecsEx
6
+
7
+ from PyCriCodecsEx.chunk import *
8
+ from PyCriCodecsEx.utf import UTFTypeValues, UTFBuilder
9
+ HcaHeaderStruct = Struct(">4sHH")
10
+ HcaFmtHeaderStruct = Struct(">4sIIHH")
11
+ HcaCompHeaderStruct = Struct(">4sHBBBBBBBBBB")
12
+ HcaDecHeaderStruct = Struct(">4sHBBBBBB")
13
+ HcaLoopHeaderStruct = Struct(">4sIIHH")
14
+ HcaAthHeaderStruct = Struct(">4sH")
15
+ HcaVbrHeaderStruct = Struct(">4sHH")
16
+ HcaCiphHeaderStruct = Struct(">4sH")
17
+ HcaRvaHeaderStruct = Struct(">4sf")
18
+
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
+ """
24
+ stream: BinaryIO
25
+ hcastream: BinaryIO
26
+ HcaSig: bytes
27
+ version: int
28
+ header_size: int
29
+ key: int
30
+ subkey: int
31
+ hca: dict
32
+ filetype: str
33
+ wavbytes: bytearray
34
+ hcabytes: bytearray
35
+ riffSignature: bytes
36
+ riffSize: int
37
+ wave: bytes
38
+ fmt: bytes
39
+ fmtSize: int
40
+ fmtType: int
41
+ fmtChannelCount: int
42
+ fmtSamplingRate: int
43
+ fmtSamplesPerSec: int
44
+ fmtSamplingSize: int
45
+ fmtBitCount: int
46
+ dataSig: bytes
47
+ dataSize: int
48
+ encrypted: bool
49
+ enc_table: array
50
+ table: array
51
+ looping: bool
52
+
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
+ """
61
+ if type(stream) == str:
62
+ self.stream = FileIO(stream)
63
+ self.hcastream = FileIO(stream)
64
+ else:
65
+ # copying since for encryption and decryption we use the internal buffer in C++.
66
+ stream = bytearray(stream).copy()
67
+ self.stream = BytesIO(stream)
68
+ self.hcastream = BytesIO(stream)
69
+ if type(key) == str:
70
+ self.key = int(key, 16)
71
+ else:
72
+ self.key = key
73
+ if type(subkey) == str:
74
+ self.subkey = int(subkey, 16)
75
+ else:
76
+ self.subkey = subkey
77
+ self.hcabytes: bytearray = b''
78
+ self.enc_table: array = b''
79
+ self.table: array = b''
80
+ self._Pyparse_header()
81
+
82
+
83
+ def _Pyparse_header(self) -> None:
84
+ self.HcaSig, self.version, self.header_size = HcaHeaderStruct.unpack(
85
+ self.hcastream.read(HcaHeaderStruct.size)
86
+ )
87
+ if self.HcaSig in [HCAType.HCA.value, HCAType.EHCA.value]:
88
+ if not self.hcabytes:
89
+ self.filetype = "hca"
90
+ if self.HcaSig == HCAType.EHCA.value:
91
+ self.encrypted = True
92
+ else:
93
+ self.encrypted = False
94
+ if self.HcaSig != HCAType.HCA.value and self.HcaSig != HCAType.EHCA.value:
95
+ raise ValueError("Invalid HCA file.")
96
+ elif self.HcaSig == HCAType.EHCA.value and not self.key:
97
+ self.key = 0xCF222F1FE0748978 # Default HCA key.
98
+ elif self.key < 0:
99
+ raise ValueError("HCA key cannot be a negative.")
100
+ elif self.key > 0xFFFFFFFFFFFFFFFF:
101
+ raise OverflowError("HCA key cannot exceed the maximum size of 8 bytes.")
102
+ elif self.subkey < 0:
103
+ raise ValueError("HCA subkey cannot be a negative.")
104
+ elif self.subkey > 0xFFFF:
105
+ raise OverflowError("HCA subkey cannot exceed 65535.")
106
+
107
+ fmtsig, temp, framecount, encoder_delay, encoder_padding = HcaFmtHeaderStruct.unpack(
108
+ self.hcastream.read(HcaFmtHeaderStruct.size)
109
+ )
110
+ channelcount = temp >> 24
111
+ samplerate = temp & 0x00FFFFFF
112
+
113
+ self.hca = dict(
114
+ Encrypted = self.encrypted,
115
+ Header=self.HcaSig,
116
+ version=hex(self.version),
117
+ HeaderSize=self.header_size,
118
+ FmtSig = fmtsig,
119
+ ChannelCount = channelcount,
120
+ SampleRate = samplerate,
121
+ FrameCount = framecount,
122
+ EncoderDelay = encoder_delay,
123
+ EncoderPadding = encoder_padding,
124
+ )
125
+
126
+ while True:
127
+ sig = unpack(">I", self.hcastream.read(4))[0]
128
+ self.hcastream.seek(-4, 1)
129
+ sig = int.to_bytes(sig & 0x7F7F7F7F, 4, "big")
130
+ if sig == b"comp":
131
+ compsig, framesize, minres, maxres, trackcount, channelconfig, totalbandcount, basebandcount, stereobandcount, bandsperhfrgroup, r1, r2 = HcaCompHeaderStruct.unpack(
132
+ self.hcastream.read(HcaCompHeaderStruct.size)
133
+ )
134
+ self.hca.update(
135
+ dict(
136
+ CompSig = compsig,
137
+ FrameSize = framesize,
138
+ MinResolution = minres,
139
+ MaxResolution = maxres,
140
+ TrackCount = trackcount,
141
+ ChannelConfig = channelconfig,
142
+ TotalBandCount = totalbandcount,
143
+ BaseBandCount = basebandcount,
144
+ StereoBandCount = stereobandcount,
145
+ BandsPerHfrGroup = bandsperhfrgroup,
146
+ ReservedByte1 = r1,
147
+ ReservedByte2 = r2
148
+ )
149
+ )
150
+ elif sig == b"ciph":
151
+ ciphsig, ciphertype = HcaCiphHeaderStruct.unpack(
152
+ self.hcastream.read(HcaCiphHeaderStruct.size)
153
+ )
154
+ if ciphertype == 1:
155
+ self.encrypted = True
156
+ self.hca.update(dict(CiphSig = ciphsig, CipherType = ciphertype))
157
+ elif sig == b"loop":
158
+ self.looping = True
159
+ loopsig, loopstart, loopend, loopstartdelay, loopendpadding = HcaLoopHeaderStruct.unpack(
160
+ self.hcastream.read(HcaLoopHeaderStruct.size)
161
+ )
162
+ self.hca.update(dict(LoopSig = loopsig, LoopStart = loopstart, LoopEnd = loopend, LoopStartDelay = loopstartdelay, LoopEndPadding = loopendpadding))
163
+ elif sig == b"dec\00":
164
+ decsig, framesize, maxres, minres, totalbandcount, basebandcount, temp, stereotype = HcaDecHeaderStruct.unpack(
165
+ self.hcastream.read(HcaDecHeaderStruct.size)
166
+ )
167
+ trackcount = temp >> 4
168
+ channelconfig = temp & 0xF
169
+ self.hca.update(
170
+ dict(
171
+ DecSig = decsig,
172
+ FrameSize = framesize,
173
+ MinResolution = minres,
174
+ MaxResolution = maxres,
175
+ TotalBandCount = totalbandcount,
176
+ BaseBandCoung = basebandcount,
177
+ TrackCount = trackcount,
178
+ ChannelConfig = channelconfig,
179
+ StereoType = stereotype
180
+ )
181
+ )
182
+ elif sig == b"ath\00":
183
+ athsig, tabletype = HcaAthHeaderStruct.unpack(
184
+ self.hcastream.read(HcaAthHeaderStruct.size)
185
+ )
186
+ self.hca.update(dict(AthSig = athsig, TableType = tabletype))
187
+ elif sig == b"vbr\00":
188
+ vbrsig, maxframesize, noiselevel = HcaVbrHeaderStruct.unpack(
189
+ self.hcastream.read(HcaVbrHeaderStruct.size)
190
+ )
191
+ self.hca.update(dict(VbrSig = vbrsig, MaxFrameSize = maxframesize, NoiseLevel = noiselevel))
192
+ elif sig == b"rva\00":
193
+ rvasig, volume = HcaRvaHeaderStruct.unpack(
194
+ self.hcastream.read(HcaRvaHeaderStruct.size)
195
+ )
196
+ self.hca.update(dict(RvaSig = rvasig, Volume = volume))
197
+ else:
198
+ break
199
+ Crc16 = self.hcastream.read(2)
200
+ self.hca.update(dict(Crc16 = Crc16))
201
+
202
+ elif self.HcaSig == b"RIFF":
203
+ self.filetype = "wav"
204
+ self.riffSignature, self.riffSize, self.wave, self.fmt, self.fmtSize, self.fmtType, self.fmtChannelCount, self.fmtSamplingRate, self.fmtSamplesPerSec, self.fmtSamplingSize, self.fmtBitCount = WavHeaderStruct.unpack(
205
+ self.stream.read(WavHeaderStruct.size)
206
+ )
207
+ if self.riffSignature == b"RIFF" and self.wave == b'WAVE' and self.fmt == b'fmt ':
208
+ if self.fmtBitCount != 16:
209
+ raise ValueError(f"WAV bitdepth of {self.fmtBitCount} is not supported, only 16 bit WAV files are supported.")
210
+ elif self.fmtSize != 16:
211
+ raise ValueError(f"WAV file has an FMT chunk of an unsupported size: {self.fmtSize}, the only supported size is 16.")
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
+ else:
235
+ raise ValueError("Invalid HCA or WAV file.")
236
+ self.stream.seek(0)
237
+ self.hcastream.seek(0)
238
+
239
+ def info(self) -> dict:
240
+ """Returns info related to the input file. """
241
+ if self.filetype == "hca":
242
+ return self.hca
243
+ elif self.filetype == "wav":
244
+ wav = dict(RiffSignature=self.riffSignature.decode(), riffSize=self.riffSize, WaveSignature=self.wave.decode(), fmtSignature=self.fmt.decode(), fmtSize=self.fmtSize, fmtType=self.fmtType, fmtChannelCount=self.fmtChannelCount, fmtSamplingRate=self.fmtSamplingRate, fmtSamplesPerSec=self.fmtSamplesPerSec, fmtSamplingSize=self.fmtSamplingSize, fmtBitCount=self.fmtBitCount, dataSignature=self.dataSig.decode(), dataSize=self.dataSize)
245
+ return wav
246
+
247
+ def decode(self) -> bytes:
248
+ """Decodes the HCA or WAV file to WAV bytes. """
249
+ if self.filetype == "wav":
250
+ raise ValueError("Input type for decoding must be an HCA file.")
251
+ self.hcastream.seek(0)
252
+ self.wavbytes = CriCodecsEx.HcaDecode(self.hcastream.read(), self.header_size, self.key, self.subkey)
253
+ self.stream = BytesIO(self.wavbytes)
254
+ self.hcastream.seek(0)
255
+ return bytes(self.wavbytes)
256
+
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."""
259
+ if self.filetype == "hca":
260
+ raise ValueError("Input type for encoding must be a WAV file.")
261
+ if force_not_looping == False:
262
+ force_not_looping = 0
263
+ elif force_not_looping == True:
264
+ force_not_looping = 1
265
+ else:
266
+ raise ValueError("Forcing the encoder to not loop is by either False or True.")
267
+ if quality_level not in list(CriHcaQuality):
268
+ raise ValueError("Chosen quality level is not valid or is not the appropiate enumeration value.")
269
+ self.stream.seek(0)
270
+ self.hcabytes = CriCodecsEx.HcaEncode(self.stream.read(), force_not_looping, quality_level.value)
271
+ self.hcastream = BytesIO(self.hcabytes)
272
+ self._Pyparse_header()
273
+ if encrypt:
274
+ if self.key == 0 and not keyless:
275
+ self.key = 0xCF222F1FE0748978 # Default key.
276
+ self._encrypt(self.key, keyless)
277
+ return self.get_hca()
278
+
279
+ def _encrypt(self, keycode: int, subkey: int = 0, keyless: bool = False) -> None:
280
+ if(self.encrypted):
281
+ raise ValueError("HCA is already encrypted.")
282
+ self.encrypted = True
283
+ enc = CriCodecsEx.HcaCrypt(self.get_hca(), 1, self.header_size, (1 if keyless else 56), keycode, subkey)
284
+ self.hcastream = BytesIO(enc)
285
+
286
+ def _decrypt(self, keycode: int, subkey: int = 0) -> None:
287
+ if(not self.encrypted):
288
+ raise ValueError("HCA is already decrypted.")
289
+ self.encrypted = False
290
+ dec = CriCodecsEx.HcaCrypt(self.get_hca(), 0, self.header_size, 0, keycode, subkey)
291
+ self.hcastream = BytesIO(dec)
292
+
293
+ def get_hca(self) -> bytes:
294
+ """Get the HCA file bytes after encrypting or decrypting. """
295
+ self.hcastream.seek(0)
296
+ fl: bytes = self.hcastream.read()
297
+ self.hcastream.seek(0)
298
+ return fl
299
+
300
+ def get_frames(self):
301
+ """Generator function to yield Frame number, and Frame data. """
302
+ self.hcastream.seek(self.header_size, 0)
303
+ for i in range(self.hca['FrameCount']):
304
+ yield (i, self.hcastream.read(self.hca['FrameSize']))
305
+
306
+ def get_header(self) -> bytes:
307
+ """Get the HCA Header. """
308
+ self.hcastream.seek(0)
309
+ header = self.hcastream.read(self.header_size)
310
+ self.hcastream.seek(0)
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())