PyCriCodecsEx 0.0.1__cp310-cp310-win32.whl → 0.0.2__cp310-cp310-win32.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.
- CriCodecsEx.cp310-win32.pyd +0 -0
- PyCriCodecsEx/__init__.py +1 -1
- PyCriCodecsEx/acb.py +184 -16
- PyCriCodecsEx/awb.py +28 -14
- PyCriCodecsEx/chunk.py +24 -2
- PyCriCodecsEx/cpk.py +9 -4
- PyCriCodecsEx/hca.py +20 -11
- PyCriCodecsEx/usm.py +65 -96
- PyCriCodecsEx/utf.py +17 -13
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.2.dist-info}/METADATA +8 -3
- pycricodecsex-0.0.2.dist-info/RECORD +15 -0
- pycricodecsex-0.0.1.dist-info/RECORD +0 -15
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.2.dist-info}/WHEEL +0 -0
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {pycricodecsex-0.0.1.dist-info → pycricodecsex-0.0.2.dist-info}/top_level.txt +0 -0
CriCodecsEx.cp310-win32.pyd
CHANGED
|
Binary file
|
PyCriCodecsEx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.2"
|
PyCriCodecsEx/acb.py
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
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
|
|
7
|
+
from PyCriCodecsEx.chunk import *
|
|
8
|
+
from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
|
|
9
|
+
from PyCriCodecsEx.usm import HCACodec, ADXCodec
|
|
10
|
+
from PyCriCodecsEx.awb import AWB, AWBBuilder
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
|
|
16
14
|
class CueNameTable(UTFViewer):
|
|
17
15
|
CueIndex: int
|
|
18
16
|
CueName: str
|
|
@@ -20,11 +18,13 @@ class CueNameTable(UTFViewer):
|
|
|
20
18
|
|
|
21
19
|
class CueTable(UTFViewer):
|
|
22
20
|
CueId: int
|
|
21
|
+
Length: int
|
|
23
22
|
ReferenceIndex: int
|
|
24
23
|
ReferenceType: int
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class SequenceTable(UTFViewer):
|
|
27
|
+
NumTracks : int
|
|
28
28
|
TrackIndex: bytes
|
|
29
29
|
Type: int
|
|
30
30
|
|
|
@@ -65,6 +65,75 @@ class ACBTable(UTFViewer):
|
|
|
65
65
|
TrackTable: List[TrackTable]
|
|
66
66
|
WaveformTable: List[WaveformTable]
|
|
67
67
|
|
|
68
|
+
@staticmethod
|
|
69
|
+
def decode_tlv(data : bytes):
|
|
70
|
+
pos = 0
|
|
71
|
+
while pos < len(data):
|
|
72
|
+
tag = data[pos : pos + 2]
|
|
73
|
+
length = data[pos + 3]
|
|
74
|
+
value = data[pos + 4 : pos + 4 + length]
|
|
75
|
+
pos += 3 + length
|
|
76
|
+
yield (tag, value)
|
|
77
|
+
|
|
78
|
+
def waveform_of_track(self, index: int):
|
|
79
|
+
tlv = self.decode_tlv(self.TrackEventTable[index])
|
|
80
|
+
def noteOn(data: bytes):
|
|
81
|
+
# Handle note on event
|
|
82
|
+
tlv_type, tlv_index = AcbTrackCommandNoteOnStruct.unpack(data[:AcbTrackCommandNoteOnStruct.size])
|
|
83
|
+
match tlv_type:
|
|
84
|
+
case 0x02: # Synth
|
|
85
|
+
yield from self.waveform_of_synth(tlv_index)
|
|
86
|
+
case 0x03: # Sequence
|
|
87
|
+
yield from self.waveform_of_sequence(tlv_index)
|
|
88
|
+
# Ignore others silently
|
|
89
|
+
for code, data in tlv:
|
|
90
|
+
match code:
|
|
91
|
+
case 2000:
|
|
92
|
+
yield from noteOn(data)
|
|
93
|
+
case 2003:
|
|
94
|
+
yield from noteOn(data)
|
|
95
|
+
|
|
96
|
+
def waveform_of_sequence(self, index : int):
|
|
97
|
+
seq = self.SequenceTable[index]
|
|
98
|
+
for i in range(seq.NumTracks):
|
|
99
|
+
track_index = int.from_bytes(seq.TrackIndex[i*2:i*2+2], 'big')
|
|
100
|
+
yield self.WaveformTable[track_index]
|
|
101
|
+
|
|
102
|
+
def waveform_of_synth(self, index: int):
|
|
103
|
+
item_type, item_index = AcbSynthReferenceStruct.unpack(self.SynthTable[index].ReferenceItems)
|
|
104
|
+
match item_type:
|
|
105
|
+
case 0x00: # No audio
|
|
106
|
+
return
|
|
107
|
+
case 0x01: # Waveform
|
|
108
|
+
yield self.WaveformTable[item_index]
|
|
109
|
+
case 0x02: # Yet another synth...
|
|
110
|
+
yield from self.waveform_of_synth(item_index)
|
|
111
|
+
case 0x03: # Sequence
|
|
112
|
+
yield from self.waveform_of_sequence(item_index)
|
|
113
|
+
case _:
|
|
114
|
+
raise NotImplementedError(f"Unknown synth reference type: {item_type} at index {index}")
|
|
115
|
+
|
|
116
|
+
def waveform_of(self, index : int) -> List["WaveformTable"]:
|
|
117
|
+
cue = next(filter(lambda c: c.CueId == index, self.CueTable), None)
|
|
118
|
+
assert cue, "cue of index %d not found" % index
|
|
119
|
+
match cue.ReferenceType:
|
|
120
|
+
case 0x01:
|
|
121
|
+
return [self.WaveformTable[index]]
|
|
122
|
+
case 0x02:
|
|
123
|
+
return list(self.waveform_of_synth(index))
|
|
124
|
+
case 0x03:
|
|
125
|
+
return list(self.waveform_of_sequence(index))
|
|
126
|
+
case 0x08:
|
|
127
|
+
raise NotImplementedError("BlockSequence type not implemented yet")
|
|
128
|
+
case _:
|
|
129
|
+
raise NotImplementedError(f"Unknown cue reference type: {cue.ReferenceType}")
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class CueItem:
|
|
133
|
+
CueId: int
|
|
134
|
+
CueName: str
|
|
135
|
+
Length: float
|
|
136
|
+
Waveforms: list[int] # List of waveform IDs
|
|
68
137
|
|
|
69
138
|
class ACB(UTF):
|
|
70
139
|
"""An ACB is basically a giant @UTF table. Use this class to extract any ACB, and potentially modifiy it in place."""
|
|
@@ -73,21 +142,117 @@ class ACB(UTF):
|
|
|
73
142
|
|
|
74
143
|
@property
|
|
75
144
|
def payload(self) -> dict:
|
|
76
|
-
"""Retrives the only
|
|
145
|
+
"""Retrives the only UTF table dict within the ACB file."""
|
|
77
146
|
return self.dictarray[0]
|
|
78
147
|
|
|
79
148
|
@property
|
|
80
149
|
def view(self) -> ACBTable:
|
|
81
|
-
"""Returns a view of the ACB file, with all known tables mapped to their respective classes.
|
|
150
|
+
"""Returns a view of the ACB file, with all known tables mapped to their respective classes.
|
|
151
|
+
|
|
152
|
+
* Use this to interact with the ACB payload instead of `payload` for helper functions, etc"""
|
|
82
153
|
return ACBTable(self.payload)
|
|
83
154
|
|
|
84
|
-
|
|
85
|
-
|
|
155
|
+
@property
|
|
156
|
+
def name(self) -> str:
|
|
157
|
+
"""Returns the name of the ACB file."""
|
|
158
|
+
return self.view.Name
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def awb(self) -> AWB:
|
|
162
|
+
"""Returns the AWB object associated with the ACB."""
|
|
163
|
+
return AWB(self.view.AwbFile)
|
|
164
|
+
|
|
165
|
+
def get_waveforms(self) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
|
|
166
|
+
"""Returns a list of decoded waveforms.
|
|
167
|
+
|
|
168
|
+
Item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
|
|
169
|
+
"""
|
|
170
|
+
CODEC_TABLE = {
|
|
171
|
+
AcbEncodeTypes.ADX: ADXCodec,
|
|
172
|
+
AcbEncodeTypes.HCA: HCACodec,
|
|
173
|
+
AcbEncodeTypes.HCAMX: HCACodec,
|
|
174
|
+
}
|
|
175
|
+
awb = self.awb
|
|
176
|
+
wavs = []
|
|
177
|
+
for wav in self.view.WaveformTable:
|
|
178
|
+
encode = AcbEncodeTypes(wav.EncodeType)
|
|
179
|
+
codec = (CODEC_TABLE.get(encode, None))
|
|
180
|
+
if codec:
|
|
181
|
+
wavs.append(codec(awb.get_file_at(wav.MemoryAwbId)))
|
|
182
|
+
else:
|
|
183
|
+
wavs.append((encode, wav.NumChannels, wav.NumSamples, wav.SamplingRate, awb.get_file_at(wav.MemoryAwbId)))
|
|
184
|
+
return wavs
|
|
185
|
+
|
|
186
|
+
def set_waveforms(self, value: List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]):
|
|
187
|
+
"""Sets the waveform data.
|
|
188
|
+
|
|
189
|
+
Input item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
|
|
190
|
+
|
|
191
|
+
NOTE: Cue duration is not set. You need to change that manually.
|
|
192
|
+
"""
|
|
193
|
+
WAVEFORM = self.view.WaveformTable[0]._payload.copy()
|
|
194
|
+
encoded = []
|
|
195
|
+
tables = self.view.WaveformTable
|
|
196
|
+
tables.clear()
|
|
197
|
+
for i, codec in enumerate(value):
|
|
198
|
+
if type(codec) == HCACodec:
|
|
199
|
+
encoded.append(codec.get_encoded())
|
|
200
|
+
tables.append(WaveformTable(WAVEFORM.copy()))
|
|
201
|
+
entry = tables[-1]
|
|
202
|
+
entry.EncodeType = AcbEncodeTypes.HCA.value
|
|
203
|
+
entry.NumChannels = codec.chnls
|
|
204
|
+
entry.NumSamples = codec.total_samples
|
|
205
|
+
entry.SamplingRate = codec.sampling_rate
|
|
206
|
+
elif type(codec) == ADXCodec:
|
|
207
|
+
encoded.append(codec.get_encoded())
|
|
208
|
+
tables.append(WaveformTable(WAVEFORM.copy()))
|
|
209
|
+
entry = tables[-1]
|
|
210
|
+
entry.EncodeType = AcbEncodeTypes.ADX.value
|
|
211
|
+
entry.NumChannels = codec.chnls
|
|
212
|
+
entry.NumSamples = codec.total_samples
|
|
213
|
+
entry.SamplingRate = codec.sampling_rate
|
|
214
|
+
elif isinstance(codec, tuple):
|
|
215
|
+
e_type, e_channels, e_samples, e_rate, e_data = codec
|
|
216
|
+
encoded.append(e_data)
|
|
217
|
+
tables.append(WaveformTable(WAVEFORM.copy()))
|
|
218
|
+
entry = tables[-1]
|
|
219
|
+
entry.EncodeType = e_type.value
|
|
220
|
+
entry.NumChannels = e_channels
|
|
221
|
+
entry.NumSamples = e_samples
|
|
222
|
+
entry.SamplingRate = e_rate
|
|
223
|
+
else:
|
|
224
|
+
raise TypeError(f"Unsupported codec type: {type(codec)}")
|
|
225
|
+
tables[-1].MemoryAwbId = i
|
|
226
|
+
awb = self.awb
|
|
227
|
+
self.view.AwbFile = AWBBuilder(encoded, awb.subkey, awb.version, align=awb.align).build()
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def cues(self) -> Generator[CueItem, None, None]:
|
|
232
|
+
"""Returns a generator of **read-only** Cues.
|
|
233
|
+
|
|
234
|
+
Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
|
|
235
|
+
To modify cues, use the `view` property instead.
|
|
236
|
+
"""
|
|
237
|
+
for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
|
|
238
|
+
waveforms = self.view.waveform_of(cue.CueId)
|
|
239
|
+
yield CueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
|
|
86
240
|
|
|
87
241
|
class ACBBuilder:
|
|
88
242
|
acb: ACB
|
|
89
243
|
|
|
90
244
|
def __init__(self, acb: ACB) -> None:
|
|
245
|
+
"""Initializes the ACBBuilder with an existing ACB object.
|
|
246
|
+
|
|
247
|
+
Building ACB from scratch isn't planned for now since:
|
|
248
|
+
* We don't know how SeqCommandTable TLVs work. This is the biggest issue.
|
|
249
|
+
* Many fields are unknown or not well understood
|
|
250
|
+
- Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
|
|
251
|
+
with their own ACF table. Missing these is not a fun debugging experience.
|
|
252
|
+
* ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
|
|
253
|
+
|
|
254
|
+
Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
|
|
255
|
+
"""
|
|
91
256
|
self.acb = acb
|
|
92
257
|
|
|
93
258
|
def build(self) -> bytes:
|
|
@@ -95,6 +260,9 @@ class ACBBuilder:
|
|
|
95
260
|
|
|
96
261
|
The object may be modified in place before building, which will be reflected in the output binary.
|
|
97
262
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
263
|
+
# Check whether all AWB indices are valid
|
|
264
|
+
assert all(
|
|
265
|
+
waveform.MemoryAwbId < self.acb.awb.numfiles for waveform in self.acb.view.WaveformTable
|
|
266
|
+
), "one or more AWB indices are out of range"
|
|
267
|
+
binary = UTFBuilder(self.acb.dictarray, encoding=self.acb.encoding, table_name=self.acb.table_name)
|
|
100
268
|
return binary.bytes()
|
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:
|
|
@@ -79,6 +83,15 @@ class AWB:
|
|
|
79
83
|
|
|
80
84
|
class AWBBuilder:
|
|
81
85
|
def __init__(self, infiles: list[bytes], subkey: int = 0, version: int = 2, id_intsize = 0x2, align: int = 0x20) -> None:
|
|
86
|
+
"""Initializes the AWB builder.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
infiles (list[bytes]): List of bytes to be included in the AWB file.
|
|
90
|
+
subkey (int, optional): AWB subkey. Defaults to 0.
|
|
91
|
+
version (int, optional): AWB version. Defaults to 2.
|
|
92
|
+
id_intsize (hexadecimal, optional): Integer size (in bytes) for string lengths. Defaults to 0x2.
|
|
93
|
+
align (int, optional): Alignment. Defaults to 0x20.
|
|
94
|
+
"""
|
|
82
95
|
if version == 1 and subkey != 0:
|
|
83
96
|
raise ValueError("Cannot have a subkey with AWB version of 1.")
|
|
84
97
|
elif id_intsize not in [0x2, 0x4, 0x8]:
|
|
@@ -89,7 +102,7 @@ class AWBBuilder:
|
|
|
89
102
|
self.subkey = subkey
|
|
90
103
|
self.id_intsize = id_intsize
|
|
91
104
|
|
|
92
|
-
def
|
|
105
|
+
def _stringtypes(self, intsize: int) -> str:
|
|
93
106
|
if intsize == 1:
|
|
94
107
|
return "B" # Probably impossible.
|
|
95
108
|
elif intsize == 2:
|
|
@@ -102,6 +115,7 @@ class AWBBuilder:
|
|
|
102
115
|
raise ValueError("Unknown int size.")
|
|
103
116
|
|
|
104
117
|
def build(self) -> bytes:
|
|
118
|
+
"""Builds the AWB file from the provided infiles bytes."""
|
|
105
119
|
size = 0
|
|
106
120
|
ofs = []
|
|
107
121
|
numfiles = 0
|
|
@@ -122,7 +136,7 @@ class AWBBuilder:
|
|
|
122
136
|
b'AFS2', self.version, intsize, self.id_intsize, numfiles, self.align, self.subkey
|
|
123
137
|
)
|
|
124
138
|
|
|
125
|
-
id_strsize = f"<{self.
|
|
139
|
+
id_strsize = f"<{self._stringtypes(self.id_intsize)}"
|
|
126
140
|
for i in range(numfiles):
|
|
127
141
|
header += pack(id_strsize, i)
|
|
128
142
|
|
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?
|
|
@@ -72,4 +73,25 @@ class CriHcaQuality(Enum):
|
|
|
72
73
|
High = 1
|
|
73
74
|
Middle = 2
|
|
74
75
|
Low = 3
|
|
75
|
-
Lowest = 5
|
|
76
|
+
Lowest = 5
|
|
77
|
+
|
|
78
|
+
class AcbEncodeTypes(Enum):
|
|
79
|
+
ADX = 0
|
|
80
|
+
PCM = 1
|
|
81
|
+
HCA = 2
|
|
82
|
+
ADX_ALT = 3
|
|
83
|
+
WiiDSP = 4
|
|
84
|
+
NDSDSP = 5
|
|
85
|
+
HCAMX = 6
|
|
86
|
+
VAG = 7
|
|
87
|
+
ATRAC3 = 8
|
|
88
|
+
CWAV = 9
|
|
89
|
+
HEVAG = 10
|
|
90
|
+
ATRAC9 = 11
|
|
91
|
+
X360XMA = 12
|
|
92
|
+
DSP = 13
|
|
93
|
+
CWACDSP = 14
|
|
94
|
+
PS4HEVAG = 15
|
|
95
|
+
PS4ATRAC9 = 16
|
|
96
|
+
AACM4A = 17
|
|
97
|
+
SwitchOpus = 18
|
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)
|
|
@@ -55,7 +55,12 @@ class CPK:
|
|
|
55
55
|
stream: BinaryIO
|
|
56
56
|
tables: dict
|
|
57
57
|
filename: str
|
|
58
|
-
def __init__(self, filename) -> None:
|
|
58
|
+
def __init__(self, filename : str | BinaryIO) -> None:
|
|
59
|
+
"""Loads a CPK archive's table-of-content and ready for file reading.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
filename (str | BinaryIO): The path to the CPK file or a BinaryIO stream containing the CPK data.
|
|
63
|
+
"""
|
|
59
64
|
if type(filename) == str:
|
|
60
65
|
self.filename = filename
|
|
61
66
|
self.stream = FileIO(filename)
|
|
@@ -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
|
PyCriCodecsEx/hca.py
CHANGED
|
@@ -46,7 +46,14 @@ class HCA:
|
|
|
46
46
|
table: array
|
|
47
47
|
looping: bool
|
|
48
48
|
|
|
49
|
-
def __init__(self, stream: BinaryIO, key: int = 0, subkey: int = 0) -> None:
|
|
49
|
+
def __init__(self, stream: str | BinaryIO, key: int = 0, subkey: int = 0) -> None:
|
|
50
|
+
"""Initializes the HCA encoder/decoder
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
stream (str | BinaryIO): Path to the HCA or WAV file, or a BinaryIO stream.
|
|
54
|
+
key (int, optional): HCA key. Defaults to 0.
|
|
55
|
+
subkey (int, optional): HCA subkey. Defaults to 0.
|
|
56
|
+
"""
|
|
50
57
|
if type(stream) == str:
|
|
51
58
|
self.stream = FileIO(stream)
|
|
52
59
|
self.hcastream = FileIO(stream)
|
|
@@ -66,10 +73,10 @@ class HCA:
|
|
|
66
73
|
self.hcabytes: bytearray = b''
|
|
67
74
|
self.enc_table: array = b''
|
|
68
75
|
self.table: array = b''
|
|
69
|
-
self.
|
|
76
|
+
self._Pyparse_header()
|
|
70
77
|
|
|
71
78
|
|
|
72
|
-
def
|
|
79
|
+
def _Pyparse_header(self) -> None:
|
|
73
80
|
self.HcaSig, self.version, self.header_size = HcaHeaderStruct.unpack(
|
|
74
81
|
self.hcastream.read(HcaHeaderStruct.size)
|
|
75
82
|
)
|
|
@@ -230,7 +237,7 @@ class HCA:
|
|
|
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,20 +291,20 @@ 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)
|
PyCriCodecsEx/usm.py
CHANGED
|
@@ -332,7 +332,7 @@ class HCACodec(HCA):
|
|
|
332
332
|
|
|
333
333
|
filesize: int
|
|
334
334
|
|
|
335
|
-
def __init__(self, stream: str | bytes, filename: str, quality: CriHcaQuality = CriHcaQuality.High, key=0, subkey=0, **kwargs):
|
|
335
|
+
def __init__(self, stream: str | bytes, filename: str = "default.hca", quality: CriHcaQuality = CriHcaQuality.High, key=0, subkey=0, **kwargs):
|
|
336
336
|
self.filename = filename
|
|
337
337
|
super().__init__(stream, key, subkey)
|
|
338
338
|
if self.filetype == "wav":
|
|
@@ -433,6 +433,13 @@ class HCACodec(HCA):
|
|
|
433
433
|
p.strings = b"<NULL>\x00" + p.strings
|
|
434
434
|
return p.bytes()
|
|
435
435
|
|
|
436
|
+
def get_encoded(self):
|
|
437
|
+
"""Gets the encoded HCA audio data."""
|
|
438
|
+
self.hcastream.seek(0)
|
|
439
|
+
res = self.hcastream.read()
|
|
440
|
+
self.hcastream.seek(0)
|
|
441
|
+
return res
|
|
442
|
+
|
|
436
443
|
def save(self, filepath: str):
|
|
437
444
|
"""Saves the decoded WAV audio to filepath"""
|
|
438
445
|
with open(filepath, "wb") as f:
|
|
@@ -468,7 +475,7 @@ class ADXCodec(ADX):
|
|
|
468
475
|
total_samples: int
|
|
469
476
|
avbps: int
|
|
470
477
|
|
|
471
|
-
def __init__(self, stream: str | bytes, filename: str, bitdepth: int = 4, **kwargs):
|
|
478
|
+
def __init__(self, stream: str | bytes, filename: str = "default.adx", bitdepth: int = 4, **kwargs):
|
|
472
479
|
if type(stream) == str:
|
|
473
480
|
self.adx = open(stream, "rb").read()
|
|
474
481
|
else:
|
|
@@ -481,8 +488,8 @@ class ADXCodec(ADX):
|
|
|
481
488
|
self.sfaStream = BytesIO(self.adx)
|
|
482
489
|
header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
|
|
483
490
|
FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
|
|
484
|
-
assert FourCC == 0x8000, "
|
|
485
|
-
assert self.AdxVersion in {3,4}, "
|
|
491
|
+
assert FourCC == 0x8000, "either ADX or WAV is supported"
|
|
492
|
+
assert self.AdxVersion in {3,4}, "unsupported ADX version"
|
|
486
493
|
if self.AdxVersion == 4:
|
|
487
494
|
self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
|
|
488
495
|
self.sfaStream.seek(0)
|
|
@@ -552,6 +559,10 @@ class ADXCodec(ADX):
|
|
|
552
559
|
def get_metadata(self):
|
|
553
560
|
return None
|
|
554
561
|
|
|
562
|
+
def get_encoded(self):
|
|
563
|
+
"""Gets the encoded ADX audio data."""
|
|
564
|
+
return self.adx
|
|
565
|
+
|
|
555
566
|
def save(self, filepath: str):
|
|
556
567
|
"""Saves the encoded ADX audio to filepath"""
|
|
557
568
|
with open(filepath, "wb") as f:
|
|
@@ -729,7 +740,7 @@ class USM(USMCrypt):
|
|
|
729
740
|
stream.filename = sfname
|
|
730
741
|
return stream
|
|
731
742
|
|
|
732
|
-
def get_audios(self) -> List[HCACodec]:
|
|
743
|
+
def get_audios(self) -> List[ADXCodec | HCACodec]:
|
|
733
744
|
"""Create a list of audio codecs from the available streams."""
|
|
734
745
|
match self.audio_codec:
|
|
735
746
|
case ADXCodec.AUDIO_CODEC:
|
|
@@ -742,113 +753,71 @@ class USM(USMCrypt):
|
|
|
742
753
|
class USMBuilder(USMCrypt):
|
|
743
754
|
"""USM class for building USM files."""
|
|
744
755
|
video_stream: VP9Codec | H264Codec | MPEG1Codec
|
|
745
|
-
|
|
746
|
-
enable_audio: bool
|
|
747
756
|
audio_streams: List[HCACodec | ADXCodec]
|
|
748
757
|
|
|
749
|
-
key: int
|
|
750
|
-
encrypt: bool
|
|
751
|
-
encrypt_audio: bool
|
|
758
|
+
key: int = None
|
|
759
|
+
encrypt: bool = False
|
|
760
|
+
encrypt_audio: bool = False
|
|
752
761
|
|
|
753
|
-
audio_codec: int
|
|
754
|
-
# !!: TODO Quality settings
|
|
755
762
|
def __init__(
|
|
756
763
|
self,
|
|
757
|
-
video: str,
|
|
758
|
-
audio: List[str] | str = None,
|
|
759
764
|
key = None,
|
|
760
|
-
|
|
761
|
-
encrypt_audio: bool = False,
|
|
765
|
+
encrypt_audio = False
|
|
762
766
|
) -> None:
|
|
763
767
|
"""Initialize the USMBuilder from set source files.
|
|
764
768
|
|
|
765
769
|
Args:
|
|
766
|
-
video (str): The path to the video file. The video source format will be used to map accordingly to the ones Sofdec use.
|
|
767
|
-
- MPEG1 (with M1V container): MPEG1 Codec (Sofdec Prime)
|
|
768
|
-
- H264 (with H264 container): H264 Codec
|
|
769
|
-
- VP9 (with IVF container): VP9 Codec
|
|
770
|
-
audio (List[str] | str, optional): The path(s) to the audio file(s). Defaults to None.
|
|
771
770
|
key (str | int, optional): The encryption key. Either int64 or a hex string. Defaults to None.
|
|
772
|
-
|
|
773
|
-
encrypt_audio (bool, optional): Whether to encrypt the audio. Defaults to False.
|
|
771
|
+
encrypt_audio (bool, optional): Whether to also encrypt the audio. Defaults to False.
|
|
774
772
|
"""
|
|
775
|
-
self.audio_codec = audio_codec
|
|
776
|
-
self.encrypt = False
|
|
777
|
-
self.enable_audio = False
|
|
778
|
-
self.encrypt_audio = encrypt_audio
|
|
779
|
-
self.key = 0
|
|
780
|
-
if encrypt_audio and not key:
|
|
781
|
-
raise ValueError("Cannot encrypt Audio without key.")
|
|
782
773
|
if key:
|
|
783
774
|
self.init_key(key)
|
|
784
775
|
self.encrypt = True
|
|
785
|
-
self.
|
|
776
|
+
self.encrypt_audio = encrypt_audio
|
|
786
777
|
self.audio_streams = []
|
|
787
|
-
if audio:
|
|
788
|
-
self.load_audio(audio)
|
|
789
|
-
self.enable_audio = True
|
|
790
|
-
|
|
791
|
-
def load_video(self, video):
|
|
792
|
-
temp_stream = FFmpegCodec(video)
|
|
793
|
-
self.video_stream = None
|
|
794
|
-
match temp_stream.stream["codec_name"]:
|
|
795
|
-
case "h264":
|
|
796
|
-
self.video_stream = H264Codec(video)
|
|
797
|
-
case "vp9":
|
|
798
|
-
self.video_stream = VP9Codec(video)
|
|
799
|
-
case "mpeg1video":
|
|
800
|
-
self.video_stream = MPEG1Codec(video)
|
|
801
|
-
assert self.video_stream, (
|
|
802
|
-
"fail to match suitable video codec. Codec=%s"
|
|
803
|
-
% temp_stream.stream["codec_name"]
|
|
804
|
-
)
|
|
805
778
|
|
|
806
|
-
def
|
|
807
|
-
|
|
808
|
-
if type(audio) == list:
|
|
809
|
-
count = 0
|
|
810
|
-
for track in audio:
|
|
811
|
-
if type(track) == str:
|
|
812
|
-
self.audio_filenames.append(os.path.basename(track))
|
|
813
|
-
else:
|
|
814
|
-
self.audio_filenames.append("{:02d}.sfa".format(count))
|
|
815
|
-
count += 1
|
|
816
|
-
else:
|
|
817
|
-
if type(audio) == str:
|
|
818
|
-
self.audio_filenames.append(os.path.basename(audio))
|
|
819
|
-
else:
|
|
820
|
-
self.audio_filenames.append("00.sfa")
|
|
779
|
+
def add_video(self, video : str | H264Codec | VP9Codec | MPEG1Codec):
|
|
780
|
+
"""Sets the video stream from the specified video file.
|
|
821
781
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
782
|
+
USMs only support one video stream. Consecutive calls to this method will replace the existing video stream.
|
|
783
|
+
|
|
784
|
+
When `video` is str - it will be treated as a file path. The video source format will be used to map accordingly to the ones Sofdec use.
|
|
785
|
+
- MPEG1 (with M1V container): MPEG1 Codec (Sofdec Prime)
|
|
786
|
+
- H264 (with H264 container): H264 Codec
|
|
787
|
+
- VP9 (with IVF container): VP9 Codec
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
video (str | FFmpegCodec): The path to the video file or an FFmpegCodec instance.
|
|
791
|
+
"""
|
|
792
|
+
if isinstance(video, str):
|
|
793
|
+
temp_stream = FFmpegCodec(video)
|
|
794
|
+
self.video_stream = None
|
|
795
|
+
match temp_stream.stream["codec_name"]:
|
|
796
|
+
case "h264":
|
|
797
|
+
self.video_stream = H264Codec(video)
|
|
798
|
+
case "vp9":
|
|
799
|
+
self.video_stream = VP9Codec(video)
|
|
800
|
+
case "mpeg1video":
|
|
801
|
+
self.video_stream = MPEG1Codec(video)
|
|
802
|
+
assert self.video_stream, (
|
|
803
|
+
"fail to match suitable video codec. Codec=%s"
|
|
804
|
+
% temp_stream.stream["codec_name"]
|
|
805
|
+
)
|
|
840
806
|
else:
|
|
841
|
-
|
|
842
|
-
fn = os.path.basename(audio)
|
|
843
|
-
else:
|
|
844
|
-
fn = "00.sfa"
|
|
845
|
-
hcaObj = codec(audio, fn, key=self.key)
|
|
846
|
-
self.audio_streams.append(hcaObj)
|
|
807
|
+
self.video_stream = video
|
|
847
808
|
|
|
809
|
+
def add_audio(self, audio : ADXCodec | HCACodec):
|
|
810
|
+
"""Append the audio stream(s) from the specified audio file(s).
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
audio (ADXCodec | HCACodec): The path(s) to the audio file(s).
|
|
814
|
+
"""
|
|
815
|
+
self.audio_streams.append(audio)
|
|
848
816
|
|
|
849
817
|
def build(self) -> bytes:
|
|
818
|
+
"""Build the USM payload"""
|
|
850
819
|
SFV_list = self.video_stream.generate_SFV(self)
|
|
851
|
-
if self.
|
|
820
|
+
if self.audio_streams:
|
|
852
821
|
SFA_chunks = [s.generate_SFA(i, self) for i, s in enumerate(self.audio_streams) ]
|
|
853
822
|
else:
|
|
854
823
|
SFA_chunks = []
|
|
@@ -928,7 +897,7 @@ class USMBuilder(USMCrypt):
|
|
|
928
897
|
)
|
|
929
898
|
CRIUSF_DIR_STREAM.append(video_dict)
|
|
930
899
|
|
|
931
|
-
if self.
|
|
900
|
+
if self.audio_streams:
|
|
932
901
|
chno = 0
|
|
933
902
|
for stream in self.audio_streams:
|
|
934
903
|
avbps = stream.avbps
|
|
@@ -936,7 +905,7 @@ class USMBuilder(USMCrypt):
|
|
|
936
905
|
minbuf += 27860
|
|
937
906
|
audio_dict = dict(
|
|
938
907
|
fmtver=(UTFTypeValues.uint, 0),
|
|
939
|
-
filename=(UTFTypeValues.string,
|
|
908
|
+
filename=(UTFTypeValues.string, stream.filename),
|
|
940
909
|
filesize=(UTFTypeValues.uint, stream.filesize),
|
|
941
910
|
datasize=(UTFTypeValues.uint, 0),
|
|
942
911
|
stmid=(
|
|
@@ -1018,7 +987,7 @@ class USMBuilder(USMCrypt):
|
|
|
1018
987
|
|
|
1019
988
|
audio_metadata = []
|
|
1020
989
|
audio_headers = []
|
|
1021
|
-
if self.
|
|
990
|
+
if self.audio_streams:
|
|
1022
991
|
chno = 0
|
|
1023
992
|
for stream in self.audio_streams:
|
|
1024
993
|
metadata = stream.get_metadata()
|
|
@@ -1141,7 +1110,7 @@ class USMBuilder(USMCrypt):
|
|
|
1141
1110
|
VIDEO_HDRINFO = gen_video_hdr_info(len(VIDEO_SEEKINFO))
|
|
1142
1111
|
|
|
1143
1112
|
total_len = sum([len(x) for x in SFV_list]) + first_chk_ofs
|
|
1144
|
-
if self.
|
|
1113
|
+
if self.audio_streams:
|
|
1145
1114
|
sum_len = 0
|
|
1146
1115
|
for stream in SFA_chunks:
|
|
1147
1116
|
for x in stream:
|
|
@@ -1181,7 +1150,7 @@ class USMBuilder(USMCrypt):
|
|
|
1181
1150
|
|
|
1182
1151
|
# Header chunks
|
|
1183
1152
|
header += VIDEO_HDRINFO
|
|
1184
|
-
if self.
|
|
1153
|
+
if self.audio_streams:
|
|
1185
1154
|
header += b''.join(audio_headers)
|
|
1186
1155
|
SFV_END = USMChunkHeader.pack(
|
|
1187
1156
|
USMChunckHeaderType.SFV.value,
|
|
@@ -1202,7 +1171,7 @@ class USMBuilder(USMCrypt):
|
|
|
1202
1171
|
header += SFV_END
|
|
1203
1172
|
|
|
1204
1173
|
SFA_chk_END = b'' # Maybe reused
|
|
1205
|
-
if self.
|
|
1174
|
+
if self.audio_streams:
|
|
1206
1175
|
SFA_chk_END = b''.join([
|
|
1207
1176
|
USMChunkHeader.pack(
|
|
1208
1177
|
USMChunckHeaderType.SFA.value,
|
|
@@ -1223,7 +1192,7 @@ class USMBuilder(USMCrypt):
|
|
|
1223
1192
|
header += SFA_chk_END # Ends audio_headers
|
|
1224
1193
|
header += VIDEO_SEEKINFO
|
|
1225
1194
|
|
|
1226
|
-
if self.
|
|
1195
|
+
if self.audio_streams:
|
|
1227
1196
|
header += b''.join(audio_metadata)
|
|
1228
1197
|
SFV_END = USMChunkHeader.pack(
|
|
1229
1198
|
USMChunckHeaderType.SFV.value,
|
PyCriCodecsEx/utf.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from typing import BinaryIO, TypeVar, Type, List
|
|
2
|
+
from copy import deepcopy
|
|
2
3
|
|
|
3
4
|
T = TypeVar("T")
|
|
5
|
+
Ty = TypeVar("Ty", bound="UTFViewer")
|
|
4
6
|
from io import BytesIO, FileIO
|
|
5
7
|
from struct import unpack, calcsize, pack
|
|
6
8
|
from PyCriCodecsEx.chunk import *
|
|
@@ -302,7 +304,7 @@ class UTFBuilder:
|
|
|
302
304
|
|
|
303
305
|
def __init__(
|
|
304
306
|
self,
|
|
305
|
-
|
|
307
|
+
dictarray_src: list[dict],
|
|
306
308
|
encrypt: bool = False,
|
|
307
309
|
encoding: str = "utf-8",
|
|
308
310
|
table_name: str = "PyCriCodecs_table",
|
|
@@ -311,14 +313,14 @@ class UTFBuilder:
|
|
|
311
313
|
"""Packs UTF payload back into their binary form
|
|
312
314
|
|
|
313
315
|
Args:
|
|
314
|
-
|
|
316
|
+
dictarray_src: list[dict]: A list of dictionaries representing the UTF table.
|
|
315
317
|
encrypt: Whether to encrypt the table (default: False).
|
|
316
318
|
encoding: The character encoding to use (default: "utf-8").
|
|
317
319
|
table_name: The name of the table (default: "PyCriCodecs_table").
|
|
318
320
|
ignore_recursion: Whether to ignore recursion when packing (default: False).
|
|
319
321
|
"""
|
|
320
|
-
assert type(
|
|
321
|
-
|
|
322
|
+
assert type(dictarray_src) == list, "dictarray must be a list of dictionaries (see UTF.dictarray)."
|
|
323
|
+
dictarray = deepcopy(dictarray_src)
|
|
322
324
|
# Preprocess for nested dictarray types
|
|
323
325
|
def dfs(payload: list[dict], name: str) -> None:
|
|
324
326
|
for dict in range(len(payload)):
|
|
@@ -618,7 +620,7 @@ class UTFViewer:
|
|
|
618
620
|
See __new__ for the actual constructor.
|
|
619
621
|
```
|
|
620
622
|
"""
|
|
621
|
-
assert isinstance(payload, dict), "
|
|
623
|
+
assert isinstance(payload, dict), "payload must be a dictionary."
|
|
622
624
|
super().__setattr__("_payload", payload)
|
|
623
625
|
|
|
624
626
|
def __getattr__(self, item):
|
|
@@ -643,7 +645,9 @@ class UTFViewer:
|
|
|
643
645
|
def __setattr__(self, item, value):
|
|
644
646
|
payload = super().__getattribute__("_payload")
|
|
645
647
|
if item not in payload:
|
|
646
|
-
raise AttributeError(f"{item} not in payload")
|
|
648
|
+
raise AttributeError(f"{item} not in payload. UTFViewer should not store extra states")
|
|
649
|
+
if isinstance(value, dict) or isinstance(value, list):
|
|
650
|
+
raise AttributeError(f"Dict or list assignment is not allowed as this may potentially change the table layout. Access by elements and use list APIs instead")
|
|
647
651
|
typeof, _ = payload[item]
|
|
648
652
|
payload[item] = (typeof, value)
|
|
649
653
|
|
|
@@ -659,9 +663,9 @@ class UTFViewer:
|
|
|
659
663
|
|
|
660
664
|
class ListView(list):
|
|
661
665
|
_payload : List[dict]
|
|
662
|
-
def __init__(self, payload: list[
|
|
666
|
+
def __init__(self, clazz : Type[Ty], payload: list[Ty]):
|
|
663
667
|
self._payload = payload
|
|
664
|
-
super().__init__([
|
|
668
|
+
super().__init__([clazz(item) for item in payload])
|
|
665
669
|
|
|
666
670
|
def pop(self, index = -1):
|
|
667
671
|
self._payload.pop(index)
|
|
@@ -669,7 +673,7 @@ class UTFViewer:
|
|
|
669
673
|
|
|
670
674
|
def append(self, o : "UTFViewer"):
|
|
671
675
|
if len(self):
|
|
672
|
-
assert type(self[0]) == type(o), "all items in the list must be of the same type."
|
|
676
|
+
assert isinstance(o, UTFViewer) and type(self[0]) == type(o), "all items in the list must be of the same type, and must be an instance of UTFViewer."
|
|
673
677
|
self._payload.append(o._payload)
|
|
674
678
|
return super().append(o)
|
|
675
679
|
|
|
@@ -677,9 +681,9 @@ class UTFViewer:
|
|
|
677
681
|
for item in iterable:
|
|
678
682
|
self.append(item)
|
|
679
683
|
|
|
680
|
-
def insert(self, index, o : "UTFViewer"):
|
|
684
|
+
def insert(self, index, o : "UTFViewer"):
|
|
681
685
|
if len(self):
|
|
682
|
-
assert type(self[0]) == type(o), "all items in the list must be of the same type."
|
|
686
|
+
assert isinstance(o, UTFViewer) and type(self[0]) == type(o), "all items in the list must be of the same type, and must be an instance of UTFViewer."
|
|
683
687
|
self._payload.insert(index, o._payload)
|
|
684
688
|
return super().insert(index, o)
|
|
685
689
|
|
|
@@ -698,7 +702,7 @@ class UTFViewer:
|
|
|
698
702
|
self._payload[:] = [self._payload[i] for x,i in p]
|
|
699
703
|
self[:] = [x for x,i in p]
|
|
700
704
|
|
|
701
|
-
def __new__(cls: Type[
|
|
705
|
+
def __new__(cls: Type[Ty], payload: list | dict, **args) -> Ty | List[Ty]:
|
|
702
706
|
if isinstance(payload, list):
|
|
703
|
-
return UTFViewer.ListView(payload)
|
|
707
|
+
return UTFViewer.ListView(cls, payload)
|
|
704
708
|
return super().__new__(cls)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyCriCodecsEx
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Criware formats library for Python
|
|
5
5
|
Home-page: https://github.com/mos9527/PyCriCodecsEx
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -33,8 +33,13 @@ For USM features, you need `ffmpeg` installed and available in your PATH. See al
|
|
|
33
33
|
## Features
|
|
34
34
|
If not otherwise mentioned, all features marked with [x] are considered working, and has been verified with official tools.
|
|
35
35
|
|
|
36
|
+
Examples are available in [Tests](https://github.com/mos9527/PyCriCodecsEx/tree/main/Tests)
|
|
37
|
+
|
|
36
38
|
### ACB Cue sheets (also AWB)
|
|
37
|
-
- [x]
|
|
39
|
+
- [x] Cue extraction support for most ACBs
|
|
40
|
+
- [x] Cue waveform(s) encoding with ADX/HCA support
|
|
41
|
+
- [x] Comprehensive Cue metadata editing support (via Python API)
|
|
42
|
+
|
|
38
43
|
### USM Sofdec2 (Encode & Decode)
|
|
39
44
|
#### Audio Stream
|
|
40
45
|
For audio to be muxed in, you need a PCM WAV sample with NO metadata, which can be produced with e.g.:
|
|
@@ -63,7 +68,7 @@ Decoding and Encoded format can be the following:
|
|
|
63
68
|
- [x] Packing
|
|
64
69
|
|
|
65
70
|
## Roadmap
|
|
66
|
-
- [
|
|
71
|
+
- [x] ACB Extraction (Massive TODO. see also https://github.com/mos9527/PyCriCodecsEx/blob/main/Research/ACBSchema.py)
|
|
67
72
|
- [ ] Interface for encode tasks (CLI then maybe GUI?)
|
|
68
73
|
- [ ] Documentation
|
|
69
74
|
- [ ] C/C++ port + FFI
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
CriCodecsEx.cp310-win32.pyd,sha256=FvlM7DK0i9zikXRpCtxwU6FEOKdwBgSpjj5_szkGviA,68096
|
|
2
|
+
PyCriCodecsEx/__init__.py,sha256=j7895T8JfsWQAOvxm8SAdESs5iFmIuAqKpyfjSAfKZ4,23
|
|
3
|
+
PyCriCodecsEx/acb.py,sha256=vNPiGW1S0pkdWjcegTWId4C0suUCeX-kfFXUWuRnB-w,10352
|
|
4
|
+
PyCriCodecsEx/adx.py,sha256=KnL4wy1ccoc6uHYZVino7cJQB2pMn4BbRhpH3r527TY,777
|
|
5
|
+
PyCriCodecsEx/awb.py,sha256=sWUo5ITERYG504YVe9ejDYMM-bpLB31Ny4X-7rmmWlQ,6296
|
|
6
|
+
PyCriCodecsEx/chunk.py,sha256=quk9VicOfp-_PgmcjyLL9rCIZQG8DhophXQZWpqfEUw,2925
|
|
7
|
+
PyCriCodecsEx/cpk.py,sha256=sLFUGtG1yuwNC4puyp_TOgkhv1qnRGxVQu4ctlqXeUQ,37932
|
|
8
|
+
PyCriCodecsEx/hca.py,sha256=ArZAaHogejBRE2rWEkTt8lS993Wf4SkjkjhNTo5Anqs,14550
|
|
9
|
+
PyCriCodecsEx/usm.py,sha256=t2V98eno_beVuvU1tbuaFe4p7OohwROiUerMDHhPhEw,45107
|
|
10
|
+
PyCriCodecsEx/utf.py,sha256=YWvgpgLy1tdhVWvRe4pbiJZFxnBRHdtQiR6xEetLsGk,28819
|
|
11
|
+
pycricodecsex-0.0.2.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
|
|
12
|
+
pycricodecsex-0.0.2.dist-info/METADATA,sha256=CAVs16QnoaoIrn8_PJH2w8wM64DTiIXNfxMUvCb-ZN0,3438
|
|
13
|
+
pycricodecsex-0.0.2.dist-info/WHEEL,sha256=GWZF0cboiU4MhsG0baPl8rrtCaXFSLW25384gp3vddM,97
|
|
14
|
+
pycricodecsex-0.0.2.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
|
|
15
|
+
pycricodecsex-0.0.2.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
CriCodecsEx.cp310-win32.pyd,sha256=TiZDF1HJAcXyeCGm2dauxrYWXPT6EQngPA_0A7m7fiE,68096
|
|
2
|
-
PyCriCodecsEx/__init__.py,sha256=ry5O5SW74ugwOUMKSV2_LbWjYa8B0IEvUm7S-lHPA2c,23
|
|
3
|
-
PyCriCodecsEx/acb.py,sha256=v0CxjveQewxUWuila1O0qDGcAfvOxqnzYqAL4AHneqU,2706
|
|
4
|
-
PyCriCodecsEx/adx.py,sha256=KnL4wy1ccoc6uHYZVino7cJQB2pMn4BbRhpH3r527TY,777
|
|
5
|
-
PyCriCodecsEx/awb.py,sha256=p68tVqZOorOE1Js-w1Sqy4GMvu9gxzlBgUL2LAUNEqU,5657
|
|
6
|
-
PyCriCodecsEx/chunk.py,sha256=nrRUANDFZ1LDszHEGDDnQDuNnAB_kGCzbpC7bsgtjqc,2504
|
|
7
|
-
PyCriCodecsEx/cpk.py,sha256=AKShN6P7p70i3GQfKKS-VH3mkbexOLfHEJrjuxS0SQQ,37692
|
|
8
|
-
PyCriCodecsEx/hca.py,sha256=lSslLyv1D1zM_kHFyOlKpbM7qzghmJDYRd-isP8qcyg,14192
|
|
9
|
-
PyCriCodecsEx/usm.py,sha256=X4mv-3ksiX10C6EDACpu-dwRn9eSy3wmY7of2aO7eiA,46216
|
|
10
|
-
PyCriCodecsEx/utf.py,sha256=Yp1Le3AS7UC4So_sGO39N5X5D23X7MsWugHo52npwR0,28256
|
|
11
|
-
pycricodecsex-0.0.1.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
|
|
12
|
-
pycricodecsex-0.0.1.dist-info/METADATA,sha256=Z4KAoITGoFoNhiiJ_YXSnrBE--W51_Q-pXt3lerFMX8,3324
|
|
13
|
-
pycricodecsex-0.0.1.dist-info/WHEEL,sha256=GWZF0cboiU4MhsG0baPl8rrtCaXFSLW25384gp3vddM,97
|
|
14
|
-
pycricodecsex-0.0.1.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
|
|
15
|
-
pycricodecsex-0.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|