PyCriCodecsEx 0.0.1__tar.gz → 0.0.5__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.
Files changed (34) hide show
  1. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/adx.cpp +2 -2
  2. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/crilayla.cpp +103 -64
  3. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/hca.cpp +2 -2
  4. pycricodecsex-0.0.5/PKG-INFO +35 -0
  5. pycricodecsex-0.0.5/PyCriCodecsEx/__init__.py +1 -0
  6. pycricodecsex-0.0.5/PyCriCodecsEx/acb.py +306 -0
  7. pycricodecsex-0.0.5/PyCriCodecsEx/adx.py +158 -0
  8. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx/awb.py +31 -17
  9. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx/chunk.py +24 -7
  10. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx/cpk.py +62 -51
  11. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx/hca.py +190 -38
  12. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx/usm.py +82 -347
  13. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx/utf.py +25 -37
  14. pycricodecsex-0.0.5/PyCriCodecsEx.egg-info/PKG-INFO +35 -0
  15. pycricodecsex-0.0.5/README.md +13 -0
  16. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/setup.py +2 -2
  17. pycricodecsex-0.0.1/PKG-INFO +0 -81
  18. pycricodecsex-0.0.1/PyCriCodecsEx/__init__.py +0 -1
  19. pycricodecsex-0.0.1/PyCriCodecsEx/acb.py +0 -100
  20. pycricodecsex-0.0.1/PyCriCodecsEx/adx.py +0 -16
  21. pycricodecsex-0.0.1/PyCriCodecsEx.egg-info/PKG-INFO +0 -81
  22. pycricodecsex-0.0.1/README.md +0 -59
  23. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/CriCodecsEx.cpp +0 -0
  24. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/IO.cpp +0 -0
  25. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/IO.hpp +0 -0
  26. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/hca.h +0 -0
  27. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/CriCodecsEx/pcm.cpp +0 -0
  28. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/LICENSE +0 -0
  29. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx.egg-info/SOURCES.txt +0 -0
  30. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx.egg-info/dependency_links.txt +0 -0
  31. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx.egg-info/requires.txt +0 -0
  32. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/PyCriCodecsEx.egg-info/top_level.txt +0 -0
  33. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/pyproject.toml +0 -0
  34. {pycricodecsex-0.0.1 → pycricodecsex-0.0.5}/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
- unsigned int len = adx.size;
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
- unsigned int len = wav.wav.size+8;
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
- Note: I have no idea how this compression technique works. I just made the wrapper.
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
@@ -16,38 +22,43 @@
16
22
  #include <stdlib.h>
17
23
  #include <string.h>
18
24
 
25
+ unsigned fourCC(const char a, const char b, const char c, const char d) {
26
+ return (a << 0) | (b << 8) | (c << 16) | (d << 24);
27
+ };
28
+ const unsigned CRILAYLA_LO = fourCC('C', 'R', 'I', 'L');
29
+ const unsigned CRILAYLA_HI = fourCC('A', 'Y', 'L', 'A');
30
+ const unsigned long long CRILAYLA_MAGIC = CRILAYLA_LO | (unsigned long long)CRILAYLA_HI << 32uLL;
31
+
19
32
  struct crilayla_header{
20
33
  unsigned long long crilayla;
21
34
  unsigned int decompress_size;
22
35
  unsigned int compressed_size;
23
36
  };
