Files
aklive2d/fsb5/__init__.py
Haoyu Xu 57e889fb45 Squashed 'python/fsb5/' content from commit 5acfaed
git-subtree-dir: python/fsb5
git-subtree-split: 5acfaed9b44167eeebbd5f0414745cc23a2104a7
2025-05-01 10:40:57 +08:00

231 lines
5.3 KiB
Python

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<<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)