使用webCodec完成视频播放器(2)-音频数据播放

概要

本文主要讲解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()
}

播放的关键点主要就是分片

  1. tick方法将pcm数据进行分段。因为之前得到了音频的sampleRate(取样率)采样大小,所以可以直接用deltaTime * audioTrack.audio.sample_rate获取特定时长的pcm数据。
  2. tick分片后,对这些pcm片段创建多个source进行分片播放,这样就不需要等待所有音频处理完成后再播放。

思考

现在可以使用pcm数据进行直接播放了,对比浏览器提供的video标签,有什么收益呢?

音量调节

音量调节的原理就是调节声音的振幅

我们可以看到,最终拿到的pcm数据如下:

只要将这些数据乘以想要调节的倍数,就可以做对应大小调节了,非常方便。

倍速播放

倍速播放的原理就是缩短每个音频片段的播放时长

只需要在上述地方做改动就行(如果不适用tick分片,直接播放一整段片段就不太好实现了)

声道处理

声道处理的原理就是处理单个声道的pcm数据

由于我们之前已经分别拿到了两条声道的数据,就可以很方便的处理单个声道的数据啦。

例如我们只需要一条声道的数据,直接注释一行代码就行,这时候耳机里就只有一边有声音了。

相关推荐
牛奶几秒前
为什么敲几个字母就能访问网站?DNS原理大揭秘
前端·http·dns
wuhen_n3 分钟前
破冰——建立我们的AI开发实验环境
前端·javascript
HelloReader7 分钟前
Flutter 自适应布局一套代码适配手机和平板(十二)
前端
牛奶10 分钟前
HTTP裸奔,HTTPS穿盔甲——它们有什么区别?
前端·http·https
梓言12 分钟前
tailwindcss构建执行npm exec tailwindcss init -p 报错
前端
哈罗哈皮13 分钟前
龙虾(openclaw)本地快速安装及使用教程
前端·aigc·ai编程
用户231154445305814 分钟前
React中实现“双向绑定”效果的几种方式
前端
HelloReader15 分钟前
Flutter Sliver 高级滚动打造 iOS 通讯录体验(十三)
前端
a1117761 小时前
程序化几何背景生成器(html 开源)
前端·开源·html
浮笙若有梦1 小时前
我开源了一个比 Ant Design Table 更好用的高性能虚拟表格
前端·vue.js