5

JS纯前端实现audio音频剪裁剪切复制播放与上传

 3 years ago
source link: https://www.zhangxinxu.com/wordpress/2020/07/js-audio-clip-copy-upload/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=9505
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。

音频在线剪辑与处理

背景是这样的,用户上传音频文件,可能只需要几十秒就够了,但是常规的音乐都要3~5分钟,80%的流量都是不需要的,要是就这么传上去,其实是流量的浪费,如果可以在前端就进行剪裁,也就是只取前面一段时间的音频,岂不是可以给公司省很多流量费用,前端的业务价值就体现了。

关键如何实现呢?

下面,就以“截取用户上传音频前3秒内容”的需求示意下如何借助Web Audio API实现音频的部分复制与播放功能。

一、不哔哔,直接正题

实现步骤如下。

1. File对象转ArrayBuffer

在Web网页中,用户选择的文件是个file对象,我们可以将这个文件对象转换成Blob、ArrayBuffer或者Base64。

在音频处理这里,都是使用ArrayBuffer这个数据类型。

代码如下所示,假设file类型的文件选择框的id是'file'

file.onchange = function (event) {
    var file = event.target.files[0];
    // 开始识别
    var reader = new FileReader();
    reader.onload = function (event) {
        var arrBuffer = event.target.result;
        // arrBuffer就是包含音频数据的ArrayBuffer对象
    });
    reader.readAsArrayBuffer(file);
};

使用的是readAsArrayBuffer()方法,无论是MP3格式、OGG格式还是WAV格式,都可以转换成ArrayBuffer类型。

2. ArrayBuffer转AudioBuffer

这里的ArrayBuffer相对于把音频文件数组化了,大家可以理解为把音频文件分解成一段一段的,塞进了一个一个有地址的小屋子里,在计算机领域称为“缓冲区”,就是单词Buffer的意思。

所谓音频的剪裁,其实就是希望可以复制音频前面一段时间的内容。

但是问题来了,ArrayBuffer里面的数据并没有分类,统一分解了,想要准确提取某一截音频数据,提取不出来。

所以,才需要转换成AudioBuffer,纯粹的音频数据,方便提取。

AudioBuffer是一个仅仅包含音频数据的数据对象,是Web Audio API中的一个概念。

既然说到了Web Audio API,那我们就顺便……顺便……,想了想,还是不展开,因为太庞杂了,这Web Audio API至少比Web Animation API复杂了10倍,API之多,体量之大,世间罕见,想要完全吃透了,没有三年五载,啃不下来。

如果大家不是想要立志成为音视频处理专家,仅仅是临时解决一点小毛小病的问题,则不必深入,否则脑坑疼,使用MDN文档中的一些案例东拼西凑,基本的效果也能弄出来。

扯远了,回到这里。

AudioBuffer大家可以理解为音乐数据,那为什么叫AudioBuffer,不叫AudioData呢?

因为Buffer是个专有名词,直译为缓冲区,大家可以理解为高速公路,AudioBuffer处理数据更快,而且还有很多延伸的API,就像是高速公路上的服务区,有吃有喝还有加油的地方。

AudioData一看名字就是乡下土鳖,虽然接地气,但是,处理好几兆的数据的时候,就有些带不动了,就好像骑小电驴,在公速公路和乡道县道没多大区别,但是如果是开跑车,啧啧,乡下路就带不动了。

如何才能转换成AudioBuffer呢?

使用AudioContext对象的decodeAudioData()方法,代码如下:

var audioCtx = new AudioContext();

audioCtx.decodeAudioData(arrBuffer, function(audioBuffer) {
    // audioBuffer就是AudioBuffer
});

3. 复制AudioBuffer前3秒数据

AudioBuffer对象是一个音频专用Buffer对象,包含很多音频信息,包括:

  • 音频时长 duration
  • 声道数量 numberOfChannels
  • 采样率 sampleRate

包括一些音频声道数据处理方法,例如:

  • 获取通道数据 getChannelData()
  • 复制通道数据 copyFromChannel()
  • 写入通道数据 copyToChannel()

文档见这里:https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer

所以,实现的原理很简单,创建一个空的AudioBuffer,复制现有的通道数据前3秒的数据,然后复制的内容写入到这个空的AudioBuffer,于是我们就得到了一个剪裁后的音频Buffer数据了。

代码如下:

// 声道数量和采样率
var channels = audioBuffer.numberOfChannels;
var rate = audioBuffer.sampleRate;

// 截取前3秒
var startOffset = 0;
var endOffset = rate * 3;
// 3秒对应的帧数
var frameCount = endOffset - startOffset;

// 创建同样采用率、同样声道数量,长度是前3秒的空的AudioBuffer
var newAudioBuffer = new AudioContext().createBuffer(channels, endOffset - startOffset, rate);
// 创建临时的Array存放复制的buffer数据
var anotherArray = new Float32Array(frameCount);
// 声道的数据的复制和写入
var offset = 0;
for (var channel = 0; channel < channels; channel++) {
    audioBuffer.copyFromChannel(anotherArray, channel, startOffset);
    newAudioBuffer.copyToChannel(anotherArray, channel, offset);
}

// newAudioBuffer就是全新的复制的3秒长度的AudioBuffer对象

上面JavaScript代码中的变量newAudioBuffer就是全新的复制的3秒长度的AudioBuffer对象。

4. 使用newAudioBuffer做点什么?

