前言
我以前从来没接触过音频操作,甚至压根不知道这个api,然后新公司里面要做音频编辑,声纹之类啥的,用wavesurfer.js可视化,编辑音频复制粘贴剪切删除等等,这几天一直在怼这俩个,网上百度资料感觉也好少,我现在也只是会了个皮毛,简单记录下,如果对webaudio完全没了解过的,建议先看看前面api的介绍,如果以前有弄过或者看过一点点的可以直接跳到后面的例子。
使用wavesurfer.js配合webaudio api实现音频波形可视化,简单编辑操作(复制、粘贴、剪切、删除、插入、撤销、重做),可以参考这篇,我有点菜,有什么写的不好的地方请大家多多指点下谢谢!
AudioContext
AudioContext接口表示由链接在一起的音频模块构建的音频处理图,每个模块由一个AudioNode表示。音频上下文控制它包含的节点的创建和音频处理或解码的执行。
AudioContext()创建并返回一个新的 AudioContext 对象。参数可选
let audioCtx = new AudioContext(options)
音频节点分三大类:
- Source Node:能产生音频的节点,只有输出,没有输入。
- Process Node:对音频进行处理的节点,有输入(可能有多个)和输出。
- Destination Node:通常为音频播放设备,如扬声器。
AudioDestinationNode
AudioDestinationNode接口代表给定上下文中音频图的最终目的地——通常是设备的扬声器。
AudioContext.createBuffer()
createBuffer()用于新建一个空白的 AudioBuffer 对象 ,以便用于填充数据,通过AudioBufferSourceNode 播放 。
参数依次为:声频声道数,样本帧数,采样率。返回值 AudioBuffer
let arrayBuffer = audioCtx.createBuffer(numOfChannels,length,sampleRate);
AudioContext.createBufferSource()
createBufferSource() 方法用于创建一个新的AudioBufferSourceNode接口,该接口可以通过AudioBuffer 对象来播放音频数据。
创建一个 2 秒的缓冲器,并用白噪音填充它,然后通过AudioBufferSourceNode来播放它
// 创建音频上下文
let audioCtx = new AudioContext();
// 立体声
let channels = 2;
// 创建一个 采样率与音频环境 (AudioContext) 相同的 时长 2 秒的 音频片段。
let frameCount = audioCtx.sampleRate * 2.0;
// 创建空白的AudioBuffer对象
let myArrayBuffer = audioCtx.createBuffer(channels,frameCount,audioCtx.sampleRate);
function fn(){
// 使用白噪声填充;
// 就是 -1.0 到 1.0 之间的随机数
for (let channel = 0; channel < channels; channel++) {
// 这允许我们读取实际音频片段 (AudioBuffer) 中包含的数据
let nowBuffering = myArrayBuffer.getChannelData(channel);
for (let i = 0; i < frameCount; i++) {
// Math.random() is in [0; 1.0]
// audio needs to be in [-1.0; 1.0]
nowBuffering[i] = Math.random() * 2 - 1;
}
}
// 获取一个 音频片段源节点 (AudioBufferSourceNode)。
// 当我们想播放音频片段时,我们会用到这个源节点。
let source = audioCtx.createBufferSource();
// 把刚才生成的片段加入到 音频片段源节点 (AudioBufferSourceNode)。
source.buffer = myArrayBuffer;
// 把 音频片段源节点 (AudioBufferSourceNode) 连接到
// 音频环境 (AudioContext) 的终节点,这样我们就能听到声音了。
source.connect(audioCtx.destination);
// 开始播放声源
source.start();
}
fn();
AudioContext.createMediaElementSource()
createMediaElementSource() 方法用于创建一个新的 MediaElementAudioSourceNode 对象,输入audio/viedo 标签元素,对应的音频即可被播放或者修改。 网络加载的音频文件(比如audio标签) ,需要将其转换成音频源节点,才能连接到路由图中。
结合AudioDestinationNode的例子,使用到了GainNode 增益节点(表示音量的变化 )。
let myAudio = document.querySelector('#audio');
let sourceNode = audioCtx.createMediaElementSource(myAudio); // 从audio标签创建一个音频源节点
let gainNode = audioCtx.createGain(); // 创建一个增益节点
gainNode.gain.value = 0; // 将增益设置为0(相当于音量设置为0)
$audio.addEventListener('play', () => {
// 在1秒的时间内指数增长到1,实现一个播放渐入效果
gainNode.gain.exponentialRampToValueAtTime(1, 1);
});
sourceNode.connect(gainNode); // 音频源节点连接到增益节点
gainNode.connect(audioCtx.destination); // 增益节点连接到destination进行播放
AudioContext.decodeAudioData()
decodeAudioData()方法可用于异步解码音频文件中的 ArrayBuffer 。
参数依次为:请求返回的音频流audioData,解码成功后的回调函数(decodedData)。
// 旧语法
audioCtx.decodeAudioData(audioData, function(decodedData) {});
// 新语法
audioCtx.decodeAudioData(audioData).then(function(decodedData) {});
AudioBuffer
AudioBuffer接口表示存在内存里的一段短小的音频资源,利用AudioContext.decodeAudioData()方法从一个音频文件构建,或者利用 AudioContext.createBuffer()从原始数据构建。把音频放入 AudioBuffer 后,可以传入到一个 AudioBufferSourceNode进行播放。
属性
AudioBuffer.sampleRate 只读
存储在缓存区的 PCM 数据的采样率:浮点数,单位为 sample/s。
AudioBuffer.length 只读
返回存储在缓存区的 PCM 数据的采样帧数:整形。
AudioBuffer.duration 只读
返回存储在缓存区的 PCM 数据的时长:双精度型(单位为秒)。
AudioBuffer.numberOfChannels 只读
返回存储在缓存区的 PCM 数据的声道数:整形。
方法
AudioBuffer.getChannelData()
getChannelData() 方法返回一Float32Array ,获取指定声道数据。
参数channel 属性是要获取特定声道数据的索引。0 代表第一个声道。 如果索引值大于或等于AudioBuffer.numberOfChannels, 会抛出一个索引大小异常(INDEX_SIZE_ERR )的错误。
let myArrayBuffer = audioCtx.createBuffer(声频声道数,样本帧数,采样率);
let nowBuffering = myArrayBuffer.getChannelData(channel);
AudioBuffer.copyFromChannel()
copyFromChannel() 方法将样本从 AudioBuffer 的指定声道复制到目标数组中
参数依次为:目标数组,声道索引,被复制的声道数据(Float32Array)的起始索引(从channel的哪个位置开始复制到目标数组)
myArrayBuffer.copyFromChannel(destination,channelNumber,startInChannel);
AudioBuffer.copyToChannel()
copyToChannel() 方法将样本从源数组AudioBuffer 复制到指定声道。
参数依次为:目标数组,声道索引,被复制的声道数据(Float32Array)的起始索引(从源数组要复制到AudioBuffer的哪个位置)
AudioBuffer.copyToChannel(source, channelNumber, startInChannel)
例子
使用以上几个方法实现复制,这里我们拿input文件选择框选中的文件结合FileReader举例
复制
let inp = document.getElementById('input'); // 获取input节点
inp.onchange = e => {
let file = e.target.files[0]; // 获取选中的音频文件
let reader = new FileReader();// FileReader 异步读取存储在用户计算机上的文件
reader.onload = async event => { // 读取完成
let audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // 创建音频上下文
let arrayBuffer = event.target.result;// result是被读取文件的 ArrayBuffer 数据对象
// audioBuffer 就是要被我们复制的源数据了
let audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);// 解码 arrayBuffer 为 audioBuffer
let channels = audioBuffer.numberOfChannels; // 源数据的声道数
let rate = audioBuffer.sampleRate; // 源数据的采样率
let start = 5 ; // 从第几秒开始复制
let end = 10 ; // 复制到第几秒结束
// 这里的 >> 0 在这里 你们就理解成和向下取整差不多意思就可以了(具体我也不太懂)
let startOffset = (start * rate) >> 0; // 起始位置 = 开始时间 * 采样率
let endOffset = (end * rate) >> 0; // 结束位置 = 结束时间 * 采样率
let frameCount = endOffset - startOffset; // 音频帧数/长度 = 结束位置 - 起始位置
// 创建同样采用率、同样声道数量,指定长度的空的AudioBuffer
let newAudioBuffer = audioCtx.createBuffer(channels, frameCount, rate);
// 以上是前置条件,下面是执行复制的代码
...
...
...
};
reader.readAsArrayBuffer(file); // 按字节读取文件内容,并转换ArrayBuffer对象
}
方式一 使用getChannelData 通过数组截取实现
// 遍历声道
for (let i = 0; i < channels; i++) {
// 截取 audioBuffer 指定索引段的数据 赋值给 newAudioBuffer
newAudioBuffer.getChannelData(i).set(audioBuffer.getChannelData(i).slice(startOffset, endOffset));
}
方式二 通过copyFromChannel和copyToChannel实现复制
// 创建临时的Array存放要复制的buffer数据
let tempArray = new Float32Array(frameCount);
// 遍历声道
for (let i = 0; i < channels; i++) {
// 把数据从 audioBuffer 复制到 tempArray,startOffset 是你要从哪里开始截取
audioBuffer.copyFromChannel(tempArray, i, startOffset);
// 把数据从 tempArray 取出来复制到 newAudioBuffer,如果你要做的是插入,这里的0替换成你要插入的数组的索引
newAudioBuffer.copyToChannel(tempArray, i, 0);
}
最后可以播放音频测试下复制效果
// 创建AudioBufferSourceNode对象
let source = audioCtx.createBufferSource();
source.buffer = newAudioBuffer;
// 连接音频播放设备
source.connect(audioCtx.destination);
// 资源开始播放
source.start();
粘贴
粘贴的数据会覆盖原来位置那部分的数据,假设copyData是我们已经复制的数据,audioBuffer是源音频数据,currentTime是我们要粘贴到音频的指定位置(S)
let copyData = newAudioBuffer; // 复制的数据
let currentTime = 10; // 我们要把复制的数据粘贴到源音频第10秒的位置
let channels = audioBuffer.numberOfChannels; // 声道数
let rate = audioBuffer.sampleRate; // 采样率
let duration = audioBuffer.duration; // 音频时长
let copyToStart = (currentTime * rate) >> 0; // 要复制到的目标的开始位置
let copyToEnd = copyToStart + copyData.length; // 要复制到的目标的结束位置 = 开始位置 + 复制的音频长度
// 音频帧数差值 = ( 复制的音频长度 - ( 源音频长度 - 当前光标所在位置为止长度 ) ) * 采样率
let diffLength = copyData.length - (audioBuffer.length - copyToStart);
// 音频时长如果大于 当前光标时间点 + 复制的音频时长,音频帧数则同等于源音频帧数,否则等于 源音频帧数 + 音频帧数差值
let frameCount = duration > currentTime + copyDuration ? audioBuffer.length : audioBuffer.length + diffLength;
// 创建同样同样声道数、采样率,指定长度的空的AudioBuffer
let newBuffer = ac.createBuffer(channels, frameCount, rate);
for (let i = 0; i < channels; i++) {
let before = audioBuffer.getChannelData(i).slice(0, copyToStart); // 复制的音频要插入的时间点的前面部分
let add = copyData.getChannelData(i).slice(0, copyData.length); // 复制的音频
let after = audioBuffer.getChannelData(i).slice(copyToEnd); // 复制的音频要插入的时间点的后面部分
// 拼接数据 这个newBuffer就是粘贴完成后的新数据
newBuffer.getChannelData(i).set([...before, ...add, ...after]);
}
插入
插入的数据不会覆盖原来位置那部分的数据,同上 audioBuffer 为源音频数据
let copyData = newAudioBuffer; // 复制的数据
let currentTime = 10; // 我们要把复制的数据粘贴到源音频第10秒的位置
let channels = audioBuffer.numberOfChannels; // 声道数
let rate = audioBuffer.sampleRate; // 采样率
let copyToStart = (currentTime * rate) >> 0; // 要复制到的目标的开始位置
// 音频帧数等于 源音频长度 + 复制音频长度
let frameCount = audioBuffer.length + copyData.length;
// 创建同样采样率、同样声道数量,指定长度的空的AudioBuffer
let newBuffer = ac.createBuffer(channels, frameCount, rate);
for (let i = 0; i < channels; i++) {
let before = audioBuffer.getChannelData(i).slice(0, copyToStart); // 复制的音频要插入的时间点的前面部分
let add = copyData.getChannelData(i).slice(0, copyData.length); // 复制的音频
let after = audioBuffer.getChannelData(i).slice(copyToStart); // 复制的音频要插入的时间点的后面部分
// 拼接数据 这个newBuffer就是插入完成后的新数据
newBuffer.getChannelData(i).set([...before, ...add, ...after]);
}
剪切
剪切就是复制数据,然后把复制的部分截掉,先执行复制操作,然后获取复制数据的长度,同上 audioBuffer 为源音频数据,copyData是已经复制的数据
let copyLength = copyData.copyLength;
let start = 5 ; // 从第几秒开始剪切
let end = 10 ; // 剪切到第几秒
let cutStart = (start * rate) >> 0; // 要剪切的目标的开始位置
let cutEnd = (end * rate) >> 0; // 要剪切的目标的结束位置
// 音频帧数 = 源音频长度 - 复制的音频数据长度
let frameCount = audioBuffer.length - copyLength;
// 创建同样采样率、同样声道数量,指定长度的空的AudioBuffer
let newBuffer = ac.createBuffer(channels, frameCount, rate);
for (let i = 0; i < channels; i++) {
let before = audioBuffer.getChannelData(i).slice(0, cutStart); // 复制数据 前面部分
let after = audioBuffer.getChannelData(i).slice(cutEnd); // 复制数据 后面部分
// 截掉复制的数据后,拼接数据前后部分
newBuffer.getChannelData(i).set([...before, ...after]);
}
删除
删除和剪切操作一样,区别只是不复制数据,不做演示,参考剪切。
撤销和重做
撤销和重做其实就是用一个数组arr记录(push)每次操作后的newBuffer,用index记录下标,撤销就是index--,重做就是index++,然后获取数组指定下标的数据,再判断一下边界就好了,index>0 才执行撤销,index<数组长度-1 才执行重做, 如果撤销之后又执行了其他操作,直接截取index前面的部分 arr = arr.slice(0, index+ 1) ,数组后面数据被新操作覆盖了。