You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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格式:

  1. 安装依赖:
pip install pyfmodex
  1. 修改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,提问作者Ξένη Γήινος

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 18:18:11