概要
本文主要讲解mp4中音频相关的处理方案,大致流程如下:
处理流程
mp4box解码
js
// 请求视频资源
function queryVideo() {
// fetch到的是mp4刻度流,包含视频,音频等多个track
fetch(videoUrl.value)
.then(res => res.arrayBuffer())
.then(buffer => {
videoBuffer = buffer;
buffer.fileStart = 0;
mp4boxfile.appendBuffer(buffer);
// flush后即可触发onReady事件
mp4boxfile.flush();
})
}
function initMP4Box() {
mp4boxfile = MP4Box.createFile();
mp4boxfile.onReady = async (info) => {
audioTrack = info.audioTracks?.[0];
// 播放音频
await getPcmData();
startPlayAudio();
}
}
和处理视频时的第一步一致,直接用Mp4Box
解析出视频容器中的音频轨道即可。
这时候可以拿到音频的取样率(sampleRate)
以及频道数(channelNum)
,为后续解码做准备。
decodeAudioData解码
js
// 开始获取音频Sample
async function getPcmData() {
if(!audioContext) {
audioContext = new AudioContext({
sampleRate: audioTrack.audio.sample_rate
});
}
audioPcm = extractPCM4AudioBuffer(await audioContext.decodeAudioData(videoBuffer));
}
/**
* 从 AudioBuffer 中提取 PCM
*/
function extractPCM4AudioBuffer(ab) {
return Array(ab.numberOfChannels)
.fill(0)
.map((_, idx) => {
return ab.getChannelData(idx);
});
}
用decodeAudioData
配合刚刚获取到的sampleRate
即可对视频数据进行解码。(直接将视频的ArrayBuffer喂给decodeAudioData
就行,甚至不需要先拆分出音频)
播放
js
let frameOffset = 0;
let endIdx = 0;
function tick(deltaTime, chan0, chan1) {
// 这个方法有待商榷,不知move_duration是否确定为ms
if(frameOffset >= audioTrack.movie_duration / 1000) {
return { audio: [], state: "done"};
}
const frameCnt = Math.ceil(
deltaTime * audioTrack.audio.sample_rate,
);
const audio = [
ringSliceFloat32Array(chan0, endIdx, endIdx + frameCnt),
ringSliceFloat32Array(chan1, endIdx, endIdx + frameCnt),
];
endIdx = endIdx + frameCnt;
frameOffset += deltaTime;
return { audio, state: "success"};
}
/**
* 循环 即 环形取值,主要用于截取 PCM
*/
function ringSliceFloat32Array(
data,
start,
end
) {
const cnt = end - start;
const rs = new Float32Array(cnt);
let i = 0;
while (i < cnt) {
rs[i] = data[(start + i) % data.length];
i += 1;
}
return rs;
}
// 开始播放音频
function startPlayAudio() {
// 每次取1s的数据
const duration = 0.1;
let startAt = 0;
const ctx = new AudioContext();
function play() {
console.log("play audio");
const {audio, state} = tick(duration, audioPcm[0], audioPcm[1]);
if(state === "done") {
return;
}
const [chan0, chan1] = audio;
const buf = ctx.createBuffer(audioTrack.audio.channel_count, chan0?.length || 0, audioTrack.audio.sample_rate);
buf.copyToChannel(chan0, 0);
buf.copyToChannel(chan1, 1);
const source = ctx.createBufferSource();
source.buffer = buf;
source.connect(ctx.destination);
startAt = Math.max(ctx.currentTime, startAt);
source.start(startAt);
startAt += buf.duration;
play()
}
play()
}
播放的关键点主要就是分片
- tick方法将pcm数据进行分段。因为之前得到了音频的
sampleRate(取样率)
和采样大小
,所以可以直接用deltaTime * audioTrack.audio.sample_rate
获取特定时长的pcm数据。 - tick分片后,对这些pcm片段创建多个source进行分片播放,这样就不需要等待所有音频处理完成后再播放。
思考
现在可以使用pcm数据进行直接播放了,对比浏览器提供的video标签,有什么收益呢?
音量调节
音量调节的原理就是调节声音的振幅
我们可以看到,最终拿到的pcm数据如下:
只要将这些数据乘以想要调节的倍数,就可以做对应大小调节了,非常方便。
倍速播放
倍速播放的原理就是缩短每个音频片段的播放时长
只需要在上述地方做改动就行(如果不适用tick分片,直接播放一整段片段就不太好实现了)
声道处理
声道处理的原理就是处理单个声道的pcm数据
由于我们之前已经分别拿到了两条声道的数据,就可以很方便的处理单个声道的数据啦。
例如我们只需要一条声道的数据,直接注释一行代码就行,这时候耳机里就只有一边有声音了。