前一陣子一直在作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的方案了,整體來講變音效果仍是不錯的,就是我姿式水平還不夠多,沒能解讀出變音的奧祕,只能徘徊在新手村用着大神們的開源項目了😂