PyCriCodecsEx 0.0.1__tar.gz → 0.0.3__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.

Files changed (34) hide show
  1. pycricodecsex-0.0.3/PKG-INFO +35 -0
  2. pycricodecsex-0.0.3/PyCriCodecsEx/__init__.py +1 -0
  3. pycricodecsex-0.0.3/PyCriCodecsEx/acb.py +279 -0
  4. pycricodecsex-0.0.3/PyCriCodecsEx/adx.py +155 -0
  5. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx/awb.py +29 -14
  6. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx/chunk.py +24 -7
  7. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx/cpk.py +25 -16
  8. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx/hca.py +187 -38
  9. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx/usm.py +68 -339
  10. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx/utf.py +25 -37
  11. pycricodecsex-0.0.3/PyCriCodecsEx.egg-info/PKG-INFO +35 -0
  12. pycricodecsex-0.0.3/README.md +13 -0
  13. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/setup.py +1 -1
  14. pycricodecsex-0.0.1/PKG-INFO +0 -81
  15. pycricodecsex-0.0.1/PyCriCodecsEx/__init__.py +0 -1
  16. pycricodecsex-0.0.1/PyCriCodecsEx/acb.py +0 -100
  17. pycricodecsex-0.0.1/PyCriCodecsEx/adx.py +0 -16
  18. pycricodecsex-0.0.1/PyCriCodecsEx.egg-info/PKG-INFO +0 -81
  19. pycricodecsex-0.0.1/README.md +0 -59
  20. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/CriCodecsEx.cpp +0 -0
  21. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/IO.cpp +0 -0
  22. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/IO.hpp +0 -0
  23. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/adx.cpp +0 -0
  24. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/crilayla.cpp +0 -0
  25. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/hca.cpp +0 -0
  26. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/hca.h +0 -0
  27. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/CriCodecsEx/pcm.cpp +0 -0
  28. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/LICENSE +0 -0
  29. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx.egg-info/SOURCES.txt +0 -0
  30. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx.egg-info/dependency_links.txt +0 -0
  31. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx.egg-info/requires.txt +0 -0
  32. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/PyCriCodecsEx.egg-info/top_level.txt +0 -0
  33. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/pyproject.toml +0 -0
  34. {pycricodecsex-0.0.1 → pycricodecsex-0.0.3}/setup.cfg +0 -0
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyCriCodecsEx
3
+ Version: 0.0.3
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.3"
@@ -0,0 +1,279 @@
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
+ CueName: str
18
+
19
+
20
+ class CueTable(UTFViewer):
21
+ CueId: int
22
+ Length: int
23
+ ReferenceIndex: int
24
+ ReferenceType: int
25
+
26
+
27
+ class SequenceTable(UTFViewer):
28
+ NumTracks : int
29
+ TrackIndex: bytes
30
+ Type: int
31
+
32
+
33
+ class SynthTable(UTFViewer):
34
+ ReferenceItems: bytes
35
+
36
+
37
+ class TrackEventTable(UTFViewer):
38
+ Command: bytes
39
+
40
+
41
+ class TrackTable(UTFViewer):
42
+ EventIndex: int
43
+
44
+
45
+ class WaveformTable(UTFViewer):
46
+ EncodeType: int
47
+ MemoryAwbId: int
48
+ NumChannels: int
49
+ NumSamples: int
50
+ SamplingRate: int
51
+ Streaming: int
52
+
53
+
54
+ class ACBTable(UTFViewer):
55
+ AcbGuid: bytes
56
+ Name: str
57
+ Version: int
58
+ VersionString: str
59
+
60
+ AwbFile: bytes
61
+ CueNameTable: List[CueNameTable]
62
+ CueTable: List[CueTable]
63
+ SequenceTable: List[SequenceTable]
64
+ SynthTable: List[SynthTable]
65
+ TrackEventTable: List[TrackEventTable]
66
+ TrackTable: List[TrackTable]
67
+ WaveformTable: List[WaveformTable]
68
+
69
+ @staticmethod
70
+ def decode_tlv(data : bytes):
71
+ pos = 0
72
+ while pos < len(data):
73
+ tag = data[pos : pos + 2]
74
+ length = data[pos + 3]
75
+ value = data[pos + 4 : pos + 4 + length]
76
+ pos += 3 + length
77
+ yield (tag, value)
78
+
79
+ def waveform_of_track(self, index: int):
80
+ tlv = self.decode_tlv(self.TrackEventTable[index])
81
+ def noteOn(data: bytes):
82
+ # Handle note on event
83
+ tlv_type, tlv_index = AcbTrackCommandNoteOnStruct.unpack(data[:AcbTrackCommandNoteOnStruct.size])
84
+ match tlv_type:
85
+ case 0x02: # Synth
86
+ yield from self.waveform_of_synth(tlv_index)
87
+ case 0x03: # Sequence
88
+ yield from self.waveform_of_sequence(tlv_index)
89
+ # Ignore others silently
90
+ for code, data in tlv:
91
+ match code:
92
+ case 2000:
93
+ yield from noteOn(data)
94
+ case 2003:
95
+ yield from noteOn(data)
96
+
97
+ def waveform_of_sequence(self, index : int):
98
+ seq = self.SequenceTable[index]
99
+ for i in range(seq.NumTracks):
100
+ track_index = int.from_bytes(seq.TrackIndex[i*2:i*2+2], 'big')
101
+ yield self.WaveformTable[track_index]
102
+
103
+ def waveform_of_synth(self, index: int):
104
+ item_type, item_index = AcbSynthReferenceStruct.unpack(self.SynthTable[index].ReferenceItems)
105
+ match item_type:
106
+ case 0x00: # No audio
107
+ return
108
+ case 0x01: # Waveform
109
+ yield self.WaveformTable[item_index]
110
+ case 0x02: # Yet another synth...
111
+ yield from self.waveform_of_synth(item_index)
112
+ case 0x03: # Sequence
113
+ yield from self.waveform_of_sequence(item_index)
114
+ case _:
115
+ raise NotImplementedError(f"Unknown synth reference type: {item_type} at index {index}")
116
+
117
+ def waveform_of(self, index : int) -> List["WaveformTable"]:
118
+ cue = next(filter(lambda c: c.CueId == index, self.CueTable), None)
119
+ assert cue, "cue of index %d not found" % index
120
+ match cue.ReferenceType:
121
+ case 0x01:
122
+ return [self.WaveformTable[index]]
123
+ case 0x02:
124
+ return list(self.waveform_of_synth(index))
125
+ case 0x03:
126
+ return list(self.waveform_of_sequence(index))
127
+ case 0x08:
128
+ raise NotImplementedError("BlockSequence type not implemented yet")
129
+ case _:
130
+ raise NotImplementedError(f"Unknown cue reference type: {cue.ReferenceType}")
131
+
132
+ @dataclass(frozen=True)
133
+ class CueItem:
134
+ CueId: int
135
+ CueName: str
136
+ Length: float
137
+ Waveforms: list[int] # List of waveform IDs
138
+
139
+ class ACB(UTF):
140
+ """Use this class to read, and modify ACB files in memory."""
141
+ def __init__(self, stream : str | BinaryIO) -> None:
142
+ """Loads an ACB file from the given stream.
143
+
144
+ Args:
145
+ stream (str | BinaryIO): The path to the ACB file or a BinaryIO stream containing the ACB data.
146
+ """
147
+ super().__init__(stream, recursive=True)
148
+
149
+ @property
150
+ def payload(self) -> dict:
151
+ """Retrives the only UTF table dict within the ACB file."""
152
+ return self.dictarray[0]
153
+
154
+ @property
155
+ def view(self) -> ACBTable:
156
+ """Returns a view of the ACB file, with all known tables mapped to their respective classes.
157
+
158
+ * Use this to interact with the ACB payload instead of `payload` for helper functions, etc"""
159
+ return ACBTable(self.payload)
160
+
161
+ @property
162
+ def name(self) -> str:
163
+ """Returns the name of the ACB file."""
164
+ return self.view.Name
165
+
166
+ @property
167
+ def awb(self) -> AWB:
168
+ """Returns the AWB object associated with the ACB."""
169
+ return AWB(self.view.AwbFile)
170
+
171
+ def get_waveforms(self) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
172
+ """Returns a list of decoded waveforms.
173
+
174
+ Item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
175
+ """
176
+ CODEC_TABLE = {
177
+ AcbEncodeTypes.ADX: ADXCodec,
178
+ AcbEncodeTypes.HCA: HCACodec,
179
+ AcbEncodeTypes.HCAMX: HCACodec,
180
+ }
181
+ awb = self.awb
182
+ wavs = []
183
+ for wav in self.view.WaveformTable:
184
+ encode = AcbEncodeTypes(wav.EncodeType)
185
+ codec = (CODEC_TABLE.get(encode, None))
186
+ if codec:
187
+ wavs.append(codec(awb.get_file_at(wav.MemoryAwbId)))
188
+ else:
189
+ wavs.append((encode, wav.NumChannels, wav.NumSamples, wav.SamplingRate, awb.get_file_at(wav.MemoryAwbId)))
190
+ return wavs
191
+
192
+ def set_waveforms(self, value: List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]):
193
+ """Sets the waveform data.
194
+
195
+ Input item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
196
+
197
+ NOTE: Cue duration is not set. You need to change that manually.
198
+ """
199
+ WAVEFORM = self.view.WaveformTable[0]._payload.copy()
200
+ encoded = []
201
+ tables = self.view.WaveformTable
202
+ tables.clear()
203
+ for i, codec in enumerate(value):
204
+ if type(codec) == HCACodec:
205
+ encoded.append(codec.get_encoded())
206
+ tables.append(WaveformTable(WAVEFORM.copy()))
207
+ entry = tables[-1]
208
+ entry.EncodeType = AcbEncodeTypes.HCA.value
209
+ entry.NumChannels = codec.chnls
210
+ entry.NumSamples = codec.total_samples
211
+ entry.SamplingRate = codec.sampling_rate
212
+ elif type(codec) == ADXCodec:
213
+ encoded.append(codec.get_encoded())
214
+ tables.append(WaveformTable(WAVEFORM.copy()))
215
+ entry = tables[-1]
216
+ entry.EncodeType = AcbEncodeTypes.ADX.value
217
+ entry.NumChannels = codec.chnls
218
+ entry.NumSamples = codec.total_samples
219
+ entry.SamplingRate = codec.sampling_rate
220
+ elif isinstance(codec, tuple):
221
+ e_type, e_channels, e_samples, e_rate, e_data = codec
222
+ encoded.append(e_data)
223
+ tables.append(WaveformTable(WAVEFORM.copy()))
224
+ entry = tables[-1]
225
+ entry.EncodeType = e_type.value
226
+ entry.NumChannels = e_channels
227
+ entry.NumSamples = e_samples
228
+ entry.SamplingRate = e_rate
229
+ else:
230
+ raise TypeError(f"Unsupported codec type: {type(codec)}")
231
+ tables[-1].MemoryAwbId = i
232
+ awb = self.awb
233
+ self.view.AwbFile = AWBBuilder(encoded, awb.subkey, awb.version, align=awb.align).build()
234
+ pass
235
+
236
+ @property
237
+ def cues(self) -> Generator[CueItem, None, None]:
238
+ """Returns a generator of **read-only** Cues.
239
+
240
+ Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
241
+ To modify cues, use the `view` property instead.
242
+ """
243
+ for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
244
+ waveforms = self.view.waveform_of(cue.CueId)
245
+ yield CueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
246
+
247
+ class ACBBuilder:
248
+ """Use this class to build ACB files from an existing ACB object."""
249
+ acb: ACB
250
+
251
+ def __init__(self, acb: ACB) -> None:
252
+ """Initializes the ACBBuilder with an existing ACB object.
253
+
254
+ Args:
255
+ acb (ACB): The ACB object to build from.
256
+
257
+ Building ACB from scratch isn't planned for now since:
258
+
259
+ * We don't know how SeqCommandTable TLVs work. This is the biggest issue.
260
+ * Many fields are unknown or not well understood
261
+ - Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
262
+ with their own ACF table. Missing these is not a fun debugging experience.
263
+ * ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
264
+
265
+ Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
266
+ """
267
+ self.acb = acb
268
+
269
+ def build(self) -> bytes:
270
+ """Builds an ACB binary blob from the current ACB object.
271
+
272
+ The object may be modified in place before building, which will be reflected in the output binary.
273
+ """
274
+ # Check whether all AWB indices are valid
275
+ assert all(
276
+ waveform.MemoryAwbId < self.acb.awb.numfiles for waveform in self.acb.view.WaveformTable
277
+ ), "one or more AWB indices are out of range"
278
+ binary = UTFBuilder(self.acb.dictarray, encoding=self.acb.encoding, table_name=self.acb.table_name)
279
+ return binary.bytes()
@@ -0,0 +1,155 @@
1
+ from typing import BinaryIO
2
+ from io import BytesIO
3
+ from PyCriCodecsEx.chunk import *
4
+ import CriCodecsEx
5
+ class ADX:
6
+ """ADX class for decoding and encoding ADX files, pass the either `adx file` or `wav file` in bytes to either `decode` or `encode` respectively.
7
+
8
+ **NOTE:** Direct usage of this class is not recommended, use the `ADXCodec` wrapper instead.
9
+ """
10
+
11
+ # Decodes ADX to WAV.
12
+ @staticmethod
13
+ def decode(data: bytes) -> bytes:
14
+ """ Decodes ADX to WAV. """
15
+ return CriCodecsEx.AdxDecode(bytes(data))
16
+
17
+ # Encodes WAV to ADX.
18
+ @staticmethod
19
+ def encode(data: bytes, BitDepth = 0x4, Blocksize = 0x12, Encoding = 3, AdxVersion = 0x4, Highpass_Frequency = 0x1F4, Filter = 0, force_not_looping = False) -> bytes:
20
+ """ Encodes WAV to ADX. """
21
+ return CriCodecsEx.AdxEncode(bytes(data), BitDepth, Blocksize, Encoding, Highpass_Frequency, Filter, AdxVersion, force_not_looping)
22
+
23
+ class ADXCodec(ADX):
24
+ """Use this class for encoding and decoding ADX files, from and to WAV."""
25
+
26
+ CHUNK_INTERVAL = 99.9
27
+ BASE_FRAMERATE = 2997
28
+ # TODO: Move these to an enum
29
+ AUDIO_CODEC = 2
30
+ METADATA_COUNT = 0
31
+
32
+ filename : str
33
+ filesize : int
34
+
35
+ adx : bytes
36
+ header : bytes
37
+ sfaStream: BinaryIO
38
+
39
+ AdxDataOffset: int
40
+ AdxEncoding: int
41
+ AdxBlocksize: int
42
+ AdxSampleBitdepth: int
43
+ AdxChannelCount: int
44
+ AdxSamplingRate: int
45
+ AdxSampleCount: int
46
+ AdxHighpassFrequency: int
47
+ AdxVersion: int
48
+ AdxFlags: int
49
+
50
+ chnls: int
51
+ sampling_rate: int
52
+ total_samples: int
53
+ avbps: int
54
+
55
+ def __init__(self, stream: str | bytes, filename: str = "default.adx", bitdepth: int = 4, **kwargs):
56
+ """Initializes the ADX encoder/decoder
57
+
58
+ Args:
59
+ stream (str | bytes): Path to the ADX or WAV file, or a BinaryIO stream. WAV files will be automatically encoded with the given settings first.
60
+ filename (str, optional): Output filename. Defaults to "default.adx".
61
+ bitdepth (int, optional): Audio bit depth within [2,15]. Defaults to 4.
62
+ """
63
+ if type(stream) == str:
64
+ self.adx = open(stream, "rb").read()
65
+ else:
66
+ self.adx = stream
67
+ self.filename = filename
68
+ self.filesize = len(self.adx)
69
+ magic = self.adx[:4]
70
+ if magic == b"RIFF":
71
+ self.adx = self.encode(self.adx, bitdepth, force_not_looping=True)
72
+ self.sfaStream = BytesIO(self.adx)
73
+ header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
74
+ FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
75
+ assert FourCC == 0x8000, "either ADX or WAV is supported"
76
+ assert self.AdxVersion in {3,4}, "unsupported ADX version"
77
+ if self.AdxVersion == 4:
78
+ self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
79
+ self.sfaStream.seek(0)
80
+ self.chnls = self.AdxChannelCount
81
+ self.sampling_rate = self.AdxSamplingRate
82
+ self.total_samples = self.AdxSampleCount
83
+ self.avbps = int(self.filesize * 8 * self.chnls) - self.filesize
84
+
85
+ def generate_SFA(self, index: int, builder):
86
+ # USMBuilder usage
87
+ current_interval = 0
88
+ stream_size = len(self.adx) - self.AdxBlocksize
89
+ chunk_size = int(self.AdxSamplingRate // (self.BASE_FRAMERATE / 100) // 32) * (self.AdxBlocksize * self.AdxChannelCount)
90
+ self.sfaStream.seek(0)
91
+ res = []
92
+ while self.sfaStream.tell() < stream_size:
93
+ if self.sfaStream.tell() > 0:
94
+ if self.sfaStream.tell() + chunk_size < stream_size:
95
+ datalen = chunk_size
96
+ else:
97
+ datalen = (stream_size - (self.AdxDataOffset + 4) - chunk_size) % chunk_size
98
+ else:
99
+ datalen = self.AdxDataOffset + 4
100
+ if not datalen:
101
+ break
102
+ padding = (0x20 - (datalen % 0x20) if datalen % 0x20 != 0 else 0)
103
+ SFA_chunk = USMChunkHeader.pack(
104
+ USMChunckHeaderType.SFA.value,
105
+ datalen + 0x18 + padding,
106
+ 0,
107
+ 0x18,
108
+ padding,
109
+ index,
110
+ 0,
111
+ 0,
112
+ 0,
113
+ round(current_interval),
114
+ self.BASE_FRAMERATE,
115
+ 0,
116
+ 0
117
+ )
118
+ chunk_data = self.sfaStream.read(datalen)
119
+ if builder.encrypt_audio:
120
+ SFA_chunk = builder.AudioMask(chunk_data)
121
+ SFA_chunk += chunk_data.ljust(datalen + padding, b"\x00")
122
+ current_interval += self.CHUNK_INTERVAL
123
+ res.append(SFA_chunk)
124
+ # ---
125
+ SFA_chunk = USMChunkHeader.pack(
126
+ USMChunckHeaderType.SFA.value,
127
+ 0x38,
128
+ 0,
129
+ 0x18,
130
+ 0,
131
+ index,
132
+ 0,
133
+ 0,
134
+ 2,
135
+ 0,
136
+ 30,
137
+ 0,
138
+ 0
139
+ )
140
+ SFA_chunk += b"#CONTENTS END ===============\x00"
141
+ res[-1] += SFA_chunk
142
+ return res
143
+
144
+ def get_metadata(self):
145
+ return None
146
+
147
+ def get_encoded(self):
148
+ """Gets the encoded ADX audio data."""
149
+ return self.adx
150
+
151
+ def save(self, filepath: str):
152
+ """Saves the encoded ADX audio to filepath"""
153
+ with open(filepath, "wb") as f:
154
+ f.write(self.decode(self.adx))
155
+
@@ -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.readheader()
34
+ self._readheader()
30
35
 
31
- def readheader(self):
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.stringtypes(self.id_intsize)}", self.stream.read(self.id_intsize*self.numfiles)):
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.stringtypes(offset_intsize)}", self.stream.read(offset_intsize*(self.numfiles+1))):
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
- """ Generator function to yield all data blobs from an AWB. """
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
- """ Gets you a file at specific index. """
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-1])
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 stringtypes(self, intsize: int) -> str:
72
+ def _stringtypes(self, intsize: int) -> str:
69
73
  if intsize == 1:
