WebGL是一個不錯的API。但不是一個很好的API,這是因為OpenGL不是一個很好的API。WebGL提供了對GPU的原始訪問,但非常低級。對于那些被如此低層次的東西所嚇倒的人來說,這也有相當多的高級引擎,比如three.js和Unity更容易使用。它們是很好的API,擁有強大的功能,它是我們在網絡上使用GPU的佳簡便抽象的好方法。
HTML5 Canvas是一個相當不錯的API。但它也有很多缺點:例如沒有色彩空間,如果你不花費很大精力將它移植到SVG上,你就不能直接將DOM元素畫在畫布上,模糊被用戶隱藏到“陰影”API和其他一些內容中。但它確實是繪制2D圖形的一個很好的方法。
相反,Web Audio是一個我不明白的API。 Web Audio的范圍絕對是巨大的,我無法想象任何人使用這功能,絕對昂貴的核心抽象和基本功能的缺失。引用規格本身:“這個規范的目標是包括現代游戲音頻引擎中發現的功能以及現代桌面音頻制作應用中的一些混合,處理和過濾任務。”
我無法想象任何想要使用Web Audio的任何高級功能的游戲引擎或音樂制作應用程序。像DynamicsCompressorNode這樣的東西實際上就是一個笑話:基本上缺少真正的壓縮器的基本功能,而且這個行為是不合適的,所以我甚至不敢相信它在瀏覽器之間是正確的。這樣的過濾器可能會使用asm.js或WebAssembly進行編寫,或者由于DSP的無輸入,輸入/輸出性質而被運行為Web Workers。像這樣的數學和緊湊的循環并不難,但它們不是火箭科學。這是確保正確行為的方法。
對于那些想做這樣的事情的人:計算我們的音頻樣本,然后再播放它,那么API使它幾乎不可能以任何性能的方式進行。
對于那些用傳統的聲音API進行音頻編程的人來說,你將有一個充滿了樣本的緩沖區。硬件揚聲器貫穿于這些樣品。當API認為它即將耗盡時,它就會進入程序并要求更多。這通常是通過叫做“環形緩沖區”的數據結構完成的,在這個結構中,我們讓揚聲器“追蹤”應用程序正在寫入緩沖區的樣本。“讀指針”和“寫指針”之間的差距是很重要的:如果系統過載,揚聲器就會耗盡,導致裂紋和其他工件,聲音太大,聲音會出現明顯的滯后。
還有一些細節,比如我們每秒鐘有多少個樣本,或者“采樣率”。現在,有兩種常用的采樣率:448000hz,現在大多數系統都使用它,44100Hz,雖然有點奇怪的數字,
但由于它在CD音頻中的使用而流行起來(CDDA為什么是44100Hz ?)因為索尼(Sony)是參與CD的組織之一,它在一個早期的數字音頻項目中,從u - matic磁帶中抄襲了CDDA。通常情況下,操作系統必須在運行時轉換為不同的采樣率或“重新采樣”音頻。
這是一個理論上的非web音頻API,計算和播放440Hz的正弦波。
const frequency = 440; // 440Hz A note. // 1 channel (mono), 44100Hz sample rate const stream = window.audio.newStream(1, 44100);
stream.onfillsamples = function(samples) { // The stream needs more samples! const startTime = stream.currentTime; // Time in seconds. for (var i = 0; i < samples.length; i++) { const t = startTime + (i / stream.sampleRate); // samples is an Int16Array samples[i] = Math.sin(t * frequency) * 0x7FFF;
}
};
stream.play();
然而,在Web音頻API中,上述情況幾乎是不可能的。這是我能做的接近的東西。
const frequency = 440; const ctx = new AudioContext(); // Buffer size of 4096, 0 input channels, 1 output channel. const scriptProcessorNode = ctx.createScriptProcessorNode(4096, 0, 1);
scriptProcessorNode.onaudioprocess = function(event) { const startTime = ctx.currentTime; const samples = event.outputBuffer.getChannelData(0); for (var i = 0; i < 4096; i++) { const t = startTime + (i / ctx.sampleRate); // samples is a Float32Array samples[i] = Math.sin(t * frequency);
}
}; // Route it to the main output. scriptProcessorNode.connect(ctx.destination);
似乎很相似,但有一些重要的區別。首先,這是不贊成的。是的。自2014年以來,音頻工作者已經棄用ScriptProcessorNode。順便說一下,音頻工作者不存在。它們在任何瀏覽器中實現之前,它們被AudioWorklet API所取代,后者在瀏覽器中沒有任何實現。
其次,整個上下文的采樣率是全局的。沒有辦法讓瀏覽器重新對動態生成的音頻進行重新采樣。盡管瀏覽器要求在C++中快速重新采樣代碼,但這并沒有暴露在ScriptProcessorNode的用戶中。一個音頻環境的采樣率并沒有被定義為44100Hz或48000Hz。它不僅依賴于瀏覽器,還依賴于設備的操作系統和硬件。連接到藍牙耳機可以使音頻環境的采樣率在沒有任何警告的情況下發生變化。
所以ScriptProcessorNode是不可以的。然而,有一個API允許我們提供一個不同的采樣緩沖區,并讓Web Audio API發揮它的作用。然而,這并不是一種“拉動式”的方法,瀏覽器每次都要獲取樣本,而是一種“推送”式的方法,在這種方法中,我們經常會播放音頻的新緩沖區。這就是所謂的BufferSourceNode,它是emscripten的SDL端口用來播放音頻的。(他們曾經使用ScriptProcessorNode,但隨后刪除了它,因為它不太好)
讓我們嘗試使用BufferSourceNode來播放我們的正弦波:
const frequency = 440; const ctx = new AudioContext(); let playTime = ctx.currentTime; function pumpAudio() { // The rough idea here is that we buffer audio roughly a // second ahead of schedule and rely on AudioContext's // internal timekeeping to keep it gapless. playTime is // the time in seconds that our stream is currently // buffered to. // Buffer up audio for roughly a second in advance. while (playTime - ctx.currentTime < 1) { // 1 channel, buffer size of 4096, at // a 48KHz sampling rate. const buffer = ctx.createBuffer(1, 4096, 48000); const samples = buffer.getChannelData(0); for (let i = 0; i < 4096; i++) { const t = playTime + Math.sin(i / 48000);
samples[i] = Math.sin(t * frequency);
} // Play the buffer at some time in the future. const bsn = ctx.createBufferSource();
bsn.buffer = buffer;
bsn.connect(ctx.destination); // When a buffer is done playing, try to queue up // some more audio. bsn.onended = function() { pumpAudio();
};
bsn.start(playTime); // Advance our expected time. // (samples) / (samples per second) = seconds playTime += 4096 / 48000;
}
}
pumpAudio();
這里有一些不好的消息。首先,我們基本上依靠浮點計時,在幾秒鐘內可以保持播放時間一致,無縫隙。但沒有辦法重置音頻上下文的當前時間來構建一個新版本,因此如果有人想要構建一個能夠存活數天的專業數字音頻工作站,那么浮點精度損失將成為一個大問題。
其次,這也是ScriptProcessorNode的一個問題,示例數組中充滿了浮點數。但是強迫每個人使用浮點數是很慢的。16位對于每個人來說都足夠了,而且對于輸出格式來說,它也已經足夠了。整數運算單位是非常快的,沒有什么大的理由將它們擺脫方程式。你可以將代碼從一個浮點數轉換為一個用于終輸出的int16,但是一旦某事物處于浮動狀態,它將永遠是緩慢的。
第三,重要的是,我們為每個音頻樣本分配兩個新對象!每個緩沖區大約85毫秒,所以每85毫秒,我們分配兩個新的GC’d對象。如果我們可以使用我們切割的現有的大型ArrayBuffer,可以減輕這一點,但是我們不能提供我們自己的ArrayBuffer:讓createBuffer為我們請求的每個通道創建一個緩沖區。你可能會想象,你可以使用一個非常大的空間來創建緩沖區,并只在BufferSourceNode中使用很小的一部分,但是沒有辦法對AudioBuffer對象進行切片,也沒有辦法在AudioBufferSourceNode對應的對象中指定一個偏移量。
您可能會認為好的解決方案是簡單地保留一個緩沖池對象池,并在它們完成之后對它們進行回收,但BufferSourceNode設計為一次性使用,即時消失的API。該文檔還很好地說明了它們“創建簡單”,而且“在適當的時候會自動收集垃圾信息”。
我知道我在這里打了一場艱苦的戰斗,但我們在實時音頻播放期間不需要GC。
根據Chrome分析器,盡管在我自己的測試應用程序中,在一個主要的GC擦除之前,我仍然會看到緩慢增長到12MB。
更諷刺的是,Mozilla提出了一個非常類似的API,叫做音頻數據API。它有三個函數:它有三個功能:setup(),currentSampleOffset()和writeAudio()。它仍然是一個push API,不是一個pull API,但是它非常簡單,在運行時支持重新采樣,不需要您將其分解為GC緩沖區,并且沒有任何內容。
規格和庫不能在憑空創建。如果我們把簡單的接口放在那里,然后讓人們在JavaScript(重采樣,FFT)中實現,并將它們放在C ++中,我相信我們會看到比今天更多的增長和使用。而且我們將有這個API的實際用戶,以及在生產中使用它的用戶的真實世界反饋。相反,Web Audio的大用戶現在似乎是emscripten,他們顯然不會關心任何圖形路由的無稽之談,并且已經嘗試解決可怕的API本身。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。