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.
- CriCodecsEx.cpython-312-x86_64-linux-gnu.so +0 -0
- PyCriCodecsEx/__init__.py +1 -0
- PyCriCodecsEx/acb.py +306 -0
- PyCriCodecsEx/adx.py +158 -0
- PyCriCodecsEx/awb.py +165 -0
- PyCriCodecsEx/chunk.py +92 -0
- PyCriCodecsEx/cpk.py +743 -0
- PyCriCodecsEx/hca.py +454 -0
- PyCriCodecsEx/usm.py +1001 -0
- PyCriCodecsEx/utf.py +692 -0
- pycricodecsex-0.0.5.dist-info/METADATA +35 -0
- pycricodecsex-0.0.5.dist-info/RECORD +15 -0
- pycricodecsex-0.0.5.dist-info/WHEEL +6 -0
- pycricodecsex-0.0.5.dist-info/licenses/LICENSE +21 -0
- pycricodecsex-0.0.5.dist-info/top_level.txt +2 -0
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())
|