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

如何从SF2音色库提取样本适配Tone.js?解码报错求助

在Tone.js中加载SoundFont(.sf2)样本时解码音频数据失败的问题

已成功使用sf2-parser解析SoundFont文件,提取到ArrayBuffer格式的样本数据,但调用AudioContext.decodeAudioData()时抛出EncodingError: Unable to decode audio data错误,无法将样本适配到Tone.js Sampler中使用。

现有代码

加载与提取SF2样本的代码

import { sf2 } from '../scripts/sf2-parser';

export const loadSoundFont = async (url: string): Promise<sf2.Parser> => {
    const response = await fetch(url);
    if (response.ok) {
        const parser = new sf2.Parser(new Uint8Array(await response.arrayBuffer()), { parserOptions: { isSf3: false }, isSf3: false })
        parser.parse()
        return parser
    } else {
        throw `Failed to load soundfont file from URL: ${url}`
    }
}

export const getSample = (parser: any, note: string): ArrayBuffer => {
  const sampleIndex = parser.sampleHeader.findIndex((s: any) => s.sampleName.startsWith(note));
  if (sampleIndex < 0) {
    throw new Error(`Sample for note ${note} not found`);
  }
  const sampleData = parser.sample[sampleIndex];
  return sampleData;
}

Tone.js中使用样本的代码

const soundFont = await loadSoundFont('./src/assets/soundfonts/SMK_Style_Synth_Brass_Recreation.sf2')

const sampleArrayBuffer = getSample(soundFont, 'Synth Brass C4L')

const audioContext = Tone.getContext()
audioContext.resume()

const audioBuffer = await audioContext.decodeAudioData(sampleArrayBuffer)

sampler = new Tone.Sampler({
    urls: {
        C4: audioBuffer,
    },
}).toDestination()

问题原因

sf2-parser提取的是原始PCM采样数据,不包含WAV/MP3等标准音频格式的文件头信息。而decodeAudioData()仅能解码带有格式头的封装音频数据,无法直接识别原始PCM,因此抛出解码错误。

解决方案

需要从SF2样本头中提取采样参数,手动创建AudioBuffer并填充转换后的PCM数据:

修改样本提取函数,获取采样信息

export const getSampleAndInfo = (parser: any, note: string) => {
  const sampleIndex = parser.sampleHeader.findIndex((s: any) => s.sampleName.startsWith(note));
  if (sampleIndex < 0) {
    throw new Error(`Sample for note ${note} not found`);
  }
  const sampleData = parser.sample[sampleIndex];
  const sampleHeader = parser.sampleHeader[sampleIndex];
  
  return {
    data: sampleData,
    sampleRate: sampleHeader.sampleRate,
    bitDepth: sampleHeader.sampleSize, // SF2样本通常为16位
    channels: 1 // SF2样本默认单声道,左右声道为独立样本
  };
}

手动创建AudioBuffer并填充数据

const soundFont = await loadSoundFont('./src/assets/soundfonts/SMK_Style_Synth_Brass_Recreation.sf2')
const { data: sampleArrayBuffer, sampleRate, bitDepth, channels } = getSampleAndInfo(soundFont, 'Synth Brass C4L')

const audioContext = Tone.getContext()
audioContext.resume()

// 根据采样参数创建空的AudioBuffer
const audioBuffer = audioContext.createBuffer(
  channels,
  sampleArrayBuffer.byteLength / (bitDepth / 8), // 计算采样点数量
  sampleRate
);

// 将原始PCM数据转换为AudioBuffer所需的浮点格式(-1到1之间)
const channelData = audioBuffer.getChannelData(0);
const dataView = new DataView(sampleArrayBuffer);
let byteOffset = 0;

// 处理16位有符号整数格式的PCM数据(SF2标准格式)
if (bitDepth === 16) {
  for (let i = 0; i < channelData.length; i++) {
    // 读取16位有符号整数,转换为浮点值
    channelData[i] = dataView.getInt16(byteOffset, true) / 32768;
    byteOffset += 2;
  }
}

// 初始化Tone.js Sampler
sampler = new Tone.Sampler({
    urls: {
        C4: audioBuffer,
    },
}).toDestination()

补充说明

  • SF2格式中,立体声乐器的左右声道是独立的样本(如C4L和C4R),若需要立体声输出,需分别提取两个样本并合并到双声道的AudioBuffer中。
  • 若样本位深度为8位,需调整转换逻辑:将无符号整数转换为浮点值(dataView.getUint8(byteOffset) / 127 - 1)。

内容的提问来源于stack exchange,提问作者apps2go

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 05:13:16