其实应该是有了AudioBuffer对象后我们可以做点什么。

能做很多事情。

1) 如果希望直接播放

我们可以直接把AudioBuffer的数据作为音频数据进行播放

// 创建AudioBufferSourceNode对象
var source = audioCtx.createBufferSource();
// 设置AudioBufferSourceNode对象的buffer为复制的3秒AudioBuffer对象
source.buffer = newAudioBuffer;
// 这一句是必须的,表示结束,没有这一句没法播放,没有声音
// 这里直接结束,实际上可以对结束做一些特效处理
source.connect(audioCtx.destination);
// 资源开始播放
source.start();

2) 如果希望在<audio>元素中播放

这个还挺麻烦的。

<audio>的src属性获取音频资源,再进行处理是简单的,网上的案例也很多。

但是,想要处理后的AudioBuffer再变成src让<audio>元素播放,嘿嘿,就没那么容易了。

我找了一圈,没有看到Web Audio API中有专门的“逆转录”方法。

唯一可行的路数就是根据AudioBuffer数据,重新构建原始的音频数据。研究了一番,转成WAV格式相对容易,想要转换成MP3格式比较麻烦,这里有个项目:https://github.com/higuma/mp3-lame-encoder-js 不过自己没验证过,不过看代码量,还挺惊人的。

因此,我们的目标还是转到WAV音频文件生成上吧,下面这段方法是从网上找的AudioBuffer转WAV文件的方法,以Blob数据格式返回。

// Convert AudioBuffer to a Blob using WAVE representation
function bufferToWave(abuffer, len) {
    var numOfChan = abuffer.numberOfChannels,
    length = len * numOfChan * 2 + 44,
    buffer = new ArrayBuffer(length),
    view = new DataView(buffer),
    channels = [], i, sample,
    offset = 0,
    pos = 0;

    // write WAVE header
    // "RIFF"
    setUint32(0x46464952);
    // file length - 8                      
    setUint32(length - 8);
    // "WAVE"                     
    setUint32(0x45564157);
    // "fmt " chunk
    setUint32(0x20746d66);  
    // length = 16                       
    setUint32(16);  
    // PCM (uncompressed)                               
    setUint16(1); 
    setUint16(numOfChan);
    setUint32(abuffer.sampleRate);
    // avg. bytes/sec
    setUint32(abuffer.sampleRate * 2 * numOfChan);
    // block-align
    setUint16(numOfChan * 2);
    // 16-bit (hardcoded in this demo)
    setUint16(16);                           
    // "data" - chunk
    setUint32(0x61746164); 
    // chunk length                   
    setUint32(length - pos - 4);                   

    // write interleaved data
    for(i = 0; i < abuffer.numberOfChannels; i++)
        channels.push(abuffer.getChannelData(i));

    while(pos < length) {
         // interleave channels
        for(i = 0; i < numOfChan; i++) {
            // clamp
            sample = Math.max(-1, Math.min(1, channels[i][offset])); 
            // scale to 16-bit signed int
            sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; 
            // write 16-bit sample
            view.setInt16(pos, sample, true);          
            pos += 2;
        }
        // next source sample
        offset++                                     
    }

    // create Blob
    return new Blob([buffer], {type: "audio/wav"});

    function setUint16(data) {
        view.setUint16(pos, data, true);
        pos += 2;
    }

    function setUint32(data) {
        view.setUint32(pos, data, true);
        pos += 4;
    }
}

WAV格式的兼容性还是很6的,如下图所示:

wav兼容性

凡事支持Web Audio API的浏览器都支持WAV格式,所以,技术上完全可行。

下面这段JS可以得到剪裁后的WAV音频的Blob数据格式:

var blob = bufferToWave(newAudioBuffer, frameCount);

有了Blob数据,接下来事情就简单了。

我们可以直接把Blob数据转换成URL,可以使用URL.createObjectURL()生成一个Blob链接。

假设页面上有如下HTML代码:

<audio id="audio" controls=""></audio>

则如下设置,就可以点击上面的<audio>元素进行播放了。

audio.src = URL.createObjectURL(blob);

如果要转换成Base64地址,可以这么处理:

var reader2 = new FileReader();
reader2.onload = function(event){
    audio.src = event.target.result;
};
reader2.readAsDataURL(blob);

3) 如果希望上传剪裁的音频

有了Blob数据,上传还不是洒洒水的事情。

可以使用FormData进行传输,例如:

var formData = new FormData();
formData.append('audio', blob);
// 请求走起
var xhr = new XMLHttpRequest();
xhr.open('POST', this.cgiGetImg, true);
// 请求成功
xhr.onload = function () {
};
// 发送数据
xhr.send(formData);

有demo可以进行效果体验的,您可以狠狠地点击这里:用户上传的MP3音频剪裁并播放demo

使用截图示意如下:

声音检测与播放示意

二、不哔哔,直接结语

本文内容在圈子里算是稀缺的,文章质量还是不错的。

可惜,不太容易被用户发现。

即使有分享,也都是短时间的。

自己也没那么多精力去社交平台打理。

有些可惜。

可以找个人兼职打理自己的账号,做下类似运营的事情呢?1f914.svg

总之,感谢阅读,如果本文内容正好可以解决你的需求,记得在心里面点个赞!

1f618.svg

(本篇完)1f44d.svg 是不是学到了很多?可以分享到微信
1f44a.svg 有话要说?点击这里


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK