PyCriCodecsEx 0.0.3__tar.gz → 0.0.4__tar.gz
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-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/adx.cpp +2 -2
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/crilayla.cpp +96 -64
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/hca.cpp +2 -2
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PKG-INFO +1 -1
- pycricodecsex-0.0.4/PyCriCodecsEx/__init__.py +1 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/acb.py +36 -16
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/adx.py +8 -5
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/awb.py +2 -3
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/cpk.py +41 -39
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/hca.py +8 -5
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/usm.py +13 -3
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx.egg-info/PKG-INFO +1 -1
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/setup.py +1 -1
- pycricodecsex-0.0.3/PyCriCodecsEx/__init__.py +0 -1
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/CriCodecsEx.cpp +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/IO.cpp +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/IO.hpp +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/hca.h +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/CriCodecsEx/pcm.cpp +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/LICENSE +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/chunk.py +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx/utf.py +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx.egg-info/SOURCES.txt +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx.egg-info/dependency_links.txt +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx.egg-info/requires.txt +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/PyCriCodecsEx.egg-info/top_level.txt +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/README.md +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/pyproject.toml +0 -0
- {pycricodecsex-0.0.3 → pycricodecsex-0.0.4}/setup.cfg +0 -0
|
@@ -542,7 +542,7 @@ static PyObject* AdxEncode(PyObject* self, PyObject* args){
|
|
|
542
542
|
PyAdxSetError(AdxErrorCode);
|
|
543
543
|
return NULL;
|
|
544
544
|
}
|
|
545
|
-
|
|
545
|
+
Py_ssize_t len = adx.size;
|
|
546
546
|
return Py_BuildValue("y#", adxdata, len);
|
|
547
547
|
}
|
|
548
548
|
|
|
@@ -557,6 +557,6 @@ static PyObject* AdxDecode(PyObject* self, PyObject* args){
|
|
|
557
557
|
return NULL;
|
|
558
558
|
}
|
|
559
559
|
unsigned char *out = wav.WAVEBuffer;
|
|
560
|
-
|
|
560
|
+
Py_ssize_t len = wav.wav.size+8;
|
|
561
561
|
return Py_BuildValue("y#", out, len);
|
|
562
562
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
/*
|
|
2
|
-
CRI layla decompression.
|
|
3
|
-
written by tpu. (https://forum.xentax.com/viewtopic.php?f=21&t=5137&p=44220&hilit=CRILAYLA#p44220)
|
|
4
|
-
Python wrapper by me (and modification).
|
|
1
|
+
/* CRILAYLA Encoder/Decoder
|
|
5
2
|
|
|
6
|
-
CRIcompress method by KenTse
|
|
7
|
-
Taken from wmltogether's fork of CriPakTools.
|
|
8
|
-
Python wrapper by me.
|
|
9
3
|
|
|
10
|
-
|
|
4
|
+
CRI layla decompression.
|
|
5
|
+
written by tpu. (https://forum.xentax.com/viewtopic.php?f=21&t=5137&p=44220&hilit=CRILAYLA#p44220)
|
|
6
|
+
Python wrapper by https://github.com/Youjose/PyCriCodecs (and modification).
|
|
7
|
+
|
|
8
|
+
CRIcompress method by KenTse
|
|
9
|
+
Taken from wmltogether's fork of CriPakTools.
|
|
10
|
+
Python wrapper by https://github.com/Youjose/PyCriCodecs.
|
|
11
|
+
TODO: This implementation may produce larger output - which shouldn't be
|
|
12
|
+
possible with LZ-based compression. Investigate and fix.
|
|
13
|
+
For now, if compression fails, the original data is returned.
|
|
14
|
+
See also:
|
|
15
|
+
- https://github.com/FanTranslatorsInternational/Kuriimu2/blob/imgui/src/lib/Kompression/Encoder/CrilaylaEncoder.cs
|
|
16
|
+
- https://glinscott.github.io/lz/index.html
|
|
11
17
|
*/
|
|
12
18
|
#define PY_SSIZE_T_CLEAN
|
|
13
19
|
#pragma once
|
|
@@ -22,32 +28,30 @@ struct crilayla_header{
|
|
|
22
28
|
unsigned int compressed_size;
|
|
23
29
|
};
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
unsigned char *sbuf;
|
|
27
|
-
unsigned int bitcnt;
|
|
28
|
-
unsigned int bitdat;
|
|
29
|
-
unsigned int get_bits(unsigned int n){
|
|
30
|
-
unsigned int data, mask;
|
|
31
|
-
|
|
32
|
-
if (bitcnt<n){
|
|
33
|
-
data = ((24-bitcnt)>>3)+1;
|
|
34
|
-
bitcnt += data*8;
|
|
35
|
-
while(data) {
|
|
36
|
-
bitdat = (bitdat<<8) | (*sbuf--);
|
|
37
|
-
data--;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
data = bitdat>>(bitcnt-n);
|
|
42
|
-
bitcnt -= n;
|
|
43
|
-
mask = (1<<n)-1;
|
|
44
|
-
data &= mask;
|
|
45
|
-
return data;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
unsigned int llcp_dec(unsigned char *src, unsigned int src_len, unsigned char *dst, unsigned int dst_len){
|
|
49
|
-
|
|
32
|
+
unsigned char *dbuf, *pbuf;
|
|
50
33
|
unsigned int plen, poffset, byte;
|
|
34
|
+
unsigned char *sbuf;
|
|
35
|
+
unsigned int bitcnt;
|
|
36
|
+
unsigned int bitdat;
|
|
37
|
+
auto get_bits = [&](unsigned int n){
|
|
38
|
+
unsigned int data, mask;
|
|
39
|
+
|
|
40
|
+
if (bitcnt<n){
|
|
41
|
+
data = ((24-bitcnt)>>3)+1;
|
|
42
|
+
bitcnt += data*8;
|
|
43
|
+
while(data) {
|
|
44
|
+
bitdat = (bitdat<<8) | (*sbuf--);
|
|
45
|
+
data--;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
data = bitdat>>(bitcnt-n);
|
|
50
|
+
bitcnt -= n;
|
|
51
|
+
mask = (1<<n)-1;
|
|
52
|
+
data &= mask;
|
|
53
|
+
return data;
|
|
54
|
+
};
|
|
51
55
|
|
|
52
56
|
sbuf = src+src_len-1;
|
|
53
57
|
dbuf = dst+dst_len-1;
|
|
@@ -108,40 +112,41 @@ unsigned char* layla_decomp(unsigned char* data, crilayla_header header){
|
|
|
108
112
|
return dst;
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
unsigned int layla_comp(unsigned char*
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
unsigned int layla_comp(unsigned char *dest, int *destLen, unsigned char *src, int srcLen)
|
|
116
|
+
{
|
|
117
|
+
int n = srcLen - 1, m = *destLen - 0x1, T = 0, d = 0, p, q, i, j, k;
|
|
118
|
+
unsigned char *odest = dest;
|
|
114
119
|
for (; n >= 0x100;)
|
|
115
120
|
{
|
|
116
121
|
j = n + 3 + 0x2000;
|
|
117
|
-
if (j
|
|
118
|
-
for (i = n + 3, p = 0; i
|
|
122
|
+
if (j>srcLen) j = srcLen;
|
|
123
|
+
for (i = n + 3, p = 0; i<j; i++)
|
|
119
124
|
{
|
|
120
125
|
for (k = 0; k <= n - 0x100; k++)
|
|
121
126
|
{
|
|
122
127
|
if (*(src + n - k) != *(src + i - k)) break;
|
|
123
128
|
}
|
|
124
|
-
if (k
|
|
129
|
+
if (k>p)
|
|
125
130
|
{
|
|
126
131
|
q = i - n - 3; p = k;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
|
-
if (p
|
|
134
|
+
if (p<3)
|
|
130
135
|
{
|
|
131
136
|
d = (d << 9) | (*(src + n--)); T += 9;
|
|
132
137
|
}
|
|
133
138
|
else
|
|
134
139
|
{
|
|
135
140
|
d = (((d << 1) | 1) << 13) | q; T += 14; n -= p;
|
|
136
|
-
if (p
|
|
141
|
+
if (p<6)
|
|
137
142
|
{
|
|
138
143
|
d = (d << 2) | (p - 3); T += 2;
|
|
139
144
|
}
|
|
140
|
-
else if (p
|
|
145
|
+
else if (p<13)
|
|
141
146
|
{
|
|
142
147
|
d = (((d << 2) | 3) << 3) | (p - 6); T += 5;
|
|
143
148
|
}
|
|
144
|
-
else if (p
|
|
149
|
+
else if (p<44)
|
|
145
150
|
{
|
|
146
151
|
d = (((d << 5) | 0x1f) << 5) | (p - 13); T += 10;
|
|
147
152
|
}
|
|
@@ -150,45 +155,50 @@ unsigned int layla_comp(unsigned char* dest, unsigned int* destLen, unsigned cha
|
|
|
150
155
|
d = ((d << 10) | 0x3ff); T += 10; p -= 44;
|
|
151
156
|
for (;;)
|
|
152
157
|
{
|
|
153
|
-
for (; T >= 8;)
|
|
158
|
+
for (; m > 0 && T >= 8;)
|
|
154
159
|
{
|
|
155
|
-
*(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d
|
|
160
|
+
*(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d&((1 << T) - 1);
|
|
156
161
|
}
|
|
157
|
-
if (p
|
|
162
|
+
if (p<255) break;
|
|
158
163
|
d = (d << 8) | 0xff; T += 8; p = p - 0xff;
|
|
159
164
|
}
|
|
160
165
|
d = (d << 8) | p; T += 8;
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
|
-
for (; T >= 8;)
|
|
168
|
+
for (; m > 0 && T >= 8;)
|
|
164
169
|
{
|
|
165
|
-
*(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d
|
|
170
|
+
*(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d&((1 << T) - 1);
|
|
166
171
|
}
|
|
167
172
|
}
|
|
168
|
-
if (T != 0)
|
|
173
|
+
if (m > 0 && T != 0)
|
|
169
174
|
{
|
|
170
175
|
*(dest + m--) = d << (8 - T);
|
|
171
176
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
if (m > 0) {
|
|
178
|
+
*(dest + m--) = 0; *(dest + m) = 0;
|
|
179
|
+
for (;;)
|
|
180
|
+
{
|
|
181
|
+
if (((*destLen - m) & 3) == 0) break;
|
|
182
|
+
*(dest + m--) = 0;
|
|
183
|
+
}
|
|
177
184
|
}
|
|
185
|
+
if (m <= 0)
|
|
186
|
+
return 0; // Underflow
|
|
178
187
|
*destLen = *destLen - m; dest += m;
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
// CRIL AYLA srcLen-0x100 destLen
|
|
189
|
+
int l[] = { 0x4c495243,0x414c5941,srcLen - 0x100,*destLen };
|
|
190
|
+
for (j = 0; j<4; j++)
|
|
181
191
|
{
|
|
182
|
-
for (i = 0; i
|
|
192
|
+
for (i = 0; i<4; i++)
|
|
183
193
|
{
|
|
184
194
|
*(odest + i + j * 4) = l[j] & 0xff; l[j] >>= 8;
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
|
-
for (j = 0, odest += 0x10; j
|
|
197
|
+
for (j = 0, odest += 0x10; j<*destLen; j++)
|
|
188
198
|
{
|
|
189
199
|
*(odest++) = *(dest + j);
|
|
190
200
|
}
|
|
191
|
-
for (j = 0; j
|
|
201
|
+
for (j = 0; j<0x100; j++)
|
|
192
202
|
{
|
|
193
203
|
*(odest++) = *(src + j);
|
|
194
204
|
}
|
|
@@ -199,7 +209,17 @@ unsigned int layla_comp(unsigned char* dest, unsigned int* destLen, unsigned cha
|
|
|
199
209
|
PyObject* CriLaylaDecompress(PyObject* self, PyObject* d){
|
|
200
210
|
unsigned char *data = (unsigned char *)PyBytes_AsString(d);
|
|
201
211
|
crilayla_header header = *(crilayla_header*)data;
|
|
202
|
-
|
|
212
|
+
|
|
213
|
+
if (header.crilayla != 0x414c59434152494cULL) {
|
|
214
|
+
PyErr_SetString(PyExc_ValueError, "Invalid CRILAYLA header.");
|
|
215
|
+
return NULL;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
unsigned char *out;
|
|
219
|
+
Py_BEGIN_ALLOW_THREADS
|
|
220
|
+
out = layla_decomp((data+16), header);
|
|
221
|
+
Py_END_ALLOW_THREADS
|
|
222
|
+
|
|
203
223
|
PyObject *outObj = Py_BuildValue("y#", out, header.decompress_size+256);
|
|
204
224
|
delete[] out;
|
|
205
225
|
return outObj;
|
|
@@ -213,14 +233,26 @@ unsigned char* CriLaylaDecompress(unsigned char* d){
|
|
|
213
233
|
|
|
214
234
|
PyObject* CriLaylaCompress(PyObject* self, PyObject* args){
|
|
215
235
|
unsigned char *data;
|
|
216
|
-
|
|
236
|
+
Py_ssize_t data_size;
|
|
217
237
|
if(!PyArg_ParseTuple(args, "y#", &data, &data_size)){
|
|
218
238
|
return NULL;
|
|
219
239
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
240
|
+
|
|
241
|
+
unsigned char *buf = new unsigned char[data_size + 0x200];
|
|
242
|
+
|
|
243
|
+
int compressed_size = data_size;
|
|
244
|
+
int res = 0;
|
|
245
|
+
Py_BEGIN_ALLOW_THREADS
|
|
246
|
+
res = layla_comp(buf, &compressed_size, data, compressed_size);
|
|
247
|
+
Py_END_ALLOW_THREADS
|
|
248
|
+
PyObject* bufObj;
|
|
249
|
+
if (res && res < data_size) {
|
|
250
|
+
bufObj = Py_BuildValue("y#", buf, compressed_size);
|
|
251
|
+
} else {
|
|
252
|
+
// FIXME
|
|
253
|
+
PyErr_SetString(PyExc_RuntimeError, "Compression failure.");
|
|
254
|
+
return NULL;
|
|
255
|
+
}
|
|
224
256
|
delete[] buf;
|
|
225
257
|
return bufObj;
|
|
226
258
|
}
|
|
@@ -3296,7 +3296,7 @@ static PyObject* HcaCrypt(PyObject* self, PyObject* args){
|
|
|
3296
3296
|
}
|
|
3297
3297
|
|
|
3298
3298
|
unsigned char *buffer = (unsigned char *)view.buf;
|
|
3299
|
-
|
|
3299
|
+
Py_ssize_t length = view.len;
|
|
3300
3300
|
PyBuffer_Release(&view);
|
|
3301
3301
|
|
|
3302
3302
|
clHCA* hca = (clHCA*)malloc(sizeof(clHCA));
|
|
@@ -3345,7 +3345,7 @@ static PyObject* HcaDecode(PyObject* self, PyObject* args){
|
|
|
3345
3345
|
data *wavData = &wav.wav.chunks.WAVEdata;
|
|
3346
3346
|
|
|
3347
3347
|
unsigned char *data;
|
|
3348
|
-
|
|
3348
|
+
Py_ssize_t data_size;
|
|
3349
3349
|
unsigned int header_size;
|
|
3350
3350
|
unsigned long long keycode;
|
|
3351
3351
|
unsigned short subkey;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.4"
|
|
@@ -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."""
|
|
@@ -234,7 +254,7 @@ class ACB(UTF):
|
|
|
234
254
|
pass
|
|
235
255
|
|
|
236
256
|
@property
|
|
237
|
-
def cues(self) -> Generator[
|
|
257
|
+
def cues(self) -> Generator[PackedCueItem, None, None]:
|
|
238
258
|
"""Returns a generator of **read-only** Cues.
|
|
239
259
|
|
|
240
260
|
Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
|
|
@@ -242,7 +262,7 @@ class ACB(UTF):
|
|
|
242
262
|
"""
|
|
243
263
|
for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
|
|
244
264
|
waveforms = self.view.waveform_of(cue.CueId)
|
|
245
|
-
yield
|
|
265
|
+
yield PackedCueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
|
|
246
266
|
|
|
247
267
|
class ACBBuilder:
|
|
248
268
|
"""Use this class to build ACB files from an existing ACB object."""
|
|
@@ -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
|
|
|
@@ -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)):
|
|
@@ -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:
|
|
@@ -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())
|
|
@@ -150,6 +150,7 @@ class USMCrypt:
|
|
|
150
150
|
# 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
151
|
# seems like it could be random values and the USM would still work.
|
|
152
152
|
class FFmpegCodec:
|
|
153
|
+
"""Base codec for FFMpeg-based Video streams"""
|
|
153
154
|
filename: str
|
|
154
155
|
filesize: int
|
|
155
156
|
|
|
@@ -232,7 +233,7 @@ class FFmpegCodec:
|
|
|
232
233
|
return len(self.packets)
|
|
233
234
|
|
|
234
235
|
def frames(self):
|
|
235
|
-
"""frame data, frame dict, is keyframe, duration"""
|
|
236
|
+
"""Generator of [frame data, frame dict, is keyframe, duration]"""
|
|
236
237
|
offsets = [int(packet["pos"]) for packet in self.packets] + [self.filesize]
|
|
237
238
|
for i, frame in enumerate(self.packets):
|
|
238
239
|
frame_size = offsets[i + 1] - offsets[i]
|
|
@@ -290,13 +291,16 @@ class FFmpegCodec:
|
|
|
290
291
|
return SFV_list
|
|
291
292
|
|
|
292
293
|
def save(self, filepath: str):
|
|
293
|
-
'''Saves the
|
|
294
|
+
'''Saves the underlying video stream to a file.'''
|
|
294
295
|
tell = self.file.tell()
|
|
295
296
|
self.file.seek(0)
|
|
296
297
|
shutil.copyfileobj(self.file, open(filepath, 'wb'))
|
|
297
298
|
self.file.seek(tell)
|
|
298
299
|
|
|
299
300
|
class VP9Codec(FFmpegCodec):
|
|
301
|
+
"""VP9 Video stream codec.
|
|
302
|
+
|
|
303
|
+
Only streams with `.ivf` containers are supported."""
|
|
300
304
|
MPEG_CODEC = 9
|
|
301
305
|
MPEG_DCPREC = 0
|
|
302
306
|
VERSION = 16777984
|
|
@@ -305,6 +309,9 @@ class VP9Codec(FFmpegCodec):
|
|
|
305
309
|
super().__init__(filename)
|
|
306
310
|
assert self.format == "ivf", "must be ivf format."
|
|
307
311
|
class H264Codec(FFmpegCodec):
|
|
312
|
+
"""H264 Video stream codec.
|
|
313
|
+
|
|
314
|
+
Only streams with `.h264` containers are supported."""
|
|
308
315
|
MPEG_CODEC = 5
|
|
309
316
|
MPEG_DCPREC = 11
|
|
310
317
|
VERSION = 0
|
|
@@ -315,6 +322,9 @@ class H264Codec(FFmpegCodec):
|
|
|
315
322
|
self.format == "h264"
|
|
316
323
|
), "must be raw h264 data. transcode with '.h264' suffix as output"
|
|
317
324
|
class MPEG1Codec(FFmpegCodec):
|
|
325
|
+
"""MPEG1 Video stream codec.
|
|
326
|
+
|
|
327
|
+
Only streams with `.mpeg1` containers are supported."""
|
|
318
328
|
MPEG_CODEC = 1
|
|
319
329
|
MPEG_DCPREC = 11
|
|
320
330
|
VERSION = 0
|
|
@@ -482,7 +492,7 @@ class USM(USMCrypt):
|
|
|
482
492
|
stmid = int.to_bytes(stmid, 4, 'big', signed='False')
|
|
483
493
|
yield stmid, str(filename), self.output.get(f'{stmid.decode()}_{chno}', None)
|
|
484
494
|
|
|
485
|
-
def get_video(self):
|
|
495
|
+
def get_video(self) -> VP9Codec | H264Codec | MPEG1Codec:
|
|
486
496
|
"""Create a video codec from the available streams.
|
|
487
497
|
|
|
488
498
|
NOTE: A temporary file may be created with this process to determine the stream information."""
|
|
@@ -24,7 +24,7 @@ class BuildExt(build_ext):
|
|
|
24
24
|
# ASAN on Linux
|
|
25
25
|
# This only works with GCC - you also need to specify
|
|
26
26
|
# LD_PRELOAD=$(gcc -print-file-name=libasan.so)
|
|
27
|
-
compile_args += ['-O0', '-g'
|
|
27
|
+
compile_args += ['-O0', '-g']
|
|
28
28
|
else:
|
|
29
29
|
compile_args += ['-O2']
|
|
30
30
|
for ext in self.extensions:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|