70
74
  return "B" # Probably impossible.
71
75
  elif intsize == 2:
@@ -78,7 +82,17 @@ class AWB:
78
82
  raise ValueError("Unknown int size.")
79
83
 
80
84
  class AWBBuilder:
85
+ """Use this class to build AWB files from a list of bytes."""
81
86
  def __init__(self, infiles: list[bytes], subkey: int = 0, version: int = 2, id_intsize = 0x2, align: int = 0x20) -> None:
87
+ """Initializes the AWB builder.
88
+
89
+ Args:
90
+ infiles (list[bytes]): List of bytes to be included in the AWB file.
91
+ subkey (int, optional): AWB subkey. Defaults to 0.
92
+ version (int, optional): AWB version. Defaults to 2.
93
+ id_intsize (hexadecimal, optional): Integer size (in bytes) for string lengths. Defaults to 0x2.
94
+ align (int, optional): Alignment. Defaults to 0x20.
95
+ """
82
96
  if version == 1 and subkey != 0:
83
97
  raise ValueError("Cannot have a subkey with AWB version of 1.")
84
98
  elif id_intsize not in [0x2, 0x4, 0x8]:
@@ -89,7 +103,7 @@ class AWBBuilder:
89
103
  self.subkey = subkey
90
104
  self.id_intsize = id_intsize
