-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy path__init__.py
230 lines (188 loc) · 5.3 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from collections import namedtuple
from enum import IntEnum
from io import BytesIO
from .utils import BinaryReader
__version__ = "1.0"
__author__ = "Simon Pinfold"
__email__ = "[email protected]"
class SoundFormat(IntEnum):
NONE = 0
PCM8 = 1
PCM16 = 2
PCM24 = 3
PCM32 = 4
PCMFLOAT = 5
GCADPCM = 6
IMAADPCM = 7
VAG = 8
HEVAG = 9
XMA = 10
MPEG = 11
CELT = 12
AT9 = 13
XWMA = 14
VORBIS = 15
@property
def file_extension(self):
if self == SoundFormat.MPEG:
return "mp3"
elif self == SoundFormat.VORBIS:
return "ogg"
elif self.is_pcm:
return "wav"
return "bin"
@property
def is_pcm(self):
return self in (SoundFormat.PCM8, SoundFormat.PCM16, SoundFormat.PCM32)
FSB5Header = namedtuple("FSB5Header", [
"id",
"version",
"numSamples",
"sampleHeadersSize",
"nameTableSize",
"dataSize",
"mode",
"zero",
"hash",
"dummy",
"unknown",
"size"
])
Sample = namedtuple("Sample", [
"name",
"frequency",
"channels",
"dataOffset",
"samples",
"metadata",
"data"
])
frequency_values = {
1: 8000,
2: 11000,
3: 11025,
4: 16000,
5: 22050,
6: 24000,
7: 32000,
8: 44100,
9: 48000
}
class MetadataChunkType(IntEnum):
CHANNELS = 1
FREQUENCY = 2
LOOP = 3
XMASEEK = 6
DSPCOEFF = 7
XWMADATA = 10
VORBISDATA = 11
chunk_data_format = {
MetadataChunkType.CHANNELS : "B",
MetadataChunkType.FREQUENCY: "I",
MetadataChunkType.LOOP: "II"
}
VorbisData = namedtuple("VorbisData", ["crc32", "unknown"])
def bits(val, start, len):
stop = start + len
r = val & ((1<<stop)-1)
return r >> start
class FSB5:
def __init__(self, data):
buf = BinaryReader(BytesIO(data), endian="<")
magic = buf.read(4)
if magic != b"FSB5":
raise ValueError("Expected magic header 'FSB5' but got %r" % (magic))
buf.seek(0)
self.header = buf.read_struct_into(FSB5Header, "4s I I I I I I 8s 16s 8s")
if self.header.version == 0:
self.header = self.header._replace(unknown=buf.read_type("I"))
self.header = self.header._replace(mode=SoundFormat(self.header.mode), size=buf.tell())
self.raw_size = self.header.size + self.header.sampleHeadersSize + self.header.nameTableSize + self.header.dataSize
self.samples = []
for i in range(self.header.numSamples):
raw = buf.read_type("Q")
next_chunk = bits(raw, 0, 1)
frequency = bits(raw, 1, 4)
channels = bits(raw, 1+4, 1) + 1
dataOffset = bits(raw, 1+4+1, 28) * 16
samples = bits(raw, 1+4+1+28, 30)
chunks = {}
while next_chunk:
raw = buf.read_type("I")
next_chunk = bits(raw, 0, 1)
chunk_size = bits(raw, 1, 24)
chunk_type = bits(raw, 1+24, 7)
try:
chunk_type = MetadataChunkType(chunk_type)
except ValueError:
pass
if chunk_type == MetadataChunkType.VORBISDATA:
chunk_data = VorbisData(
crc32 = buf.read_type("I"),
unknown = buf.read(chunk_size-4)
)
elif chunk_type in chunk_data_format:
fmt = chunk_data_format[chunk_type]
if buf.struct_calcsize(fmt) != chunk_size:
err = "Expected chunk %s of size %d, SampleHeader specified %d" % (
chunk_type, buf.struct_calcsize(fmt), chunk_size
)
raise ValueError(err)
chunk_data = buf.read_struct(fmt)
else:
chunk_data = buf.read(chunk_size)
chunks[chunk_type] = chunk_data
if MetadataChunkType.FREQUENCY in chunks:
frequency = chunks[MetadataChunkType.FREQUENCY][0]
elif frequency in frequency_values:
frequency = frequency_values[frequency]
else:
raise ValueError("Frequency value %d is not valid and no FREQUENCY metadata chunk was provided")
self.samples.append(Sample(
name="%04d" % (i),
frequency=frequency,
channels=channels,
dataOffset=dataOffset,
samples=samples,
metadata=chunks,
data=None
))
if self.header.nameTableSize:
nametable_start = buf.tell()
samplename_offsets = []
for i in range(self.header.numSamples):
samplename_offsets.append(buf.read_type("I"))
for i in range(self.header.numSamples):
buf.seek(nametable_start + samplename_offsets[i])
name = buf.read_string(maxlen=self.header.nameTableSize)
self.samples[i] = self.samples[i]._replace(name=name.decode("utf-8"))
buf.seek(self.header.size + self.header.sampleHeadersSize + self.header.nameTableSize)
for i in range(self.header.numSamples):
data_start = self.samples[i].dataOffset
data_end = data_start + self.header.dataSize
if i < self.header.numSamples-1:
data_end = self.samples[i+1].dataOffset
self.samples[i] = self.samples[i]._replace(data=buf.read(data_end - data_start))
def rebuild_sample(self, sample):
if sample not in self.samples:
raise ValueError("Sample to decode did not originate from the FSB archive decoding it")
if self.header.mode == SoundFormat.MPEG:
return sample.data
elif self.header.mode == SoundFormat.VORBIS:
# import here as vorbis.py requires native libraries
from . import vorbis
return vorbis.rebuild(sample)
elif self.header.mode.is_pcm:
from .pcm import rebuild
if self.header.mode == SoundFormat.PCM8:
width = 1
elif self.header.mode == SoundFormat.PCM16:
width = 2
else:
width = 4
return rebuild(sample, width)
raise NotImplementedError("Decoding samples of type %s is not supported" % (self.header.mode))
def get_sample_extension(self):
return self.header.mode.file_extension
def load(data):
return FSB5(data)