PyCriCodecsEx 0.0.1__cp310-cp310-musllinux_1_2_x86_64.whl → 0.0.3__cp310-cp310-musllinux_1_2_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.
Potentially problematic release.
This version of PyCriCodecsEx might be problematic. Click here for more details.
- 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 +17 -0
- pycricodecsex-0.0.1.dist-info/METADATA +0 -81
- pycricodecsex-0.0.1.dist-info/RECORD +0 -17
- {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/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.3"
|
PyCriCodecsEx/acb.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
from struct import iter_unpack
|
|
2
|
-
from typing import BinaryIO, List
|
|
3
|
-
from io import BytesIO
|
|
4
|
-
from PyCriCodecsEx.chunk import *
|
|
5
|
-
from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
|
|
6
|
-
from PyCriCodecsEx.awb import AWB, AWBBuilder
|
|
7
|
-
from PyCriCodecsEx.hca import HCA
|
|
8
|
-
from copy import deepcopy
|
|
9
|
-
import os
|
|
10
|
-
|
|
11
1
|
# Credit:
|
|
12
2
|
# - github.com/vgmstream/vgmstream which is why this is possible at all
|
|
13
3
|
# - Original work by https://github.com/Youjose/PyCriCodecs
|
|
14
4
|
# See Research/ACBSchema.py for more details.
|
|
15
5
|
|
|
6
|
+
from typing import Generator, List, Tuple, BinaryIO
|
|
7
|
+
from PyCriCodecsEx.chunk import *
|
|
8
|
+
from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
|
|
9
|
+
from PyCriCodecsEx.hca import HCACodec
|
|
10
|
+
from PyCriCodecsEx.adx import ADXCodec
|
|
11
|
+
from PyCriCodecsEx.awb import AWB, AWBBuilder
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from copy import deepcopy
|
|
14
|
+
|
|
16
15
|
class CueNameTable(UTFViewer):
|
|
17
16
|
CueIndex: int
|
|
18
17
|
CueName: str
|
|
@@ -20,11 +19,13 @@ class CueNameTable(UTFViewer):
|
|
|
20
19
|
|
|
21
20
|
class CueTable(UTFViewer):
|
|
22
21
|
CueId: int
|
|
22
|
+
Length: int
|
|
23
23
|
ReferenceIndex: int
|
|
24
24
|
ReferenceType: int
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class SequenceTable(UTFViewer):
|
|
28
|
+
NumTracks : int
|
|
28
29
|
TrackIndex: bytes
|
|
29
30
|
Type: int
|
|
30
31
|
|
|
@@ -65,29 +66,204 @@ class ACBTable(UTFViewer):
|
|
|
65
66
|
TrackTable: List[TrackTable]
|
|
66
67
|
WaveformTable: List[WaveformTable]
|
|
67
68
|
|
|
69
|
+
@staticmethod
|
|
70
|
+
def decode_tlv(data : bytes):
|
|
71
|
+
pos = 0
|
|
72
|
+
while pos < len(data):
|
|
73
|
+
tag = data[pos : pos + 2]
|
|
74
|
+
length = data[pos + 3]
|
|
75
|
+
value = data[pos + 4 : pos + 4 + length]
|
|
76
|
+
pos += 3 + length
|
|
77
|
+
yield (tag, value)
|
|
78
|
+
|
|
79
|
+
def waveform_of_track(self, index: int):
|
|
80
|
+
tlv = self.decode_tlv(self.TrackEventTable[index])
|
|
81
|
+
def noteOn(data: bytes):
|
|
82
|
+
# Handle note on event
|
|
83
|
+
tlv_type, tlv_index = AcbTrackCommandNoteOnStruct.unpack(data[:AcbTrackCommandNoteOnStruct.size])
|
|
84
|
+
match tlv_type:
|
|
85
|
+
case 0x02: # Synth
|
|
86
|
+
yield from self.waveform_of_synth(tlv_index)
|
|
87
|
+
case 0x03: # Sequence
|
|
88
|
+
yield from self.waveform_of_sequence(tlv_index)
|
|
89
|
+
# Ignore others silently
|
|
90
|
+
for code, data in tlv:
|
|
91
|
+
match code:
|
|
92
|
+
case 2000:
|
|
93
|
+
yield from noteOn(data)
|
|
94
|
+
case 2003:
|
|
95
|
+
yield from noteOn(data)
|
|
96
|
+
|
|
97
|
+
def waveform_of_sequence(self, index : int):
|
|
98
|
+
seq = self.SequenceTable[index]
|
|
99
|
+
for i in range(seq.NumTracks):
|
|
100
|
+
track_index = int.from_bytes(seq.TrackIndex[i*2:i*2+2], 'big')
|
|
101
|
+
yield self.WaveformTable[track_index]
|
|
102
|
+
|
|
103
|
+
def waveform_of_synth(self, index: int):
|
|
104
|
+
item_type, item_index = AcbSynthReferenceStruct.unpack(self.SynthTable[index].ReferenceItems)
|
|
105
|
+
match item_type:
|
|
106
|
+
case 0x00: # No audio
|
|
107
|
+
return
|
|
108
|
+
case 0x01: # Waveform
|
|
109
|
+
yield self.WaveformTable[item_index]
|
|
110
|
+
case 0x02: # Yet another synth...
|
|
111
|
+
yield from self.waveform_of_synth(item_index)
|
|
112
|
+
case 0x03: # Sequence
|
|
113
|
+
yield from self.waveform_of_sequence(item_index)
|
|
114
|
+
case _:
|
|
115
|
+
raise NotImplementedError(f"Unknown synth reference type: {item_type} at index {index}")
|
|
116
|
+
|
|
117
|
+
def waveform_of(self, index : int) -> List["WaveformTable"]:
|
|
118
|
+
cue = next(filter(lambda c: c.CueId == index, self.CueTable), None)
|
|
119
|
+
assert cue, "cue of index %d not found" % index
|
|
120
|
+
match cue.ReferenceType:
|
|
121
|
+
case 0x01:
|
|
122
|
+
return [self.WaveformTable[index]]
|
|
123
|
+
case 0x02:
|
|
124
|
+
return list(self.waveform_of_synth(index))
|
|
125
|
+
case 0x03:
|
|
126
|
+
return list(self.waveform_of_sequence(index))
|
|
127
|
+
case 0x08:
|
|
128
|
+
raise NotImplementedError("BlockSequence type not implemented yet")
|
|
129
|
+
case _:
|
|
130
|
+
raise NotImplementedError(f"Unknown cue reference type: {cue.ReferenceType}")
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class CueItem:
|
|
134
|
+
CueId: int
|
|
135
|
+
CueName: str
|
|
136
|
+
Length: float
|
|
137
|
+
Waveforms: list[int] # List of waveform IDs
|
|
68
138
|
|
|
69
139
|
class ACB(UTF):
|
|
70
|
-
"""
|
|
71
|
-
def __init__(self,
|
|
72
|
-
|
|
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)
|
|
73
148
|
|
|
74
149
|
@property
|
|
75
150
|
def payload(self) -> dict:
|
|
76
|
-
"""Retrives the only
|
|
151
|
+
"""Retrives the only UTF table dict within the ACB file."""
|
|
77
152
|
return self.dictarray[0]
|
|
78
153
|
|
|
79
154
|
@property
|
|
80
155
|
def view(self) -> ACBTable:
|
|
81
|
-
"""Returns a view of the ACB file, with all known tables mapped to their respective classes.
|
|
156
|
+
"""Returns a view of the ACB file, with all known tables mapped to their respective classes.
|
|
157
|
+
|
|
158
|
+
* Use this to interact with the ACB payload instead of `payload` for helper functions, etc"""
|
|
82
159
|
return ACBTable(self.payload)
|
|
83
160
|
|
|
84
|
-
|
|
85
|
-
|
|
161
|
+
@property
|
|
162
|
+
def name(self) -> str:
|
|
163
|
+
"""Returns the name of the ACB file."""
|
|
164
|
+
return self.view.Name
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def awb(self) -> AWB:
|
|
168
|
+
"""Returns the AWB object associated with the ACB."""
|
|
169
|
+
return AWB(self.view.AwbFile)
|
|
170
|
+
|
|
171
|
+
def get_waveforms(self) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
|
|
172
|
+
"""Returns a list of decoded waveforms.
|
|
173
|
+
|
|
174
|
+
Item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
|
|
175
|
+
"""
|
|
176
|
+
CODEC_TABLE = {
|
|
177
|
+
AcbEncodeTypes.ADX: ADXCodec,
|
|
178
|
+
AcbEncodeTypes.HCA: HCACodec,
|
|
179
|
+
AcbEncodeTypes.HCAMX: HCACodec,
|
|
180
|
+
}
|
|
181
|
+
awb = self.awb
|
|
182
|
+
wavs = []
|
|
183
|
+
for wav in self.view.WaveformTable:
|
|
184
|
+
encode = AcbEncodeTypes(wav.EncodeType)
|
|
185
|
+
codec = (CODEC_TABLE.get(encode, None))
|
|
186
|
+
if codec:
|
|
187
|
+
wavs.append(codec(awb.get_file_at(wav.MemoryAwbId)))
|
|
188
|
+
else:
|
|
189
|
+
wavs.append((encode, wav.NumChannels, wav.NumSamples, wav.SamplingRate, awb.get_file_at(wav.MemoryAwbId)))
|
|
190
|
+
return wavs
|
|
191
|
+
|
|
192
|
+
def set_waveforms(self, value: List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]):
|
|
193
|
+
"""Sets the waveform data.
|
|
194
|
+
|
|
195
|
+
Input item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
|
|
196
|
+
|
|
197
|
+
NOTE: Cue duration is not set. You need to change that manually.
|
|
198
|
+
"""
|
|
199
|
+
WAVEFORM = self.view.WaveformTable[0]._payload.copy()
|
|
200
|
+
encoded = []
|
|
201
|
+
tables = self.view.WaveformTable
|
|
202
|
+
tables.clear()
|
|
203
|
+
for i, codec in enumerate(value):
|
|
204
|
+
if type(codec) == HCACodec:
|
|
205
|
+
encoded.append(codec.get_encoded())
|
|
206
|
+
tables.append(WaveformTable(WAVEFORM.copy()))
|
|
207
|
+
entry = tables[-1]
|
|
208
|
+
entry.EncodeType = AcbEncodeTypes.HCA.value
|
|
209
|
+
entry.NumChannels = codec.chnls
|
|
210
|
+
entry.NumSamples = codec.total_samples
|
|
211
|
+
entry.SamplingRate = codec.sampling_rate
|
|
212
|
+
elif type(codec) == ADXCodec:
|
|
213
|
+
encoded.append(codec.get_encoded())
|
|
214
|
+
tables.append(WaveformTable(WAVEFORM.copy()))
|
|
215
|
+
entry = tables[-1]
|
|
216
|
+
entry.EncodeType = AcbEncodeTypes.ADX.value
|
|
217
|
+
entry.NumChannels = codec.chnls
|
|
218
|
+
entry.NumSamples = codec.total_samples
|
|
219
|
+
entry.SamplingRate = codec.sampling_rate
|
|
220
|
+
elif isinstance(codec, tuple):
|
|
221
|
+
e_type, e_channels, e_samples, e_rate, e_data = codec
|
|
222
|
+
encoded.append(e_data)
|
|
223
|
+
tables.append(WaveformTable(WAVEFORM.copy()))
|
|
224
|
+
entry = tables[-1]
|
|
225
|
+
entry.EncodeType = e_type.value
|
|
226
|
+
entry.NumChannels = e_channels
|
|
227
|
+
entry.NumSamples = e_samples
|
|
228
|
+
entry.SamplingRate = e_rate
|
|
229
|
+
else:
|
|
230
|
+
raise TypeError(f"Unsupported codec type: {type(codec)}")
|
|
231
|
+
tables[-1].MemoryAwbId = i
|
|
232
|
+
awb = self.awb
|
|
233
|
+
self.view.AwbFile = AWBBuilder(encoded, awb.subkey, awb.version, align=awb.align).build()
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def cues(self) -> Generator[CueItem, None, None]:
|
|
238
|
+
"""Returns a generator of **read-only** Cues.
|
|
239
|
+
|
|
240
|
+
Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
|
|
241
|
+
To modify cues, use the `view` property instead.
|
|
242
|
+
"""
|
|
243
|
+
for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
|
|
244
|
+
waveforms = self.view.waveform_of(cue.CueId)
|
|
245
|
+
yield CueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
|
|
86
246
|
|
|
87
247
|
class ACBBuilder:
|
|
248
|
+
"""Use this class to build ACB files from an existing ACB object."""
|
|
88
249
|
acb: ACB
|
|
89
250
|
|
|
90
251
|
def __init__(self, acb: ACB) -> None:
|
|
252
|
+
"""Initializes the ACBBuilder with an existing ACB object.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
acb (ACB): The ACB object to build from.
|
|
256
|
+
|
|
257
|
+
Building ACB from scratch isn't planned for now since:
|
|
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.
|
|
264
|
+
|
|
265
|
+
Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
|
|
266
|
+
"""
|
|
91
267
|
self.acb = acb
|
|
92
268
|
|
|
93
269
|
def build(self) -> bytes:
|
|
@@ -95,6 +271,9 @@ class ACBBuilder:
|
|
|
95
271
|
|
|
96
272
|
The object may be modified in place before building, which will be reflected in the output binary.
|
|
97
273
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
274
|
+
# Check whether all AWB indices are valid
|
|
275
|
+
assert all(
|
|
276
|
+
waveform.MemoryAwbId < self.acb.awb.numfiles for waveform in self.acb.view.WaveformTable
|
|
277
|
+
), "one or more AWB indices are out of range"
|
|
278
|
+
binary = UTFBuilder(self.acb.dictarray, encoding=self.acb.encoding, table_name=self.acb.table_name)
|
|
100
279
|
return binary.bytes()
|
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
|
|
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
|
@@ -19,16 +19,21 @@ class AWB:
|
|
|
19
19
|
headersize: int
|
|
20
20
|
id_alignment: int
|
|
21
21
|
|
|
22
|
-
def __init__(self, stream) -> None:
|
|
22
|
+
def __init__(self, stream : str | BinaryIO) -> None:
|
|
23
|
+
"""Initializes the AWB object
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
stream (str | BinaryIO): Source file path or binary stream
|
|
27
|
+
"""
|
|
23
28
|
if type(stream) == str:
|
|
24
29
|
self.stream = FileIO(stream)
|
|
25
30
|
self.filename = stream
|
|
26
31
|
else:
|
|
27
32
|
self.stream = BytesIO(stream)
|
|
28
33
|
self.filename = ""
|
|
29
|
-
self.
|
|
34
|
+
self._readheader()
|
|
30
35
|
|
|
31
|
-
def
|
|
36
|
+
def _readheader(self):
|
|
32
37
|
# Reads header.
|
|
33
38
|
magic, self.version, offset_intsize, self.id_intsize, self.numfiles, self.align, self.subkey = AWBChunkHeader.unpack(
|
|
34
39
|
self.stream.read(AWBChunkHeader.size)
|
|
@@ -39,9 +44,9 @@ class AWB:
|
|
|
39
44
|
# Reads data in the header.
|
|
40
45
|
self.ids = list()
|
|
41
46
|
self.ofs = list()
|
|
42
|
-
for i in iter_unpack(f"<{self.
|
|
47
|
+
for i in iter_unpack(f"<{self._stringtypes(self.id_intsize)}", self.stream.read(self.id_intsize*self.numfiles)):
|
|
43
48
|
self.ids.append(i[0])
|
|
44
|
-
for i in iter_unpack(f"<{self.
|
|
49
|
+
for i in iter_unpack(f"<{self._stringtypes(offset_intsize)}", self.stream.read(offset_intsize*(self.numfiles+1))):
|
|
45
50
|
self.ofs.append(i[0] if i[0] % self.align == 0 else (i[0] + (self.align - (i[0] % self.align))))
|
|
46
51
|
|
|
47
52
|
# Seeks to files offset.
|
|
@@ -51,21 +56,20 @@ class AWB:
|
|
|
51
56
|
self.stream.seek(self.headersize, 0)
|
|
52
57
|
|
|
53
58
|
def get_files(self):
|
|
54
|
-
"""
|
|
59
|
+
"""Generator function to yield all data blobs from an AWB. """
|
|
60
|
+
self.stream.seek(self.headersize, 0)
|
|
55
61
|
for i in range(1, len(self.ofs)):
|
|
56
62
|
data = self.stream.read((self.ofs[i]-self.ofs[i-1]))
|
|
57
63
|
self.stream.seek(self.ofs[i], 0)
|
|
58
64
|
yield data
|
|
59
65
|
|
|
60
|
-
def get_file_at(self, index):
|
|
61
|
-
"""
|
|
62
|
-
index += 1
|
|
66
|
+
def get_file_at(self, index) -> bytes:
|
|
67
|
+
"""Gets you a file at specific index. """
|
|
63
68
|
self.stream.seek(self.ofs[index], 0)
|
|
64
|
-
data = self.stream.read(self.ofs[index]-self.ofs[index
|
|
65
|
-
self.stream.seek(self.headersize, 0) # Seeks back to headersize for getfiles.
|
|
69
|
+
data = self.stream.read(self.ofs[index + 1]-self.ofs[index])
|
|
66
70
|
return data
|
|
67
71
|
|
|
68
|
-
def
|
|
72
|
+
def _stringtypes(self, intsize: int) -> str:
|
|
69
73
|
if intsize == 1:
|
|
70
74
|
return "B" # Probably impossible.
|
|
71
75
|
elif intsize == 2:
|
|
@@ -78,7 +82,17 @@ class AWB:
|
|
|
78
82
|
raise ValueError("Unknown int size.")
|
|
79
83
|
|
|
80
84
|
class AWBBuilder:
|
|
85
|
+
"""Use this class to build AWB files from a list of bytes."""
|
|
81
86
|
def __init__(self, infiles: list[bytes], subkey: int = 0, version: int = 2, id_intsize = 0x2, align: int = 0x20) -> None:
|
|
87
|
+
"""Initializes the AWB builder.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
infiles (list[bytes]): List of bytes to be included in the AWB file.
|
|
91
|
+
subkey (int, optional): AWB subkey. Defaults to 0.
|
|
92
|
+
version (int, optional): AWB version. Defaults to 2.
|
|
93
|
+
id_intsize (hexadecimal, optional): Integer size (in bytes) for string lengths. Defaults to 0x2.
|
|
94
|
+
align (int, optional): Alignment. Defaults to 0x20.
|
|
95
|
+
"""
|
|
82
96
|
if version == 1 and subkey != 0:
|
|
83
97
|
raise ValueError("Cannot have a subkey with AWB version of 1.")
|
|
84
98
|
elif id_intsize not in [0x2, 0x4, 0x8]:
|
|
@@ -89,7 +103,7 @@ class AWBBuilder:
|
|
|
89
103
|
self.subkey = subkey
|
|
90
104
|
self.id_intsize = id_intsize
|
|
91
105
|
|
|
92
|
-
def
|
|
106
|
+
def _stringtypes(self, intsize: int) -> str:
|
|
93
107
|
if intsize == 1:
|
|
94
108
|
return "B" # Probably impossible.
|
|
95
109
|
elif intsize == 2:
|
|
@@ -102,6 +116,7 @@ class AWBBuilder:
|
|
|
102
116
|
raise ValueError("Unknown int size.")
|
|
103
117
|
|
|
104
118
|
def build(self) -> bytes:
|
|
119
|
+
"""Builds the AWB file from the provided infiles bytes."""
|
|
105
120
|
size = 0
|
|
106
121
|
ofs = []
|
|
107
122
|
numfiles = 0
|
|
@@ -122,7 +137,7 @@ class AWBBuilder:
|
|
|
122
137
|
b'AFS2', self.version, intsize, self.id_intsize, numfiles, self.align, self.subkey
|
|
123
138
|
)
|
|
124
139
|
|
|
125
|
-
id_strsize = f"<{self.
|
|
140
|
+
id_strsize = f"<{self._stringtypes(self.id_intsize)}"
|
|
126
141
|
for i in range(numfiles):
|
|
127
142
|
header += pack(id_strsize, i)
|
|
128
143
|
|
PyCriCodecsEx/chunk.py
CHANGED
|
@@ -12,7 +12,8 @@ WavNoteHeaderStruct = Struct("<4sII")
|
|
|
12
12
|
WavDataHeaderStruct = Struct("<4sI")
|
|
13
13
|
AdxHeaderStruct = Struct(">HHBBBBIIHBB")
|
|
14
14
|
AdxLoopHeaderStruct = Struct(">HHHHIIII")
|
|
15
|
-
|
|
15
|
+
AcbSynthReferenceStruct = Struct(">HH")
|
|
16
|
+
AcbTrackCommandNoteOnStruct = Struct(">HH")
|
|
16
17
|
class USMChunckHeaderType(Enum):
|
|
17
18
|
CRID = b"CRID" # Header.
|
|
18
19
|
SFSH = b"SFSH" # SofDec1 Header?
|
|
@@ -45,11 +46,6 @@ class HCAType(Enum):
|
|
|
45
46
|
HCA = b"HCA\x00" # Header.
|
|
46
47
|
EHCA = b"\xC8\xC3\xC1\x00" # Encrypted HCA header.
|
|
47
48
|
|
|
48
|
-
class VideoType(Enum):
|
|
49
|
-
IVF = b"DKIF" # Header.
|
|
50
|
-
# H264 = b"" # Header.
|
|
51
|
-
# MPEG = b"" # Header.
|
|
52
|
-
|
|
53
49
|
# I saw some devs swap the unsigned/signed indexes. So I am not sure what's correct or not.
|
|
54
50
|
# In my own experience, swapping those results in an incorrect signed values (should be unsigned) in ACB's/CPK's.
|
|
55
51
|
# If someone were to change this, they must change 'stringtypes' function in UTF/UTFBuilder classes.
|
|
@@ -72,4 +68,25 @@ class CriHcaQuality(Enum):
|
|
|
72
68
|
High = 1
|
|
73
69
|
Middle = 2
|
|
74
70
|
Low = 3
|
|
75
|
-
Lowest = 5
|
|
71
|
+
Lowest = 5
|
|
72
|
+
|
|
73
|
+
class AcbEncodeTypes(Enum):
|
|
74
|
+
ADX = 0
|
|
75
|
+
PCM = 1
|
|
76
|
+
HCA = 2
|
|
77
|
+
ADX_ALT = 3
|
|
78
|
+
WiiDSP = 4
|
|
79
|
+
NDSDSP = 5
|
|
80
|
+
HCAMX = 6
|
|
81
|
+
VAG = 7
|
|
82
|
+
ATRAC3 = 8
|
|
83
|
+
CWAV = 9
|
|
84
|
+
HEVAG = 10
|
|
85
|
+
ATRAC9 = 11
|
|
86
|
+
X360XMA = 12
|
|
87
|
+
DSP = 13
|
|
88
|
+
CWACDSP = 14
|
|
89
|
+
PS4HEVAG = 15
|
|
90
|
+
PS4ATRAC9 = 16
|
|
91
|
+
AACM4A = 17
|
|
92
|
+
SwitchOpus = 18
|