PyCriCodecsEx 0.0.1__cp310-cp310-macosx_11_0_arm64.whl → 0.0.3__cp310-cp310-macosx_11_0_arm64.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.
- CriCodecsEx.cpython-310-darwin.so +0 -0
- PyCriCodecsEx/__init__.py +1 -1
- PyCriCodecsEx/acb.py +198 -19
- PyCriCodecsEx/adx.py +143 -4
- PyCriCodecsEx/awb.py +29 -14
- PyCriCodecsEx/chunk.py +24 -7
- PyCriCodecsEx/cpk.py +25 -16
- PyCriCodecsEx/hca.py +187 -38
- PyCriCodecsEx/usm.py +68 -339
- PyCriCodecsEx/utf.py +25 -37
- pycricodecsex-0.0.3.dist-info/METADATA +35 -0
- pycricodecsex-0.0.3.dist-info/RECORD +15 -0
- pycricodecsex-0.0.1.dist-info/METADATA +0 -81
- pycricodecsex-0.0.1.dist-info/RECORD +0 -15
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.3.dist-info}/WHEEL +0 -0
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.3.dist-info}/top_level.txt +0 -0
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
|
|
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:
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
80
|
+
self._Pyparse_header()
|
|
70
81
|
|
|
71
82
|
|
|
72
|
-
def
|
|
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
|
-
|
|
202
|
-
self.stream.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
self.
|
|
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
|
-
"""
|
|
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.
|
|
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.
|
|
276
|
+
self._encrypt(self.key, keyless)
|
|
268
277
|
return self.get_hca()
|
|
269
278
|
|
|
270
|
-
def
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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())
|