24
37
 
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
38
  unsigned int llcp_dec(unsigned char *src, unsigned int src_len, unsigned char *dst, unsigned int dst_len){
49
- unsigned char *dbuf, *pbuf;
39
+ unsigned char *dbuf, *pbuf;
50
40
  unsigned int plen, poffset, byte;
41
+ unsigned char *sbuf;
42
+ unsigned int bitcnt;
43
+ unsigned int bitdat;
44
+ auto get_bits = [&](unsigned int n){
45
+ unsigned int data, mask;
46
+
47
+ if (bitcnt<n){
48
+ data = ((24-bitcnt)>>3)+1;
49
+ bitcnt += data*8;
50
+ while(data) {
51
+ bitdat = (bitdat<<8) | (*sbuf--);
52
+ data--;
53
+ }
54
+ }
55
+
56
+ data = bitdat>>(bitcnt-n);
57
+ bitcnt -= n;
58
+ mask = (1<<n)-1;
59
+ data &= mask;
60
+ return data;
61
+ };
51
62
 
52
63
  sbuf = src+src_len-1;
53
64
  dbuf = dst+dst_len-1;
@@ -108,40 +119,41 @@ unsigned char* layla_decomp(unsigned char* data, crilayla_header header){
108
119
  return dst;
109
120
  }
110
121
 
111
- unsigned int layla_comp(unsigned char* dest, unsigned int* destLen, unsigned char* src, unsigned int srcLen){
112
- unsigned int n = srcLen - 1, m = *destLen - 0x1, T = 0, d = 0, p, q, i, j, k;
113
- unsigned char* odest = dest;
122
+ unsigned int layla_comp(unsigned char *dest, int *destLen, unsigned char *src, int srcLen)
123
+ {
124
+ int n = srcLen - 1, m = *destLen - 0x1, T = 0, d = 0, p, q, i, j, k;
125
+ unsigned char *odest = dest;
114
126
  for (; n >= 0x100;)
115
127
  {
116
128
  j = n + 3 + 0x2000;
117
- if (j > srcLen) j = srcLen;
118
- for (i = n + 3, p = 0; i < j; i++)
129
+ if (j>srcLen) j = srcLen;
130
+ for (i = n + 3, p = 0; i<j; i++)
119
131
  {
120
132
  for (k = 0; k <= n - 0x100; k++)
121
133
  {
122
134
  if (*(src + n - k) != *(src + i - k)) break;
123
135
  }
124
- if (k > p)
136
+ if (k>p)
125
137
  {
126
138
  q = i - n - 3; p = k;
127
139
  }
128
140
  }
129
- if (p < 3)
141
+ if (p<3)
130
142
  {
131
143
  d = (d << 9) | (*(src + n--)); T += 9;
132
144
  }
133
145
  else
134
146
  {
135
147
  d = (((d << 1) | 1) << 13) | q; T += 14; n -= p;
136
- if (p < 6)
148
+ if (p<6)
137
149
  {
138
150
  d = (d << 2) | (p - 3); T += 2;
139
151
  }
140
- else if (p < 13)
152
+ else if (p<13)
141
153
  {
142
154
  d = (((d << 2) | 3) << 3) | (p - 6); T += 5;
143
155
  }
144
- else if (p < 44)
156
+ else if (p<44)
145
157
  {
146
158
  d = (((d << 5) | 0x1f) << 5) | (p - 13); T += 10;
147
159
  }
@@ -150,45 +162,50 @@ unsigned int layla_comp(unsigned char* dest, unsigned int* destLen, unsigned cha
150
162
  d = ((d << 10) | 0x3ff); T += 10; p -= 44;
151
163
  for (;;)
152
164
  {
153
- for (; T >= 8;)
165
+ for (; m > 0 && T >= 8;)
154
166
  {
155
- *(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d & ((1 << T) - 1);
167
+ *(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d&((1 << T) - 1);
156
168
  }
157
- if (p < 255) break;
169
+ if (p<255) break;
158
170
  d = (d << 8) | 0xff; T += 8; p = p - 0xff;
159
171
  }
160
172
  d = (d << 8) | p; T += 8;
161
173
  }
162
174
  }
163
- for (; T >= 8;)
175
+ for (; m > 0 && T >= 8;)
164
176
  {
165
- *(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d & ((1 << T) - 1);
177
+ *(dest + m--) = (d >> (T - 8)) & 0xff; T -= 8; d = d&((1 << T) - 1);
166
178
  }
167
179
  }
168
- if (T != 0)
180
+ if (m > 0 && T != 0)
169
181
  {
170
182
  *(dest + m--) = d << (8 - T);
171
183
  }
172
- *(dest + m--) = 0; *(dest + m) = 0;
173
- for (;;)
174
- {
175
- if (((*destLen - m) & 3) == 0) break;
176
- *(dest + m--) = 0;
184
+ if (m > 0) {
185
+ *(dest + m--) = 0; *(dest + m) = 0;
186
+ for (;;)
187
+ {
188
+ if (((*destLen - m) & 3) == 0) break;
189
+ *(dest + m--) = 0;
190
+ }
177
191
  }
192
+ if (m <= 0)
193
+ return 0; // Underflow
178
194
  *destLen = *destLen - m; dest += m;
179
- unsigned int l[] = { 0x4c495243,0x414c5941,srcLen - 0x100,*destLen };
180
- for (j = 0; j < 4; j++)
195
+ // CRIL AYLA srcLen-0x100 destLen
196
+ int l[] = { (int)CRILAYLA_LO,(int)CRILAYLA_HI,srcLen - 0x100,*destLen };
197
+ for (j = 0; j<4; j++)
181
198
  {
182
- for (i = 0; i < 4; i++)
199
+ for (i = 0; i<4; i++)
183
200
  {
184
201
  *(odest + i + j * 4) = l[j] & 0xff; l[j] >>= 8;
185
202
  }
186
203
  }
187
- for (j = 0, odest += 0x10; j < *destLen; j++)
204
+ for (j = 0, odest += 0x10; j<*destLen; j++)
188
205
  {
189
206
  *(odest++) = *(dest + j);
190
207
  }
191
- for (j = 0; j < 0x100; j++)
208
+ for (j = 0; j<0x100; j++)
192
209
  {
193
210
  *(odest++) = *(src + j);
194
211
  }
@@ -199,7 +216,17 @@ unsigned int layla_comp(unsigned char* dest, unsigned int* destLen, unsigned cha
199
216
  PyObject* CriLaylaDecompress(PyObject* self, PyObject* d){
200
217
  unsigned char *data = (unsigned char *)PyBytes_AsString(d);
201
218
  crilayla_header header = *(crilayla_header*)data;
202
- unsigned char *out = layla_decomp((data+16), header);
219
+
220
+ if (header.crilayla != CRILAYLA_MAGIC) {
221
+ PyErr_SetString(PyExc_ValueError, "Invalid CRILAYLA header.");
222
+ return NULL;
223
+ }
224
+
225
+ unsigned char *out;
226
+ Py_BEGIN_ALLOW_THREADS
227
+ out = layla_decomp((data+16), header);
228
+ Py_END_ALLOW_THREADS
229
+
203
230
  PyObject *outObj = Py_BuildValue("y#", out, header.decompress_size+256);
204
231
  delete[] out;
205
232
  return outObj;
@@ -213,14 +240,26 @@ unsigned char* CriLaylaDecompress(unsigned char* d){
213
240
 
214
241
  PyObject* CriLaylaCompress(PyObject* self, PyObject* args){
215
242
  unsigned char *data;
216
- unsigned int data_size;
243
+ Py_ssize_t data_size;
217
244
  if(!PyArg_ParseTuple(args, "y#", &data, &data_size)){
218
245
  return NULL;
219
246
  }
220
- unsigned char *buf = new unsigned char[data_size];
221
- memset(buf, 0, data_size);
222
- layla_comp(buf, &data_size, data, data_size);
223
- PyObject* bufObj = Py_BuildValue("y#", buf, data_size);
247
+
248
+ unsigned char *buf = new unsigned char[data_size + 0x200];
249
+
250
+ int compressed_size = data_size;
251
+ int res = 0;
252
+ Py_BEGIN_ALLOW_THREADS
253
+ res = layla_comp(buf, &compressed_size, data, compressed_size);
254
+ Py_END_ALLOW_THREADS
255
+ PyObject* bufObj;
256
+ if (res && res < data_size) {
257
+ bufObj = Py_BuildValue("y#", buf, compressed_size);
258
+ } else {
259
+ // FIXME
260
+ PyErr_SetString(PyExc_RuntimeError, "Compression failure.");
261
+ return NULL;
262
+ }
224
263
  delete[] buf;
225
264
  return bufObj;
226
265
  }
@@ -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
- unsigned int length = view.len;
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
- unsigned int data_size;
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,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyCriCodecsEx
3
+ Version: 0.0.5
4
+ Summary: Criware formats library for Python
5
+ Home-page: https://mos9527.github.io/PyCriCodecsEx/
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: usm
13
+ Requires-Dist: ffmpeg-python; extra == "usm"
14
+ Dynamic: classifier
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: license-file
19
+ Dynamic: provides-extra
20
+ Dynamic: requires-python
21
+ Dynamic: summary
22
+
23
+ PyCriCodecsEx
24
+ ---
25
+ A continuation of @Youjose's work on Criware formats. Feautres are still in flux and subject to change. When in doubt, Refer to the [original repo](https://github.com/Youjose/PyCriCodecs) for more information.
26
+
27
+ Detailed documentation, installation instructions are available at https://mos9527.com/PyCriCodecsEx
28
+
29
+
30
+ # Credits
31
+ - https://github.com/Youjose/PyCriCodecs
32
+ - https://github.com/Mikewando/PyCriCodecs ([PR#1 on USM](https://github.com/mos9527/PyCriCodecsEx/pull/1))
33
+ - https://github.com/donmai-me/WannaCRI
34
+ - https://github.com/vgmstream/vgmstream
35
+ - https://github.com/K0lb3/UnityPy (For CI script)
@@ -0,0 +1 @@
1
+ __version__ = "0.0.5"
@@ -0,0 +1,306 @@
1
+ # Credit:
2
+ # - github.com/vgmstream/vgmstream which is why this is possible at all
3
+ # - Original work by https://github.com/Youjose/PyCriCodecs
4
+ # See Research/ACBSchema.py for more details.
5
+
6
+ from typing import Generator, List, Tuple, BinaryIO
7
+ from PyCriCodecsEx.chunk import *
8
+ from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
9
+ from PyCriCodecsEx.hca import HCACodec
10
+ from PyCriCodecsEx.adx import ADXCodec
11
+ from PyCriCodecsEx.awb import AWB, AWBBuilder
12
+ from dataclasses import dataclass
13
+ from copy import deepcopy
14
+
15
+ class CueNameTable(UTFViewer):
16
+ CueIndex: int
17
+ '''Index into CueTable'''
18
+ CueName: str
19
+ '''Name of the cue'''
20
+
21
+
22
+ class CueTable(UTFViewer):
23
+ CueId: int
24
+ '''Corresponds to the cue index found in CueNameTable'''
25
+ Length: int
26
+ '''Duration of the cue in milliseconds'''
27
+ ReferenceIndex: int
28
+ ReferenceType: int
29
+
30
+
31
+ class SequenceTable(UTFViewer):
32
+ NumTracks : int
33
+ TrackIndex: bytes
34
+ Type: int
35
+
36
+
37
+ class SynthTable(UTFViewer):
38
+ ReferenceItems: bytes
39
+
40
+
41
+ class TrackEventTable(UTFViewer):
42
+ Command: bytes
43
+
44
+
45
+ class TrackTable(UTFViewer):
46
+ EventIndex: int
47
+
48
+
49
+ class WaveformTable(UTFViewer):
50
+ EncodeType: int
51
+ MemoryAwbId: int
52
+ NumChannels: int
53
+ NumSamples: int
54
+ SamplingRate: int
55
+ Streaming: int
56
+
57
+
58
+ class ACBTable(UTFViewer):
59
+ '''ACB Table View'''
60
+
61
+ AcbGuid: bytes
62
+ '''GUID of the ACB. This SHOULD be different for each ACB file.'''
63
+ Name: str
64
+ '''Name of the ACB. This is usually the name of the sound bank.'''
65
+ Version: int
66
+ VersionString: str
67
+
68
+ AwbFile: bytes
69
+ CueNameTable: List[CueNameTable]
70
+ '''A list of cue names with their corresponding indices into CueTable'''
71
+ CueTable: List[CueTable]
72
+ '''A list of cues with their corresponding references'''
73
+
74
+ SequenceTable: List[SequenceTable]
75
+ SynthTable: List[SynthTable]
76
+ TrackEventTable: List[TrackEventTable]
77
+ TrackTable: List[TrackTable]
78
+ WaveformTable: List[WaveformTable]
79
+
80
+ @staticmethod
81
+ def _decode_tlv(data : bytes):
82
+ pos = 0
83
+ while pos < len(data):
84
+ tag = data[pos : pos + 2]
85
+ length = data[pos + 3]
86
+ value = data[pos + 4 : pos + 4 + length]
87
+ pos += 3 + length
88
+ yield (tag, value)
89
+
90
+ def _waveform_of_track(self, index: int):
91
+ tlv = self._decode_tlv(self.TrackEventTable[index])
92
+ def noteOn(data: bytes):
93
+ # Handle note on event
94
+ tlv_type, tlv_index = AcbTrackCommandNoteOnStruct.unpack(data[:AcbTrackCommandNoteOnStruct.size])
95
+ match tlv_type:
96
+ case 0x02: # Synth
97
+ yield from self._waveform_of_synth(tlv_index)
98
+ case 0x03: # Sequence
99
+ yield from self._waveform_of_sequence(tlv_index)
100
+ # Ignore others silently
101
+ for code, data in tlv:
102
+ match code:
103
+ case 2000:
104
+ yield from noteOn(data)
105
+ case 2003:
106
+ yield from noteOn(data)
107
+
108
+ def _waveform_of_sequence(self, index : int):
109
+ seq = self.SequenceTable[index]
110
+ for i in range(seq.NumTracks):
111
+ track_index = int.from_bytes(seq.TrackIndex[i*2:i*2+2], 'big')
112
+ yield self.WaveformTable[track_index]
113
+
114
+ def _waveform_of_synth(self, index: int):
115
+ item_type, item_index = AcbSynthReferenceStruct.unpack(self.SynthTable[index].ReferenceItems)
116
+ match item_type:
117
+ case 0x00: # No audio
118
+ return
119
+ case 0x01: # Waveform
120
+ yield self.WaveformTable[item_index]
121
+ case 0x02: # Yet another synth...
122
+ yield from self._waveform_of_synth(item_index)
123
+ case 0x03: # Sequence
124
+ yield from self._waveform_of_sequence(item_index)
125
+ case _:
126
+ raise NotImplementedError(f"Unknown synth reference type: {item_type} at index {index}")
127
+
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."""
132
+ cue = next(filter(lambda c: c.CueId == index, self.CueTable), None)
133
+ assert cue, "cue of index %d not found" % index
134
+ match cue.ReferenceType:
135
+ case 0x01:
136
+ return [self.WaveformTable[index]]
137
+ case 0x02:
138
+ return list(self._waveform_of_synth(index))
139
+ case 0x03:
140
+ return list(self._waveform_of_sequence(index))
141
+ case 0x08:
142
+ raise NotImplementedError("BlockSequence type not implemented yet")
143
+ case _:
144
+ raise NotImplementedError(f"Unknown cue reference type: {cue.ReferenceType}")
145
+
146
+ @dataclass(frozen=True)
147
+ class PackedCueItem:
148
+ '''Helper class for read-only cue information'''
149
+
150
+ CueId: int
151
+ '''Cue ID'''
152
+ CueName: str
153
+ '''Cue name'''
154
+ Length: float
155
+ '''Duration in seconds'''
156
+ Waveforms: list[int]
157
+ '''List of waveform IDs, corresponds to ACB.get_waveforms()'''
158
+
159
+ class ACB(UTF):
160
+ """Use this class to read, and modify ACB files in memory."""
161
+ def __init__(self, stream : str | BinaryIO) -> None:
162
+ """Loads an ACB file from the given stream.
163
+
164
+ Args:
165
+ stream (str | BinaryIO): The path to the ACB file or a BinaryIO stream containing the ACB data.
166
+ """
167
+ super().__init__(stream, recursive=True)
168
+
169
+ @property
170
+ def payload(self) -> dict:
171
+ """Retrives the only UTF table dict within the ACB file."""
172
+ return self.dictarray[0]
173
+
174
+ @property
175
+ def view(self) -> ACBTable:
176
+ """Returns a view of the ACB file, with all known tables mapped to their respective classes.
177
+
178
+ * Use this to interact with the ACB payload instead of `payload` for helper functions, etc"""
179
+ return ACBTable(self.payload)
180
+
181
+ @property
182
+ def name(self) -> str:
183
+ """Returns the name of the ACB file."""
184
+ return self.view.Name
185
+
186
+ @property
187
+ def awb(self) -> AWB:
188
+ """Returns the AWB object associated with the ACB."""
189
+ return AWB(self.view.AwbFile)
190
+
191
+ def get_waveforms(self, **kwargs) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
192
+ """Returns a list of decoded waveforms.
193
+
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.
202
+ """
203
+ CODEC_TABLE = {
204
+ AcbEncodeTypes.ADX: ADXCodec,
205
+ AcbEncodeTypes.HCA: HCACodec,
206
+ AcbEncodeTypes.HCAMX: HCACodec,
207
+ }
208
+ awb = self.awb
209
+ wavs = []
210
+ for wav in self.view.WaveformTable:
211
+ encode = AcbEncodeTypes(wav.EncodeType)
212
+ codec = (CODEC_TABLE.get(encode, None))
213
+ if codec:
214
+ wavs.append(codec(awb.get_file_at(wav.MemoryAwbId), **kwargs))
215
+ else:
216
+ wavs.append((encode, wav.NumChannels, wav.NumSamples, wav.SamplingRate, awb.get_file_at(wav.MemoryAwbId)))
217
+ return wavs
218
+
219
+ def set_waveforms(self, value: List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]):
220
+ """Sets the waveform data.
221
+
222
+ Input item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
223
+
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.
225
+ """
226
+ WAVEFORM = self.view.WaveformTable[0]._payload.copy()
227
+ encoded = []
228
+ tables = self.view.WaveformTable
229
+ tables.clear()
230
+ for i, codec in enumerate(value):
231
+ if type(codec) == HCACodec:
232
+ encoded.append(codec.get_encoded())
233
+ tables.append(WaveformTable(WAVEFORM.copy()))
234
+ entry = tables[-1]
235
+ entry.EncodeType = AcbEncodeTypes.HCA.value
236
+ entry.NumChannels = codec.chnls
237
+ entry.NumSamples = codec.total_samples
238
+ entry.SamplingRate = codec.sampling_rate
239
+ elif type(codec) == ADXCodec:
240
+ encoded.append(codec.get_encoded())
241
+ tables.append(WaveformTable(WAVEFORM.copy()))
242
+ entry = tables[-1]
243
+ entry.EncodeType = AcbEncodeTypes.ADX.value
244
+ entry.NumChannels = codec.chnls
245
+ entry.NumSamples = codec.total_samples
246
+ entry.SamplingRate = codec.sampling_rate
247
+ elif isinstance(codec, tuple):
248
+ e_type, e_channels, e_samples, e_rate, e_data = codec
249
+ encoded.append(e_data)
250
+ tables.append(WaveformTable(WAVEFORM.copy()))
251
+ entry = tables[-1]
252
+ entry.EncodeType = e_type.value
253
+ entry.NumChannels = e_channels
254
+ entry.NumSamples = e_samples
255
+ entry.SamplingRate = e_rate
256
+ else:
257
+ raise TypeError(f"Unsupported codec type: {type(codec)}")
258
+ tables[-1].MemoryAwbId = i
259
+ awb = self.awb
260
+ self.view.AwbFile = AWBBuilder(encoded, awb.subkey, awb.version, align=awb.align).build()
261
+ pass
262
+
263
+ @property
264
+ def cues(self) -> Generator[PackedCueItem, None, None]:
265
+ """Returns a generator of **read-only** Cues.
266
+
267
+ Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
268
+ To modify cues, use the `view` property instead.
269
+ """
270
+ for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
271
+ waveforms = self.view.waveform_of(cue.CueId)
272
+ yield PackedCueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
273
+
274
+ class ACBBuilder:
275
+ """Use this class to build ACB files from an existing ACB object."""
276
+ acb: ACB
277
+
278
+ def __init__(self, acb: ACB) -> None:
279
+ """Initializes the ACBBuilder with an existing ACB object.
280
+
281
+ Args:
282
+ acb (ACB): The ACB object to build from.
283
+
284
+ Building ACB from scratch isn't planned for now since:
285
+
286
+ * We don't know how SeqCommandTable TLVs work. This is the biggest issue.
287
+ * Many fields are unknown or not well understood
288
+ - Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
289
+ with their own ACF table. Missing these is not a fun debugging experience.
290
+ * ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
291
+
292
+ Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
293
+ """
294
+ self.acb = acb
295
+
296
+ def build(self) -> bytes:
297
+ """Builds an ACB binary blob from the current ACB object.
298
+
299
+ The object may be modified in place before building, which will be reflected in the output binary.
300
+ """
301
+ # Check whether all AWB indices are valid
302
+ assert all(
303
+ waveform.MemoryAwbId < self.acb.awb.numfiles for waveform in self.acb.view.WaveformTable
304
+ ), "one or more AWB indices are out of range"
305
+ binary = UTFBuilder(self.acb.dictarray, encoding=self.acb.encoding, table_name=self.acb.table_name)
306
+ return binary.bytes()