91
105
 
92
- def stringtypes(self, intsize: int) -> str:
106
+ def _stringtypes(self, intsize: int) -> str:
93
107
  if intsize == 1:
94
108
  return "B" # Probably impossible.
95
109
  elif intsize == 2:
@@ -102,6 +116,7 @@ class AWBBuilder:
102
116
  raise ValueError("Unknown int size.")
103
117
 
104
118
  def build(self) -> bytes:
119
+ """Builds the AWB file from the provided infiles bytes."""
105
120
  size = 0
106
121
  ofs = []
107
122
  numfiles = 0
@@ -122,7 +137,7 @@ class AWBBuilder:
122
137
  b'AFS2', self.version, intsize, self.id_intsize, numfiles, self.align, self.subkey
123
138
  )
124
139
 
125
- id_strsize = f"<{self.stringtypes(self.id_intsize)}"
140
+ id_strsize = f"<{self._stringtypes(self.id_intsize)}"
126
141
  for i in range(numfiles):
127
142
  header += pack(id_strsize, i)
128
143
 
@@ -12,7 +12,8 @@ WavNoteHeaderStruct = Struct("<4sII")
12
12
  WavDataHeaderStruct = Struct("<4sI")
13
13
  AdxHeaderStruct = Struct(">HHBBBBIIHBB")
