WebAudio指紋概述
瀏覽器中的 WebAudio API 提供了豐富的功能,其中包括了大量生成和處理音頻數據的API。WebAudio API 的音頻指紋技術是一種利用音頻信號的特征來唯一標識音頻的技術。因為WebAudio API 提供了豐富的音頻處理功能,包括合成、過濾、分析等,通過一系列功能組合,可以使生成的音頻指紋具有一定的獨特性。生成音頻指紋的過程通常涉及到對音頻數據進行數學運算或特征提取,這使得生成的指紋在一定程度上是穩定的,即使音頻本身有一定的變化也不會影響其唯一性。而且Web Audio API 提供了實時處理音頻數據的能力,因此可以用于實時生成和識別音頻指紋。
WebAudio API主要用在AudioContext上下文中,在進行任何其他操作之前,始終需要創建一個AudioContext的實例,如圖4-7所示。
在有了音頻來源之后,通過節點壓縮,就可以得到Buffer輸出了。在實際的WebAudio API 操作過程中,通用的做法是創建單個AudioContext實例,并在所有后續處理中重復使用。每個AudioContext實例都具有一個目標屬性,該屬性用于表示該上下文中所有音頻的目標。此外,還存在一種特殊類型的AudioContext,即OfflineAudioContext。在獲取音頻指紋的時候,通常是會創建OfflineAudioContext,主要原因在于它不會將音頻呈現給設備硬件,而是快速生成音頻并將其保存到AudioBuffer中。因此,OfflineAudioContext的目標是內存中的數據結構,而常規的AudioContext的目標是音頻設備。
創建OfflineAudioContext實例時,需要傳遞三個參數:
- 通道數:通道數表示音頻數據中的聲道數量。在數字音頻中,通常有單聲道和立體聲兩種通道配置。單聲道通常用于單一音頻源,而立體聲則包含左右兩個獨立的聲道,用于模擬左右聲源的位置和方向。通道數還可以擴展到更多聲道,如環繞聲,以提供更加沉浸式的音頻體驗。
- 樣本總數:樣本總數指的是音頻數據中包含的采樣數量。音頻數據是通過對連續時間信號進行采樣來獲取的,每個采樣點對應著一個特定時間點上的音頻振幅值。樣本總數決定了音頻的持續時間,即音頻的長度。采樣率和樣本總數共同決定了音頻的時長,樣本總數越多,音頻的時長就越長。
- 采樣率:采樣率表示每秒鐘對聲音信號進行采樣的次數,單位為赫茲Hz,采樣率決定了數字音頻的精度和質量,以及其能夠表示的頻率范圍。常見的標準采樣率包括44.1 kHz和48 kHz,它們通常用于CD音質和音頻制作。更高的采樣率,如96 kHz或192kHz可以提供更高的音頻質量和更廣的頻率響應范圍,但也會增加文件大小。
Oscillator振蕩器是一種產生周期性波形的電子設備或軟件組件。在音頻領域中,振蕩器通常用于生成聲音信號的基礎波形,例如正弦波、方波、鋸齒波等。這些基礎波形可以用于合成各種聲音,是合成器和音頻處理中的重要組件之一。振蕩器通過產生連續的電壓或數字信號來生成波形。在軟件中,振蕩器通常是由算法來模擬的,這些算法根據所需的波形形狀和參數生成連續的樣本值。在處理音頻時需要一個來源,振蕩器是一個很好的選擇,因為它是通過數學方法生成樣本的,可以生成具有指定頻率的周期波形。
壓縮節點是一種用于動態范圍壓縮的節點類型。它可以降低音頻信號的動態范圍,即減小最響亮部分與最安靜部分之間的差異,從而提高音頻的平均音量并減少峰值。壓縮節點通常用于音頻信號處理的動態范圍控制,以確保音頻在播放過程中的一致性和平衡。壓縮節點通常作為音頻處理圖中的一個節點,與其他節點,如聲音源、效果器等連接在一起,以對輸入信號進行壓縮處理。通過調整壓縮節點的參數,可以控制壓縮的程度和效果,從而實現對音頻信號動態范圍的調節和控制。
AudioBuffer是WebAudio API中表示音頻數據的數據結構。它用于存儲音頻樣本的實際數據,并提供了一組函數來訪問和操作這些數據。在WebAudio API中,AudioBuffer通常作為音頻源節點的輸入,用于播放音頻或將其傳遞給其他音頻處理節點進行進一步處理。AudioBuffer中包含音頻數據的實際樣本,這些樣本表示音頻波形在離散時間點上的振幅值。音頻數據可以來自于多種來源,例如從服務器加載、用戶錄制或通過WebAudio API生成。每個AudioBuffer實例都具有固定的采樣率和通道數。這些屬性在創建AudioBuffer時被指定,并決定了AudioBuffer中存儲的音頻數據的格式和結構。此外,AudioBuffer提供了一組函數來訪問和操作存儲的音頻數據。例如,可以使用getChannelData獲取特定通道的音頻數據,并使用set修改音頻數據的值。這些函數使得可以直接對音頻數據進行編輯和處理,例如音頻混合、剪輯、變速、變調等操作。AudioBuffer通常作為音頻源節點的輸入,用于播放音頻或將其傳遞給其他音頻處理節點進行進一步處理。通過創建AudioBufferSourceNode并將AudioBuffer作為其緩沖區傳遞給它,可以將AudioBuffer中存儲的音頻數據進行播放或傳遞給音頻圖中的其他節點。由于音頻數據以二進制格式存儲在AudioBuffer中,因此可以在Web Audio API中高效地進行音頻處理操作,而無需頻繁地將數據從JavaScript代碼中復制到Web Audio API中。
在WebAudio指紋的計算過程中,一般使用OfflineAudioContext上下文,接著設置特殊的振蕩器來作為音頻源,在經過壓縮節點操作之后,就可以使用getChannelData來獲取生成的AudioBuffer了,指紋即是通過對該Buffer的計算完成的。
let audioContext = new (window.OfflineAudioContext ||window.webkitOfflineAudioContext)(1, 44100, 44100);let outputValue;if (!audioContext) {outputValue = 0;} else {let oscillator = audioContext.createOscillator();oscillator.type = "triangle";oscillator.frequency.value = 10000;let compressor = audioContext.createDynamicsCompressor();if (compressor.threshold) compressor.threshold.value = -50;if (compressor.knee) compressor.knee.value = 40;if (compressor.ratio) compressor.ratio.value = 12;if (compressor.reduction) compressor.reduction.value = -20;if (compressor.attack) compressor.attack.value = 0;if (compressor.release) compressor.release.value = 0.25;oscillator.connect(compressor);compressor.connect(audioContext.destination);oscillator.start(0);audioContext.startRendering();audioContext.oncomplete = function(event) {outputValue = 0;let renderedBuffer = event.renderedBuffer.getChannelData(0);let bufferAsString = '';for (let i = 0; i < renderedBuffer.length; i++) {bufferAsString += renderedBuffer[i].toString();}let fullBufferHash = hash(bufferAsString);console.log('Full buffer hash: ' + fullBufferHash);for (let i = 4500; i < 5000; i++) {outputValue += Math.abs(renderedBuffer[i]);}console.log('Output value: ' + outputValue);compressor.disconnect();};}
前兩行代碼創建了一個OfflineAudioContext實例。使用離線音頻上下文允許程序處理和渲染音頻數據而不實時播放。使用window.OfflineAudioContext或window.webkitOfflineAudioContext確保代碼兼容不同的瀏覽器。使用createOscillator?創建一個振蕩器,用于生成音頻信號。使用createDynamicsCompressor?創建一個動態壓縮器節點,該節點用于減少音頻信號的動態范圍。然后將振蕩器連接到壓縮器,壓縮器再連接到音頻上下文的輸出
當渲染完成后,oncomplete 定義的函數被調用,getChannelData用于獲取渲染后的音頻數據,接著將音頻數據轉換為字符串,并計算其哈希值。代碼最后計算了特定樣本范圍內的數據的絕對值之和,這個數是音頻指紋常用的指紋數字。
WebAudio指紋獲取
本節會使用JavaScript腳本編寫一個獲取WebAudio音頻信息的腳本。這段代碼利用JavaScript的WebAudio API來生成和處理音頻數據,最終目的是生成一個音頻哈希值和一個從特定樣本范圍內計算得到的輸出值。以下是具體代碼:
從4.3.2的代碼中可以看出,音頻指紋的修改點很多,但是最終是使用getChannelData來獲取渲染后的音頻數據的,因此可以選擇該函數作為音頻指紋修改點。對其中的音頻數組進行遍歷噪聲,從而影響整個音頻。
WebAudio相關的Chromium源碼位于“src\third_party\blink\renderer\modules\webaudio”目錄之中,重點要修改的是AudioBuffer相關的,因此選擇其中的audio_buffer.cc文件作為指紋定制的文件。
getChannelData有兩個重載,具體代碼如下:
NotShared<DOMFloat32Array> AudioBuffer::getChannelData(unsigned channel_index,ExceptionState& exception_state) {if (channel_index >= channels_.size()) {exception_state.ThrowDOMException(DOMExceptionCode::kIndexSizeError,"channel index (" + String::Number(channel_index) +") exceeds number of channels (" +String::Number(channels_.size()) + ")");return NotShared<DOMFloat32Array>(nullptr);}return getChannelData(channel_index);}NotShared<DOMFloat32Array> AudioBuffer::getChannelData(unsigned channel_index) {if (channel_index >= channels_.size()) {return NotShared<DOMFloat32Array>(nullptr);}return NotShared<DOMFloat32Array>(channels_[channel_index].Get());}
從JavaScript代碼獲取音頻指紋的時候,只傳遞了一個參數,因此選擇第二個函數作為切入函數。該函數用于獲取音頻緩沖區中特定通道的數據。其中的函數簽名如下:
- NotShared<DOMFloat32Array>:返回類型是 NotShared 包裝的 DOMFloat32Array 對象。NotShared 是一種智能指針,表示該對象不應與其他對象共享。
- AudioBuffer:::表明該函數是 AudioBuffer 類的成員。
- getChannelData:接收一個無符號整數參數 channel_index,表示要獲取的數據通道索引。
然后檢查傳入的通道索引 channel_index?是否超出了音頻通道的數量。如果超出了有效范圍,表示請求的通道不存在。否則將獲取到的通道數據包裝成 DOMFloat32Array對象并返回。
由此可見,可以在最后的通道數據正式返回之前,將里邊的數據進行遍歷,挨個進行微調之后,從而完成音頻指紋定制:
??//ruyiconst base::CommandLine* ruyi_command_line =base::CommandLine::ForCurrentProcess();if (ruyi_command_line->HasSwitch(blink::switches::kRuyi)) {const std::string ruyi_fp =ruyi_command_line->GetSwitchValueASCII(blink::switches::kRuyi);absl::optional<base::Value> json_reader =base::JSONReader::Read(ruyi_fp);double webaudio_data =*(json_reader->GetDict().FindDouble("webaudio"));DOMFloat32Array* channels__ = channels_[channel_index].Get();size_t channel_size = channels__->length();for (size_t i = 0; i < channel_size; i++) {channels__->Data()[i] += (0.00001 * webaudio_data);}return NotShared<DOMFloat32Array>(channels__);}//ruyi end
修改的代碼從 JSON 中讀取一個名為 webaudio 的雙精度浮點數值,為了防止音頻數據修改過大,這里對音頻數據進行遍歷的時候,將該值按一定比例加到指定通道的音頻數據樣本上。
在修改完畢之后,可以到音頻檢測網站audiofingerprint.openwpm.com進行測試,傳遞不同參數,可以得到不同的指紋信息,如圖4-8所示: