前一阵子一直在作CodeasilyX这个项目的音频处理的工做,因为对音频信号处理方面的技术欠缺,花了大量的时间在这个项目核心功能无关的音频功能上,但收获不少,学习到了不少音频处理的技术实现,因此在这里作个记录。javascript
一段声音能够理解为多种频率正弦波的叠加,而音调就是一段声音的主要频率。改变了主要频率,就是改变了音调——提升了主要频率,就是升调,反之亦然。html
一开始我找了不少文章,大多都是理论,我没有学过音频处理算法根本看不懂,幸亏后来找到了这一篇HTML5调用摄像头+视频特效+录制视频+录音+截图+变声+滤波+音频可视化,里面提到了变声的实现,他的实现方式就是这句代码outputBuffer[sample]=inputBuffer[sample/2]
,这样就能够获得一个变得浑厚的声音,虽然这么一来音频的时长会出现问题,须要更合理的算法来处理这些buffer的对应关系。但正是由于这句代码我才明白了声音的变音各类算法跟程序的二进制处理之间的关系是怎样对应起来的,打开了我对音频处理的程序实现的一扇大门,java
下面是我推断出来的正弦波与buffer的关系大概就是下面这两张图的对比:node
上图的buffer数据是我录音获得的Float32Array数组,值区间为[-1,1],而恰巧正弦波的值区间也是[-1,1],这是否是就说明了正弦波的值就对应了音频buffer的值了?而调整这些buffer的值就等因而调整了正选波的值和频率?git
我想实现把音调变高,像小黄人的声音那样,按道理就是outputBuffer[sample]=inputBuffer[sample*2]
就可让音调变高,可是这么作的话inputBuffer就不够数据分给outputBuffer了,就会致使outputBuffer音频时长少了一半,因此我本身研究怎么给outputBuffer插值,补足音频时长,但是无论怎么补,都会致使没有声音,真不知道浏览器怎么解析buffer放出声音的🤔,可能仍是须要依靠算法来实现。github
但是算法要涉及的知识实在太多了,感兴趣的话能够看一下这篇文章深刻浅出的讲解傅里叶变换(真正的通俗易懂),em..通俗易懂,我看到后面就愈来愈不懂了😰,也许数学物理专业或者通讯专业能看懂,但我一时半会学不来,留下了没技术的眼泪😭。算法
因此想本身实现等之后专门研究这方面在想吧。找找其余的方案。数组
后来我找到了SoundTouchJS这个开源项目,基本能够实现我想要的变音功能,还能保持音频时长,一来解决了燃眉之急,二来还能够看源代码学习一下,美滋滋😁。浏览器
个人使用代码以下:bash
this.context.decodeAudioData(data, audioBuffer => {
this.shifter = new PitchShifter(this.context, audioBuffer, 4096, () => {
// 播放进度达到结尾时回调
this.playOver()
});
this.shifter.tempo = 1;
this.shifter.pitch = 1;
// 负数时声音变得浑厚
this.shifter.pitchSemitones = -1.8;
});
复制代码
可是SoundTouchJS的播放形式是用AudioNode链接扬声器,流式播放的。
这里我要说明一下,通常网页里播放音频有两种方式:
- 第一种是咱们常见的用Audio标签或者new Audio来加载一个音频文件,再控制
AudioElement.play()
进行播放的;- 第二种就是经过
this.context.createScriptProcessor
监听onaudioprocess
流式的取到源数据,给outputBuffer
赋值,最后AudioNode.connect(AudioContext.destination)
链接扬声器进行音频播放,该方法提供更高级更底层的能力控制音频输出,SoundTouchJS就是用的这种方法,能够作到播放过程当中随时变音,这是比Audio标签加载文件直接播放更直观的区别。
可是我以为这种流式播放的方式在作暂停、中止方面的操做时常常会卡音(就像磁带播放器卡带的状况),多是由于流式取元数据有规定每次的块大小,块太大可能容易卡音,块过小则处理的次数会更频繁影响性能。并且每次暂停、中止、播放的操做都是要跟扬声器进行connect
和disconnect
的操做,也决定了onaudioprocess
的触发时机,以为挺容易出bug的。
因此我以为这种流式播放的形式仍是比较适合相似直播的场景或者本身调试变音的场景,不太适合录播的时候常常暂停或拖动进度条播放的状况。
因此我又要想办法把SoundTouchJS里onaudioprocess
控制的outputBuffer
存起来,再编码成文件。
上面提到的把SoundTouchJS里onaudioprocess
控制的outputBuffer
存起来,再编码成文件,首先要找到soundtouchjs里的createScriptProcessor
的地方,源码以下:
const getWebAudioNode = function( context, filter, sourcePositionCallback = noop, bufferSize = 4096 ) {
const node = context.createScriptProcessor(bufferSize, 2, 2);
const samples = new Float32Array(bufferSize * 2);
node.onaudioprocess = event => {
let left = event.outputBuffer.getChannelData(0);
let right = event.outputBuffer.getChannelData(1);
let framesExtracted = filter.extract(samples, bufferSize);
sourcePositionCallback(filter.sourcePosition);
if (framesExtracted === 0) {
filter.onEnd();
return node;
}
let i = 0;
for (; i < framesExtracted; i++) {
left[i] = samples[i * 2];
right[i] = samples[i * 2 + 1];
}
};
return node;
};
export default getWebAudioNode;
复制代码
关键的就是这一句
let framesExtracted = filter.extract(samples, bufferSize);
复制代码
它会把以前对源数据滤波以后的数据在onaudioprocess的时候作分割,而后赋值给outputBuffer,那我要的把就是这些一段一段的分割的数据给存起来,拿到数据就能够一次性编码文件了,这句代码如今放在onaudioprocess里就会按采样率每4096为一次得一次块,而所有存完须要花的时间正是音频的时长,这显然是不太符合我要编码文件的需求的,因此我不能在onaudioprocess里存数据,个人思路就是直接拿这个方法(filter.extract(samples, bufferSize)
)调用作递归,依然是每4096分割一块,而后存起来,代码以下:
filterBuffer() {
// 传入所有的buffer进行滤波
const bufferSize = 4096;
const data = new Float32Array(this._buffer.length * 2);
var offset = 0;
while(this._buffer.length * 2 - offset > bufferSize*2) {
const samples = new Float32Array(bufferSize*2);
this._filter.extract(samples, bufferSize);
data.set(samples, offset+=bufferSize*2)
}
// 抽出单声道
const singleSamples = data.filter((item,index) => index%2 == 0);
return singleSamples;
}
复制代码
按道理这个方法是可行的,可结果却让人大跌眼镜,它合并出来的数据和在onaudioprocess里的数据居然是不同的,明明传入的是一样的参数,只是一个是按采样率调用,一个是单线程递归出来,最后我也没找到解决办法,只能继续沿用流式播放的形式,若是有大神能指出问题所在,本人定当感激涕零🙏
SoundTouchJS的源代码,目录结构以下:
看了下源码,好像也没有提到相关的正弦函数算法,具体实现原理还在研究,等研究出来了再写。
最后我就是用了soundtouchjs的方案了,整体来讲变音效果仍是不错的,就是我姿式水平还不够多,没能解读出变音的奥秘,只能徘徊在新手村用着大神们的开源项目了😂