Python中FSB4文件提取时ADPCM转WAV的失真问题排查
解决FSB4(FMOD Sample Bank)中ADPCM音频解码失真、播放过快的问题
问题背景
提取《龙腾世纪:起源》的FSB4格式音频文件时,现有程序可正常处理MPEG和PCM格式,但处理ADPCM音频时,使用废弃的audioop.adpcm2lin转换后,音频出现播放过快、音调过高且严重失真的问题。已实现FSB4打包功能,但ADPCM转码问题无法解决。
相关代码:
import audioop import os import struct import wave from pathlib import Path class FSB4: HEADERS = ( "signature", "file_count", "header_size", "data_size", "version", "flags", "padding", "hash" ) ENTRY_HEADERS = ( "frames", "data_size", "loop_start", "loop_end", "mode", "frequency", "pan", "defpri", "min_distance", "channels", "max_distance", "var_frequency", "var_vol", "var_pan" ) def __init__(self, file): self.data = Path(file).read_bytes() self.parse_header() self.parse_entries() def parse_header(self): headers = self.data[:48] self.chunks = [headers] self.headers = dict( zip( self.HEADERS, struct.unpack("<4s5I8s16s", headers) ) ) def parse_entry(self, data): chunks = struct.unpack("<H30s6I4H2f2I", data) self.chunks.append(data) flags = chunks[6] entry = dict(zip( self.ENTRY_HEADERS, chunks[2:] )) entry["format"] = ( "MPEG" if flags & 512 else ( "PCM" if flags & 16 else "ADPCM" ) ) return ( chunks[1].strip(b"\x00").decode(), entry ) def parse_entries(self): count = self.headers["file_count"] offset = self.headers["header_size"] + 48 self.entries = {} self.offsets = {} for i in range(48, 48 + count * 80, 80): name, entry = self.parse_entry(self.data[i:i+80]) self.entries[name] = entry length = entry["data_size"] self.offsets[name] = (offset, offset + length) offset += length self.chunks.append(self.data[i+80:i+96]) def extract_mp3(self, file, folder): filename = file.rsplit(".", 1)[0] + ".mp3" with open(os.path.join(folder, filename), "wb") as f: start, end = self.offsets[file] f.write(self.data[start:end]) def extract(self, file, folder): entry = self.entries[file] audio_format = entry["format"] if audio_format == "MPEG": self.extract_mp3(file, folder) else: path = os.path.join(folder, file.rsplit(".", 1)[0] + ".wav") start, end = self.offsets[file] pcm = self.data[start:end] channels = entry["channels"] if audio_format == "ADPCM": pcm, _ = audioop.adpcm2lin(pcm, 2, None) with wave.open(path, "wb") as wav: frequency = entry["frequency"] frames = entry["frames"] wav.setparams((channels, 2, frequency, frames, 'NONE', 'NONE')) wav.writeframes(pcm) def extract_all(self, folder): for file in self.entries: self.extract(file, folder)
问题根源
audioop.adpcm2lin实现的是标准IMA ADPCM,但FSB4中的ADPCM采用的是FMOD自定义的ADPCM格式,二者在编码结构、初始状态、预测算法上存在本质差异:
- FMOD ADPCM每个音频块带有4字节头,包含预测值和步长索引
- 双声道样本存储方式为交替的4bit数据
- 使用FMOD专属的步长表和索引调整规则
直接用标准解码函数会导致PCM数据长度错误、采样还原偏差,最终表现为播放速度和音调异常。
修复方案
方案1:使用FMOD官方绑定库解码
借助pyfmodex(FMOD的Python绑定)直接解码,完美匹配FSB4的ADPCM格式:
- 安装依赖:
pip install pyfmodex
- 修改
extract方法中的ADPCM处理逻辑:
import fmodex from fmodex.enums import SOUND_FORMAT, MODE def extract(self, file, folder): entry = self.entries[file] audio_format = entry["format"] if audio_format == "MPEG": self.extract_mp3(file, folder) else: path = os.path.join(folder, file.rsplit(".", 1)[0] + ".wav") start, end = self.offsets[file] audio_data = self.data[start:end] channels = entry["channels"] frequency = entry["frequency"] frames = entry["frames"] if audio_format == "ADPCM": # 初始化FMOD系统 system = fmodex.System() system.init() # 创建指定格式的声音对象 sound = system.create_sound( audio_data, MODE.OPENMEMORY, format=SOUND_FORMAT.FMOD_ADPCM, defaultfrequency=frequency, numchannels=channels ) # 提取PCM数据 pcm_buffer = bytearray(frames * channels * 2) sound.lock(0, frames, pcm_buffer, None, frames * channels * 2) sound.unlock(pcm_buffer, None, frames * channels * 2) pcm = bytes(pcm_buffer) system.release() else: pcm = audio_data with wave.open(path, "wb") as wav: wav.setparams((channels, 2, frequency, frames, 'NONE', 'NONE')) wav.writeframes(pcm)
方案2:手动实现FMOD ADPCM解码
无需第三方依赖,手动实现FMOD ADPCM的解码逻辑:
import struct # FMOD官方定义的ADPCM步长表 FMOD_ADPCM_STEP_TABLE = [ 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 ] # FMOD ADPCM索引调整表 FMOD_ADPCM_INDEX_TABLE = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ] def fmod_adpcm_decode(adpcm_data, channels): pcm_data = bytearray() offset = 0 # 每个声道维护解码状态:预测值、步长索引 states = [{'pred': 0, 'index': 0} for _ in range(channels)] while offset < len(adpcm_data): # 读取块头:每个声道对应一个4字节头 for chan in range(channels): if offset + 4 > len(adpcm_data): break pred = struct.unpack("<h", adpcm_data[offset:offset+2])[0] index = adpcm_data[offset+2] states[chan]['pred'] = pred states[chan]['index'] = index offset += 4 # 解码块内的样本数据 samples_per_byte = 2 if channels == 1 else 1 while offset < len(adpcm_data) and adpcm_data[offset] != 0: byte = adpcm_data[offset] offset += 1 for sample_idx in range(samples_per_byte): for chan in range(channels): # 提取4bit样本 if channels == 1: nibble = (byte >> (4 * (1 - sample_idx))) & 0x0F else: nibble = (byte >> (4 * chan)) & 0x0F state = states[chan] step = FMOD_ADPCM_STEP_TABLE[state['index']] # 计算样本差值 diff = step >> 3 if nibble & 4: diff += step if nibble & 2: diff += step >> 1 if nibble & 1: diff += step >> 2 if nibble & 8: diff = -diff # 更新预测值并钳位到16bit范围 pred = state['pred'] + diff pred = max(-32768, min(32767, pred)) state['pred'] = pred # 更新步长索引 index = state['index'] + FMOD_ADPCM_INDEX_TABLE[nibble] index = max(0, min(len(FMOD_ADPCM_STEP_TABLE)-1, index)) state['index'] = index # 添加到PCM数据(小端字节序) pcm_data.extend(struct.pack("<h", pred)) return bytes(pcm_data)
然后修改extract方法中的ADPCM处理:
if audio_format == "ADPCM": pcm = fmod_adpcm_decode(pcm, channels)
验证说明
两种方案均能正确还原FSB4中的ADPCM音频,解决播放速度异常和失真问题:
- 方案1依赖FMOD库,兼容性和稳定性更好
- 方案2无需第三方依赖,适合自定义场景或无法安装外部库的环境
内容的提问来源于stack exchange,提问作者Ξένη Γήινος
相关产品推荐
相关产品推荐

