PyCriCodecsEx 0.0.3__cp312-cp312-win32.whl → 0.0.5__cp312-cp312-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.
- CriCodecsEx.cp312-win32.pyd +0 -0
- PyCriCodecsEx/__init__.py +1 -1
- PyCriCodecsEx/acb.py +46 -19
- PyCriCodecsEx/adx.py +9 -6
- PyCriCodecsEx/awb.py +2 -3
- PyCriCodecsEx/cpk.py +41 -39
- PyCriCodecsEx/hca.py +9 -6
- PyCriCodecsEx/usm.py +14 -8
- {pycricodecsex-0.0.3.dist-info → pycricodecsex-0.0.5.dist-info}/METADATA +1 -1
- pycricodecsex-0.0.5.dist-info/RECORD +15 -0
- pycricodecsex-0.0.3.dist-info/RECORD +0 -15
- {pycricodecsex-0.0.3.dist-info → pycricodecsex-0.0.5.dist-info}/WHEEL +0 -0
- {pycricodecsex-0.0.3.dist-info → pycricodecsex-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {pycricodecsex-0.0.3.dist-info → pycricodecsex-0.0.5.dist-info}/top_level.txt +0 -0
CriCodecsEx.cp312-win32.pyd
CHANGED
|
Binary file
|
PyCriCodecsEx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.5"
|
PyCriCodecsEx/acb.py
CHANGED
|
@@ -14,12 +14,16 @@ from copy import deepcopy
|
|
|
14
14
|
|
|
15
15
|
class CueNameTable(UTFViewer):
|
|
16
16
|
CueIndex: int
|
|
17
|
+
'''Index into CueTable'''
|
|
17
18
|
CueName: str
|
|
19
|
+
'''Name of the cue'''
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class CueTable(UTFViewer):
|
|
21
23
|
CueId: int
|
|
24
|
+
'''Corresponds to the cue index found in CueNameTable'''
|
|
22
25
|
Length: int
|
|
26
|
+
'''Duration of the cue in milliseconds'''
|
|
23
27
|
ReferenceIndex: int
|
|
24
28
|
ReferenceType: int
|
|
25
29
|
|
|
@@ -52,14 +56,21 @@ class WaveformTable(UTFViewer):
|
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
class ACBTable(UTFViewer):
|
|
59
|
+
'''ACB Table View'''
|
|
60
|
+
|
|
55
61
|
AcbGuid: bytes
|
|
62
|
+
'''GUID of the ACB. This SHOULD be different for each ACB file.'''
|
|
56
63
|
Name: str
|
|
57
|
-
|
|
64
|
+
'''Name of the ACB. This is usually the name of the sound bank.'''
|
|
65
|
+
Version: int
|
|
58
66
|
VersionString: str
|
|
59
67
|
|
|
60
68
|
AwbFile: bytes
|
|
61
69
|
CueNameTable: List[CueNameTable]
|
|
70
|
+
'''A list of cue names with their corresponding indices into CueTable'''
|
|
62
71
|
CueTable: List[CueTable]
|
|
72
|
+
'''A list of cues with their corresponding references'''
|
|
73
|
+
|
|
63
74
|
SequenceTable: List[SequenceTable]
|
|
64
75
|
SynthTable: List[SynthTable]
|
|
65
76
|
TrackEventTable: List[TrackEventTable]
|
|
@@ -67,7 +78,7 @@ class ACBTable(UTFViewer):
|
|
|
67
78
|
WaveformTable: List[WaveformTable]
|
|
68
79
|
|
|
69
80
|
@staticmethod
|
|
70
|
-
def
|
|
81
|
+
def _decode_tlv(data : bytes):
|
|
71
82
|
pos = 0
|
|
72
83
|
while pos < len(data):
|
|
73
84
|
tag = data[pos : pos + 2]
|
|
@@ -76,16 +87,16 @@ class ACBTable(UTFViewer):
|
|
|
76
87
|
pos += 3 + length
|
|
77
88
|
yield (tag, value)
|
|
78
89
|
|
|
79
|
-
def
|
|
80
|
-
tlv = self.
|
|
90
|
+
def _waveform_of_track(self, index: int):
|
|
91
|
+
tlv = self._decode_tlv(self.TrackEventTable[index])
|
|
81
92
|
def noteOn(data: bytes):
|
|
82
93
|
# Handle note on event
|
|
83
94
|
tlv_type, tlv_index = AcbTrackCommandNoteOnStruct.unpack(data[:AcbTrackCommandNoteOnStruct.size])
|
|
84
95
|
match tlv_type:
|
|
85
96
|
case 0x02: # Synth
|
|
86
|
-
yield from self.
|
|
97
|
+
yield from self._waveform_of_synth(tlv_index)
|
|
87
98
|
case 0x03: # Sequence
|
|
88
|
-
yield from self.
|
|
99
|
+
yield from self._waveform_of_sequence(tlv_index)
|
|
89
100
|
# Ignore others silently
|
|
90
101
|
for code, data in tlv:
|
|
91
102
|
match code:
|
|
@@ -94,13 +105,13 @@ class ACBTable(UTFViewer):
|
|
|
94
105
|
case 2003:
|
|
95
106
|
yield from noteOn(data)
|
|
96
107
|
|
|
97
|
-
def
|
|
108
|
+
def _waveform_of_sequence(self, index : int):
|
|
98
109
|
seq = self.SequenceTable[index]
|
|
99
110
|
for i in range(seq.NumTracks):
|
|
100
111
|
track_index = int.from_bytes(seq.TrackIndex[i*2:i*2+2], 'big')
|
|
101
112
|
yield self.WaveformTable[track_index]
|
|
102
113
|
|
|
103
|
-
def
|
|
114
|
+
def _waveform_of_synth(self, index: int):
|
|
104
115
|
item_type, item_index = AcbSynthReferenceStruct.unpack(self.SynthTable[index].ReferenceItems)
|
|
105
116
|
match item_type:
|
|
106
117
|
case 0x00: # No audio
|
|
@@ -108,33 +119,42 @@ class ACBTable(UTFViewer):
|
|
|
108
119
|
case 0x01: # Waveform
|
|
109
120
|
yield self.WaveformTable[item_index]
|
|
110
121
|
case 0x02: # Yet another synth...
|
|
111
|
-
yield from self.
|
|
122
|
+
yield from self._waveform_of_synth(item_index)
|
|
112
123
|
case 0x03: # Sequence
|
|
113
|
-
yield from self.
|
|
124
|
+
yield from self._waveform_of_sequence(item_index)
|
|
114
125
|
case _:
|
|
115
126
|
raise NotImplementedError(f"Unknown synth reference type: {item_type} at index {index}")
|
|
116
127
|
|
|
117
128
|
def waveform_of(self, index : int) -> List["WaveformTable"]:
|
|
129
|
+
"""Retrieves the waveform(s) associated with a cue.
|
|
130
|
+
|
|
131
|
+
Cues may reference multiple waveforms, which could also be reused."""
|
|
118
132
|
cue = next(filter(lambda c: c.CueId == index, self.CueTable), None)
|
|
119
133
|
assert cue, "cue of index %d not found" % index
|
|
120
134
|
match cue.ReferenceType:
|
|
121
135
|
case 0x01:
|
|
122
136
|
return [self.WaveformTable[index]]
|
|
123
137
|
case 0x02:
|
|
124
|
-
return list(self.
|
|
138
|
+
return list(self._waveform_of_synth(index))
|
|
125
139
|
case 0x03:
|
|
126
|
-
return list(self.
|
|
140
|
+
return list(self._waveform_of_sequence(index))
|
|
127
141
|
case 0x08:
|
|
128
142
|
raise NotImplementedError("BlockSequence type not implemented yet")
|
|
129
143
|
case _:
|
|
130
144
|
raise NotImplementedError(f"Unknown cue reference type: {cue.ReferenceType}")
|
|
131
145
|
|
|
132
146
|
@dataclass(frozen=True)
|
|
133
|
-
class
|
|
147
|
+
class PackedCueItem:
|
|
148
|
+
'''Helper class for read-only cue information'''
|
|
149
|
+
|
|
134
150
|
CueId: int
|
|
151
|
+
'''Cue ID'''
|
|
135
152
|
CueName: str
|
|
153
|
+
'''Cue name'''
|
|
136
154
|
Length: float
|
|
137
|
-
|
|
155
|
+
'''Duration in seconds'''
|
|
156
|
+
Waveforms: list[int]
|
|
157
|
+
'''List of waveform IDs, corresponds to ACB.get_waveforms()'''
|
|
138
158
|
|
|
139
159
|
class ACB(UTF):
|
|
140
160
|
"""Use this class to read, and modify ACB files in memory."""
|
|
@@ -168,10 +188,17 @@ class ACB(UTF):
|
|
|
168
188
|
"""Returns the AWB object associated with the ACB."""
|
|
169
189
|
return AWB(self.view.AwbFile)
|
|
170
190
|
|
|
171
|
-
def get_waveforms(self) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
|
|
191
|
+
def get_waveforms(self, **kwargs) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
|
|
172
192
|
"""Returns a list of decoded waveforms.
|
|
173
193
|
|
|
174
194
|
Item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
|
|
195
|
+
|
|
196
|
+
Additional keyword arguments are passed to the codec constructors. e.g. for encrypted HCA payloads,
|
|
197
|
+
you may do the following:
|
|
198
|
+
```python
|
|
199
|
+
get_waveforms(key=..., subkey=...)
|
|
200
|
+
```
|
|
201
|
+
See also the respective docs (ADXCodec, HCACodec) for more details.
|
|
175
202
|
"""
|
|
176
203
|
CODEC_TABLE = {
|
|
177
204
|
AcbEncodeTypes.ADX: ADXCodec,
|
|
@@ -184,7 +211,7 @@ class ACB(UTF):
|
|
|
184
211
|
encode = AcbEncodeTypes(wav.EncodeType)
|
|
185
212
|
codec = (CODEC_TABLE.get(encode, None))
|
|
186
213
|
if codec:
|
|
187
|
-
wavs.append(codec(awb.get_file_at(wav.MemoryAwbId)))
|
|
214
|
+
wavs.append(codec(awb.get_file_at(wav.MemoryAwbId), **kwargs))
|
|
188
215
|
else:
|
|
189
216
|
wavs.append((encode, wav.NumChannels, wav.NumSamples, wav.SamplingRate, awb.get_file_at(wav.MemoryAwbId)))
|
|
190
217
|
return wavs
|
|
@@ -194,7 +221,7 @@ class ACB(UTF):
|
|
|
194
221
|
|
|
195
222
|
Input item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
|
|
196
223
|
|
|
197
|
-
NOTE: Cue duration is not set. You need to change that manually.
|
|
224
|
+
NOTE: Cue duration is not set. You need to change that manually - this is usually unecessary as the player will just play until the end of the waveform.
|
|
198
225
|
"""
|
|
199
226
|
WAVEFORM = self.view.WaveformTable[0]._payload.copy()
|
|
200
227
|
encoded = []
|
|
@@ -234,7 +261,7 @@ class ACB(UTF):
|
|
|
234
261
|
pass
|
|
235
262
|
|
|
236
263
|
@property
|
|
237
|
-
def cues(self) -> Generator[
|
|
264
|
+
def cues(self) -> Generator[PackedCueItem, None, None]:
|
|
238
265
|
"""Returns a generator of **read-only** Cues.
|
|
239
266
|
|
|
240
267
|
Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
|
|
@@ -242,7 +269,7 @@ class ACB(UTF):
|
|
|
242
269
|
"""
|
|
243
270
|
for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
|
|
244
271
|
waveforms = self.view.waveform_of(cue.CueId)
|
|
245
|
-
yield
|
|
272
|
+
yield PackedCueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
|
|
246
273
|
|
|
247
274
|
class ACBBuilder:
|
|
248
275
|
"""Use this class to build ACB files from an existing ACB object."""
|
PyCriCodecsEx/adx.py
CHANGED
|
@@ -57,7 +57,7 @@ class ADXCodec(ADX):
|
|
|
57
57
|
|
|
58
58
|
Args:
|
|
59
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):
|
|
60
|
+
filename (str, optional): Filename, used by USMBuilder. Defaults to "default.adx".
|
|
61
61
|
bitdepth (int, optional): Audio bit depth within [2,15]. Defaults to 4.
|
|
62
62
|
"""
|
|
63
63
|
if type(stream) == str:
|
|
@@ -144,12 +144,15 @@ class ADXCodec(ADX):
|
|
|
144
144
|
def get_metadata(self):
|
|
145
145
|
return None
|
|
146
146
|
|
|
147
|
-
def get_encoded(self):
|
|
147
|
+
def get_encoded(self) -> bytes:
|
|
148
148
|
"""Gets the encoded ADX audio data."""
|
|
149
149
|
return self.adx
|
|
150
150
|
|
|
151
|
-
def save(self, filepath: str):
|
|
152
|
-
"""Saves the
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
def save(self, filepath: str | BinaryIO):
|
|
152
|
+
"""Saves the decoded WAV audio to filepath or a writable stream"""
|
|
153
|
+
if type(filepath) == str:
|
|
154
|
+
with open(filepath, "wb") as f:
|
|
155
|
+
f.write(self.decode(self.adx))
|
|
156
|
+
else:
|
|
157
|
+
filepath.write(self.decode(self.adx))
|
|
155
158
|
|
PyCriCodecsEx/awb.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from io import BytesIO, FileIO
|
|
2
|
-
import
|
|
3
|
-
from typing import BinaryIO
|
|
2
|
+
from typing import BinaryIO, Generator
|
|
4
3
|
from struct import iter_unpack, pack
|
|
5
4
|
from PyCriCodecsEx.chunk import *
|
|
6
5
|
from PyCriCodecsEx.hca import HCA
|
|
@@ -55,7 +54,7 @@ class AWB:
|
|
|
55
54
|
self.headersize = self.headersize + (self.align - (self.headersize % self.align))
|
|
56
55
|
self.stream.seek(self.headersize, 0)
|
|
57
56
|
|
|
58
|
-
def get_files(self):
|
|
57
|
+
def get_files(self) -> Generator[bytes, None, None]:
|
|
59
58
|
"""Generator function to yield all data blobs from an AWB. """
|
|
60
59
|
self.stream.seek(self.headersize, 0)
|
|
61
60
|
for i in range(1, len(self.ofs)):
|
PyCriCodecsEx/cpk.py
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import BinaryIO
|
|
2
|
+
from typing import BinaryIO, Generator
|
|
3
3
|
from io import BytesIO, FileIO
|
|
4
4
|
from PyCriCodecsEx.chunk import *
|
|
5
5
|
from PyCriCodecsEx.utf import UTF, UTFBuilder
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from concurrent.futures import
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
8
|
from tempfile import NamedTemporaryFile
|
|
9
9
|
import CriCodecsEx
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def _crilayla_compress_to_file(src : str, dst: str):
|
|
12
12
|
with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
|
|
13
13
|
data = fsrc.read()
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
try:
|
|
15
|
+
compressed = CriCodecsEx.CriLaylaCompress(data)
|
|
16
|
+
fdst.write(compressed)
|
|
17
|
+
except:
|
|
18
|
+
# Fallback for failed compression
|
|
19
|
+
# Again. FIXME.
|
|
20
|
+
fdst.write(data)
|
|
21
|
+
|
|
16
22
|
@dataclass
|
|
17
|
-
class
|
|
23
|
+
class PackedFile():
|
|
24
|
+
"""Helper class for packed files within a CPK."""
|
|
18
25
|
stream: BinaryIO
|
|
19
26
|
path: str
|
|
20
27
|
offset: int
|
|
@@ -22,6 +29,7 @@ class _PackFile():
|
|
|
22
29
|
compressed : bool = False
|
|
23
30
|
|
|
24
31
|
def get_bytes(self) -> bytes:
|
|
32
|
+
"""Get the raw bytes of the packed file, decompressing if necessary."""
|
|
25
33
|
self.stream.seek(self.offset)
|
|
26
34
|
data = self.stream.read(self.size)
|
|
27
35
|
if self.compressed:
|
|
@@ -29,6 +37,7 @@ class _PackFile():
|
|
|
29
37
|
return data
|
|
30
38
|
|
|
31
39
|
def save(self, path : str):
|
|
40
|
+
"""Save the packed file to a specified path."""
|
|
32
41
|
with open(path, "wb") as f:
|
|
33
42
|
f.write(self.get_bytes())
|
|
34
43
|
class _TOC():
|
|
@@ -115,6 +124,9 @@ class CPK:
|
|
|
115
124
|
|
|
116
125
|
@property
|
|
117
126
|
def mode(self):
|
|
127
|
+
"""Get the current mode of the CPK archive. [0,1,2,3]
|
|
128
|
+
|
|
129
|
+
See also CPKBuilder"""
|
|
118
130
|
TOC, ITOC, GTOC = 'TOC' in self.tables, 'ITOC' in self.tables, 'GTOC' in self.tables
|
|
119
131
|
if TOC and ITOC and GTOC:
|
|
120
132
|
return 3
|
|
@@ -127,8 +139,8 @@ class CPK:
|
|
|
127
139
|
raise ValueError("Unknown CPK mode.")
|
|
128
140
|
|
|
129
141
|
@property
|
|
130
|
-
def files(self):
|
|
131
|
-
"""
|
|
142
|
+
def files(self) -> Generator[PackedFile, None, None]:
|
|
143
|
+
"""Creates a generator for all files in the CPK archive as PackedFile."""
|
|
132
144
|
if "TOC" in self.tables:
|
|
133
145
|
toctable = self.tables['TOC']
|
|
134
146
|
rel_off = 0x800
|
|
@@ -139,10 +151,10 @@ class CPK:
|
|
|
139
151
|
filename = filename[:250] + "_" + str(i) # 250 because i might be 4 digits long.
|
|
140
152
|
if toctable['ExtractSize'][i] > toctable['FileSize'][i]:
|
|
141
153
|
self.stream.seek(rel_off+toctable["FileOffset"][i], 0)
|
|
142
|
-
yield
|
|
154
|
+
yield PackedFile(self.stream, os.path.join(dirname,filename), self.stream.tell(), toctable['FileSize'][i], compressed=True)
|
|
143
155
|
else:
|
|
144
156
|
self.stream.seek(rel_off+toctable["FileOffset"][i], 0)
|
|
145
|
-
yield
|
|
157
|
+
yield PackedFile(self.stream, os.path.join(dirname,filename), self.stream.tell(), toctable['FileSize'][i])
|
|
146
158
|
elif "ITOC" in self.tables:
|
|
147
159
|
toctableL = self.tables["ITOC"]['DataL'][0]
|
|
148
160
|
toctableH = self.tables["ITOC"]['DataH'][0]
|
|
@@ -154,18 +166,18 @@ class CPK:
|
|
|
154
166
|
if i in toctableH['ID']:
|
|
155
167
|
idx = toctableH['ID'].index(i)
|
|
156
168
|
if toctableH['ExtractSize'][idx] > toctableH['FileSize'][idx]:
|
|
157
|
-
yield
|
|
169
|
+
yield PackedFile(self.stream, str(i), self.stream.tell(), toctableH['FileSize'][idx], compressed=True)
|
|
158
170
|
else:
|
|
159
|
-
yield
|
|
171
|
+
yield PackedFile(self.stream, str(i), self.stream.tell(), toctableH['FileSize'][idx])
|
|
160
172
|
if toctableH['FileSize'][idx] % align != 0:
|
|
161
173
|
seek_size = (align - toctableH['FileSize'][idx] % align)
|
|
162
174
|
self.stream.seek(seek_size, 1)
|
|
163
175
|
elif i in toctableL['ID']:
|
|
164
176
|
idx = toctableL['ID'].index(i)
|
|
165
177
|
if toctableL['ExtractSize'][idx] > toctableL['FileSize'][idx]:
|
|
166
|
-
yield
|
|
178
|
+
yield PackedFile(self.stream, str(i), self.stream.tell(), toctableL['FileSize'][idx], compressed=True)
|
|
167
179
|
else:
|
|
168
|
-
yield
|
|
180
|
+
yield PackedFile(self.stream, str(i), self.stream.tell(), toctableL['FileSize'][idx])
|
|
169
181
|
if toctableL['FileSize'][idx] % align != 0:
|
|
170
182
|
seek_size = (align - toctableL['FileSize'][idx] % align)
|
|
171
183
|
self.stream.seek(seek_size, 1)
|
|
@@ -247,9 +259,8 @@ class CPKBuilder:
|
|
|
247
259
|
compress (bool, optional): Whether to compress the file. Defaults to False.
|
|
248
260
|
|
|
249
261
|
NOTE:
|
|
250
|
-
- In ITOC-related mode, the insertion order determines the final integer ID of the files.
|
|
251
|
-
|
|
252
|
-
"""
|
|
262
|
+
- In ITOC-related mode, the insertion order determines the final integer ID of the files.
|
|
263
|
+
"""
|
|
253
264
|
if not dst and self.mode != 0:
|
|
254
265
|
raise ValueError("Destination filename must be specified in non-ITOC mode.")
|
|
255
266
|
|
|
@@ -263,7 +274,7 @@ class CPKBuilder:
|
|
|
263
274
|
self.outfile.write(bytes(0x800 - pack_size % 0x800))
|
|
264
275
|
self.progress_cb("Write %s" % os.path.basename(filename), i + 1, len(self.files))
|
|
265
276
|
|
|
266
|
-
def _populate_files(self,
|
|
277
|
+
def _populate_files(self, threads : int = 1):
|
|
267
278
|
self.files = []
|
|
268
279
|
for src, dst, compress in self.in_files:
|
|
269
280
|
if compress:
|
|
@@ -271,23 +282,15 @@ class CPKBuilder:
|
|
|
271
282
|
self.os_files.append((tmp.name, True))
|
|
272
283
|
else:
|
|
273
284
|
self.os_files.append((src, False))
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
except:
|
|
284
|
-
pass
|
|
285
|
-
self.progress_cb("Compress %s" % os.path.basename(src), i + 1, len(futures))
|
|
286
|
-
else:
|
|
287
|
-
for i, ((src, _, _), (dst, compress)) in enumerate(zip(self.in_files,self.os_files)):
|
|
288
|
-
if compress:
|
|
289
|
-
_worker_do_compression(src, dst)
|
|
290
|
-
self.progress_cb("Compress %s" % os.path.basename(src), i + 1, len(self.in_files))
|
|
285
|
+
with ThreadPoolExecutor(max_workers=threads) as exec:
|
|
286
|
+
futures = []
|
|
287
|
+
for (src, _, _), (dst, compress) in zip(self.in_files,self.os_files):
|
|
288
|
+
if compress:
|
|
289
|
+
_crilayla_compress_to_file(src, dst)
|
|
290
|
+
# futures.append(exec.submit(_crilayla_compress_to_file, src, dst))
|
|
291
|
+
for i, fut in enumerate(as_completed(futures)):
|
|
292
|
+
fut.result()
|
|
293
|
+
self.progress_cb("Compress %s" % os.path.basename(src), i + 1, len(futures))
|
|
291
294
|
for (src, filename, _) , (dst, _) in zip(self.in_files,self.os_files):
|
|
292
295
|
file_size = os.stat(src).st_size
|
|
293
296
|
pack_size = os.stat(dst).st_size
|
|
@@ -304,23 +307,22 @@ class CPKBuilder:
|
|
|
304
307
|
pass
|
|
305
308
|
self.os_files = []
|
|
306
309
|
|
|
307
|
-
def save(self, outfile : str | BinaryIO,
|
|
310
|
+
def save(self, outfile : str | BinaryIO, threads : int = 1):
|
|
308
311
|
"""Build and save the bundle into a file
|
|
309
312
|
|
|
310
313
|
|
|
311
314
|
Args:
|
|
312
315
|
outfile (str | BinaryIO): The output file path or a writable binary stream.
|
|
313
|
-
|
|
316
|
+
threads (int, optional): The number of threads to use for file compression. Defaults to 1.
|
|
314
317
|
|
|
315
318
|
NOTE:
|
|
316
319
|
- Temporary files may be created during the process if compression is used.
|
|
317
|
-
- parallel uses multiprocessing. Make sure your main function is guarded with `if __name__ == '__main__'` clause.
|
|
318
320
|
"""
|
|
319
321
|
assert self.in_files, "cannot save empty bundle"
|
|
320
322
|
self.outfile = outfile
|
|
321
323
|
if type(outfile) == str:
|
|
322
324
|
self.outfile = open(outfile, "wb")
|
|
323
|
-
self._populate_files(
|
|
325
|
+
self._populate_files(threads)
|
|
324
326
|
if self.encrypt:
|
|
325
327
|
encflag = 0
|
|
326
328
|
else:
|
PyCriCodecsEx/hca.py
CHANGED
|
@@ -332,7 +332,7 @@ class HCACodec(HCA):
|
|
|
332
332
|
|
|
333
333
|
Args:
|
|
334
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):
|
|
335
|
+
filename (str, optional): Filename, used by USMBuilder. Defaults to "default.hca".
|
|
336
336
|
quality (CriHcaQuality, optional): Encoding quality. Defaults to CriHcaQuality.High.
|
|
337
337
|
key (int, optional): HCA key. Defaults to 0.
|
|
338
338
|
subkey (int, optional): HCA subkey. Defaults to 0.
|
|
@@ -438,14 +438,17 @@ class HCACodec(HCA):
|
|
|
438
438
|
p.strings = b"<NULL>\x00" + p.strings
|
|
439
439
|
return p.bytes()
|
|
440
440
|
|
|
441
|
-
def get_encoded(self):
|
|
441
|
+
def get_encoded(self) -> bytes:
|
|
442
442
|
"""Gets the encoded HCA audio data."""
|
|
443
443
|
self.hcastream.seek(0)
|
|
444
444
|
res = self.hcastream.read()
|
|
445
445
|
self.hcastream.seek(0)
|
|
446
446
|
return res
|
|
447
447
|
|
|
448
|
-
def save(self, filepath: str):
|
|
449
|
-
"""Saves the decoded WAV audio to filepath"""
|
|
450
|
-
|
|
451
|
-
|
|
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())
|
PyCriCodecsEx/usm.py
CHANGED
|
@@ -13,11 +13,7 @@ except ImportError:
|
|
|
13
13
|
import tempfile
|
|
14
14
|
|
|
15
15
|
# Big thanks and credit for k0lb3 and 9th helping me write this specific code.
|
|
16
|
-
# Also credit for the original C++ code from Nyagamon/bnnm.
|
|
17
|
-
|
|
18
|
-
# Apparently there is an older USM format called SofDec? This is for SofDec2 though.
|
|
19
|
-
# Extraction working only for now, although check https://github.com/donmai-me/WannaCRI/
|
|
20
|
-
# code for a complete breakdown of the USM format.
|
|
16
|
+
# Also credit for the original C++ code from Nyagamon/bnnm and https://github.com/donmai-me/WannaCRI/
|
|
21
17
|
|
|
22
18
|
class USMCrypt:
|
|
23
19
|
"""USM related crypto functions"""
|
|
@@ -150,6 +146,7 @@ class USMCrypt:
|
|
|
150
146
|
# are still unknown how to derive them, at least video wise it is possible, no idea how it's calculated audio wise nor anything else
|
|
151
147
|
# seems like it could be random values and the USM would still work.
|
|
152
148
|
class FFmpegCodec:
|
|
149
|
+
"""Base codec for FFMpeg-based Video streams"""
|
|
153
150
|
filename: str
|
|
154
151
|
filesize: int
|
|
155
152
|
|
|
@@ -232,7 +229,7 @@ class FFmpegCodec:
|
|
|
232
229
|
return len(self.packets)
|
|
233
230
|
|
|
234
231
|
def frames(self):
|
|
235
|
-
"""frame data, frame dict, is keyframe, duration"""
|
|
232
|
+
"""Generator of [frame data, frame dict, is keyframe, duration]"""
|
|
236
233
|
offsets = [int(packet["pos"]) for packet in self.packets] + [self.filesize]
|
|
237
234
|
for i, frame in enumerate(self.packets):
|
|
238
235
|
frame_size = offsets[i + 1] - offsets[i]
|
|
@@ -290,13 +287,16 @@ class FFmpegCodec:
|
|
|
290
287
|
return SFV_list
|
|
291
288
|
|
|
292
289
|
def save(self, filepath: str):
|
|
293
|
-
'''Saves the
|
|
290
|
+
'''Saves the underlying video stream to a file.'''
|
|
294
291
|
tell = self.file.tell()
|
|
295
292
|
self.file.seek(0)
|
|
296
293
|
shutil.copyfileobj(self.file, open(filepath, 'wb'))
|
|
297
294
|
self.file.seek(tell)
|
|
298
295
|
|
|
299
296
|
class VP9Codec(FFmpegCodec):
|
|
297
|
+
"""VP9 Video stream codec.
|
|
298
|
+
|
|
299
|
+
Only streams with `.ivf` containers are supported."""
|
|
300
300
|
MPEG_CODEC = 9
|
|
301
301
|
MPEG_DCPREC = 0
|
|
302
302
|
VERSION = 16777984
|
|
@@ -305,6 +305,9 @@ class VP9Codec(FFmpegCodec):
|
|
|
305
305
|
super().__init__(filename)
|
|
306
306
|
assert self.format == "ivf", "must be ivf format."
|
|
307
307
|
class H264Codec(FFmpegCodec):
|
|
308
|
+
"""H264 Video stream codec.
|
|
309
|
+
|
|
310
|
+
Only streams with `.h264` containers are supported."""
|
|
308
311
|
MPEG_CODEC = 5
|
|
309
312
|
MPEG_DCPREC = 11
|
|
310
313
|
VERSION = 0
|
|
@@ -315,6 +318,9 @@ class H264Codec(FFmpegCodec):
|
|
|
315
318
|
self.format == "h264"
|
|
316
319
|
), "must be raw h264 data. transcode with '.h264' suffix as output"
|
|
317
320
|
class MPEG1Codec(FFmpegCodec):
|
|
321
|
+
"""MPEG1 Video stream codec.
|
|
322
|
+
|
|
323
|
+
Only streams with `.mpeg1` containers are supported."""
|
|
318
324
|
MPEG_CODEC = 1
|
|
319
325
|
MPEG_DCPREC = 11
|
|
320
326
|
VERSION = 0
|
|
@@ -482,7 +488,7 @@ class USM(USMCrypt):
|
|
|
482
488
|
stmid = int.to_bytes(stmid, 4, 'big', signed='False')
|
|
483
489
|
yield stmid, str(filename), self.output.get(f'{stmid.decode()}_{chno}', None)
|
|
484
490
|
|
|
485
|
-
def get_video(self):
|
|
491
|
+
def get_video(self) -> VP9Codec | H264Codec | MPEG1Codec:
|
|
486
492
|
"""Create a video codec from the available streams.
|
|
487
493
|
|
|
488
494
|
NOTE: A temporary file may be created with this process to determine the stream information."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
CriCodecsEx.cp312-win32.pyd,sha256=TmLC8PrEbJC24pGWvKOnq2CCFaFAiqGZTBo2Zwin5fk,68608
|
|
2
|
+
PyCriCodecsEx/__init__.py,sha256=vucW-Fe97d2P_nl7TMRJ2Cp458lb7VTRk6EckM_ozdM,23
|
|
3
|
+
PyCriCodecsEx/acb.py,sha256=_YpxIsdWW9vgz0KefKkb8zFnc2WKUwSTDqtgYaPDKp0,11956
|
|
4
|
+
PyCriCodecsEx/adx.py,sha256=LHr1YjVv6W0yzm1ZIGw54bM7rDW-Qozci0_GS8J4CrE,6130
|
|
5
|
+
PyCriCodecsEx/awb.py,sha256=SCTkKvn855bpr1I8Kq24P1es89I2unJaEGgDw7hg1Do,6395
|
|
6
|
+
PyCriCodecsEx/chunk.py,sha256=xmc92tONmXpdbOW2g-KckTeppTXFo8Km0J95UYbbvQg,2810
|
|
7
|
+
PyCriCodecsEx/cpk.py,sha256=er_eBLMG8eiXUvYp0h_lDWLN8Vx8CCDJ8COE9B5Rv_A,38125
|
|
8
|
+
PyCriCodecsEx/hca.py,sha256=TokIvd8IENZXxBw_3Na_o6vAoKvC8m_ooWIQ4qhmUqk,19603
|
|
9
|
+
PyCriCodecsEx/usm.py,sha256=Z6ETFy98Gl0KFot9zRE7Qg8_Dv9wsFUFybZgFvVXT_U,37173
|
|
10
|
+
PyCriCodecsEx/utf.py,sha256=0LBzpIbJiyL_4KOf_QpEJytr3PbHj8_M4Bd-Lay4wuk,28247
|
|
11
|
+
pycricodecsex-0.0.5.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
|
|
12
|
+
pycricodecsex-0.0.5.dist-info/METADATA,sha256=h3K2wCbl1h3WyL5-7AU6dFeW9hIxKcR3_FlFnf7KJWc,1291
|
|
13
|
+
pycricodecsex-0.0.5.dist-info/WHEEL,sha256=LwxTQZ0gyDP_uaeNCLm-ZIktY9hv6x0e22Q-hgFd-po,97
|
|
14
|
+
pycricodecsex-0.0.5.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
|
|
15
|
+
pycricodecsex-0.0.5.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
CriCodecsEx.cp312-win32.pyd,sha256=BnYjmDP-c1ZievQ_Vu33S3jPWaxiURwTJkI0OFg7bXg,68096
|
|
2
|
-
PyCriCodecsEx/__init__.py,sha256=4dCrvStS1OgE3Vlcv-qx5-TAo377yOMEToEqCi-PwY4,23
|
|
3
|
-
PyCriCodecsEx/acb.py,sha256=nOWggyQTPOBArzRp_7VRO9TY1diLxr2mRPKQd-sd2s0,10723
|
|
4
|
-
PyCriCodecsEx/adx.py,sha256=s3gJBgKrwWjUahrxSJEy1GNHJrdz3iaylgr0TyTIi7M,5967
|
|
5
|
-
PyCriCodecsEx/awb.py,sha256=t6owuEOH3Be31iBuEMwriOh0cPdVtpzsNbTPfLwg_cc,6363
|
|
6
|
-
PyCriCodecsEx/chunk.py,sha256=xmc92tONmXpdbOW2g-KckTeppTXFo8Km0J95UYbbvQg,2810
|
|
7
|
-
PyCriCodecsEx/cpk.py,sha256=bMJgvzPInM95R9dEdhHGW65jR9tFBKUacyOwZS2gOyA,38206
|
|
8
|
-
PyCriCodecsEx/hca.py,sha256=BamujR3tRxMmgnPAYpUOamzOUOBs5hXWhkcBnj7Eeh4,19445
|
|
9
|
-
PyCriCodecsEx/usm.py,sha256=LWSXXkfV1FZpymV0X6UH-lyS5gV6ecBHlLLXXXN9wMc,36972
|
|
10
|
-
PyCriCodecsEx/utf.py,sha256=0LBzpIbJiyL_4KOf_QpEJytr3PbHj8_M4Bd-Lay4wuk,28247
|
|
11
|
-
pycricodecsex-0.0.3.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
|
|
12
|
-
pycricodecsex-0.0.3.dist-info/METADATA,sha256=buywcqSll1E63mnWNtEgwHiyAY8NbvMyP8CDLXAJLH0,1291
|
|
13
|
-
pycricodecsex-0.0.3.dist-info/WHEEL,sha256=LwxTQZ0gyDP_uaeNCLm-ZIktY9hv6x0e22Q-hgFd-po,97
|
|
14
|
-
pycricodecsex-0.0.3.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
|
|
15
|
-
pycricodecsex-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|