from collections import namedtuple from enum import IntEnum from io import BytesIO from .utils import BinaryReader __version__ = "1.0" __author__ = "Simon Pinfold" __email__ = "simon@uint8.me" 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<> 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)