如何用js-mp3解码MP3并在AudioContext中播放?



由于Safari 15中的一个错误有时会导致AudioContext.decodeAudioData失败(参见Safari 15无法解码以前版本解码的音频数据而没有问题),因此我正在尝试做一个解决方案。解决方法是使用库https://github.com/soundbus-technologies/js-mp3解码文件,然后从该数据创建AudioBuffer并播放。

问题是js-mp3返回一个ArrayBuffer与PCM数据,并创建一个AudioBuffer需要两个独立的数组,每个通道一个,以及采样速率和采样帧长度。目前我得到的是:


function concatTypedArrays(a, b) { // a, b TypedArray of same type
var c = new (a.constructor)(a.length + b.length);
c.set(a, 0);
c.set(b, a.length);
return c;
};
// responseData is an ArrayBuffer with the MP3 file...
let decoder = Mp3.newDecoder(responseData);
let pcmArrayBuffer = decoder.decode();
//Trying to read the frames to get the two channels. Maybe get it correctly from
//the pcmArrayBuffer instead?

decoder.source.pos = 0;
let left = new Float32Array(), right = new Float32Array();
console.log('Frame count: ' + decoder.frameStarts.length);
let result;
let i = 0;
let samplesDecoded = 0;

while (true) {
let result = decoder.readFrame();
if (result.err) {
break;
} else {
console.log('READ FRAME ' + (++i));
samplesDecoded += 1152; //Think this is the right sample count per frame for MPEG1 files
left = concatTypedArrays(left, decoder.frame.v_vec[0]);
right = concatTypedArrays(left, decoder.frame.v_vec[1]);
}
}
let audioContext = new AudioContext();
let buffer = audioContext.createBuffer(2, samplesDecoded, decoder.sampleRate);
let source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);

现在,这种方法是有效的,因为我确实能听到声音,我能听到它们是正确的声音,但它们被奇怪地扭曲了。我要播放的声音文件示例是https://cardgames.io/mahjong/sounds/selecttile.mp3

你知道这是怎么回事吗?或者如何将从.decode()函数正确返回的单个PCM数组缓冲区转换为正确播放所需的格式?

上面fdcpp链接的例子表明,可以使用decoder.decode()返回的ArrayBuffer将其写入WAV文件,而无需进一步修改。这意味着数据必须是交错的PCM数据。

因此,当将数据转换回浮点值时,应该可以工作。此外,它必须按照Web Audio API的要求放入平面数组中。

const interleavedPcmData = new DataView(pcmArrayBuffer);
const numberOfChannels = decoder.frame.header.numberOfChannels();
const audioBuffer = new AudioBuffer({
length: pcmArrayBuffer.byteLength / 2 / numberOfChannels,
numberOfChannels,
sampleRate: decoder.sampleRate
});
const planarChannelDatas = [];
for (let i = 0; i < numberOfChannels; i += 1) {
planarChannelDatas.push(audioBuffer.getChannelData(i));
}
for (let i = 0; i < interleavedPcmData.byteLength; i += 2) {
const channelNumber = i / 2 % numberOfChannels;
const value = interleavedPcmData.getInt16(i, true);
planarChannelDatas[channelNumber][Math.floor(i / 2 / numberOfChannels)]
= value < 0
? value / 32768
: value / 32767;
}