14
14
  AdxLoopHeaderStruct = Struct(">HHHHIIII")
15
-
15
+ AcbSynthReferenceStruct = Struct(">HH")
16
+ AcbTrackCommandNoteOnStruct = Struct(">HH")
16
17
  class USMChunckHeaderType(Enum):
17
18
  CRID = b"CRID" # Header.
18
19
  SFSH = b"SFSH" # SofDec1 Header?
@@ -45,11 +46,6 @@ class HCAType(Enum):
45
46
  HCA = b"HCA\x00" # Header.
46
47
  EHCA = b"\xC8\xC3\xC1\x00" # Encrypted HCA header.
47
48
 
48
- class VideoType(Enum):
49
- IVF = b"DKIF" # Header.
50
- # H264 = b"" # Header.
51
- # MPEG = b"" # Header.
52
-
53
49
  # I saw some devs swap the unsigned/signed indexes. So I am not sure what's correct or not.
54
50
  # In my own experience, swapping those results in an incorrect signed values (should be unsigned) in ACB's/CPK's.
55
51
  # If someone were to change this, they must change 'stringtypes' function in UTF/UTFBuilder classes.
@@ -72,4 +68,25 @@ class CriHcaQuality(Enum):
72
68
  High = 1
73
69
  Middle = 2
74
70
  Low = 3
75
- Lowest = 5
71
+ Lowest = 5
72
+
73
+ class AcbEncodeTypes(Enum):
74
+ ADX = 0
75
+ PCM = 1
76
+ HCA = 2
77
+ ADX_ALT = 3
78
+ WiiDSP = 4
79
+ NDSDSP = 5
80
+ HCAMX = 6
81
+ VAG = 7
82
+ ATRAC3 = 8
83
+ CWAV = 9
84
+ HEVAG = 10
85
+ ATRAC9 = 11
86
+ X360XMA = 12
87
+ DSP = 13
88
+ CWACDSP = 14
89
+ PS4HEVAG = 15
90
+ PS4ATRAC9 = 16
91
+ AACM4A = 17
92
+ SwitchOpus = 18