大家好,我是若川。持續組織了8個月源碼共讀活動,感興趣的可以?點此加我微信ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北
籍前端群,可加我微信進群。

當你用網頁在視頻網站刷視頻的時候,有沒有碰到過一個 BGM 激起你內心的波瀾,而你卻不知道它的名字。此時只能打開手機進行聽歌識曲,而通過一個瀏覽器的插件卻更容易解決這個問題。不需要繁瑣的掏出手機,也不會因為需要外放而干擾他人,更不會因為環境噪音而識別困難。
如果你恰好也有這個需要,不妨試一下云音樂出品的 Chrome 瀏覽器插件「云音樂聽歌」[1],還可以直接進行紅心收藏哦。也可以到插件官網[2]預覽實際運行的效果。
背景
目前 Chrome 商店上存在的聽歌識曲插件,大都是國外出品,國內產品寥寥,對于國內音樂支持較差。既然云音樂有這個能力,我們希望將這樣的功能覆蓋每一個角落,傳遞音樂美好力量。與此同時市面上的插件大多還是基于 manifest v2 實現(相對于 manifest v3,安全性、性能、隱私性均較差),普遍的做法是將音頻錄制之后直接交給服務端,通過服務端進行指紋提取,徒增服務端計算壓力,增加網絡傳輸。那么有沒有辦法既能使用 manifest v3 協議進行功能實現,同時將音頻指紋提取這一計算放在前端呢?
Chrome瀏覽器插件新協議
本文的重心不在如何實現一個瀏覽器插件本身,如果你不了解插件本身的開發,可查閱 Google 官方的開發文檔[3]。
特別說明的是,manifest v2(MV2) 即將被廢棄,在 2022 年逐步不接受更新,2023 年將會逐步不能運行,本文所有的內容都是基于更安全、性能更好、隱私更強的 manifest v3(MV3)進行實現。
MV3 協議對插件實現核心影響點:
原有的 Background Page 使用 Service Worker 進行替代,這意味著在 Background Page 不再能進行 Web API 等操作。
遠程代碼托管不再支持,無法進行動態加載代碼,意味著可執行的代碼都需要直接打包到插件中。
內容安全策略調整,不再支持不安全代碼的直接執行。WASM 初始化相關函數無法直接運行。
聽歌識曲的實現
聽歌識曲本身技術比較成熟,整體的思路是通過音頻數字采樣,進行音頻指紋的提取,最后將指紋在數據庫進行匹配,特征值最高的即是所認為識別到的歌曲。
瀏覽器插件中的音頻提取
利用插件進行網頁內的音視頻錄制其實非常簡單,只需要 chrome.tabCapture
API 即可實現網頁本身的音頻錄制,獲取到的流數據我們需要針對音頻數據進行采樣,保證計算 HASH 的規則和數據庫數據保持一致。
針對獲取的 stream 流可以進行音頻的轉錄采樣,一般有三種處理方式:
createScriptProcessor[4]:此方法用于音頻處理最為簡單,但是此方法已經在 W3C 標準里標記為廢棄。不建議使用
MediaRecorder[5]:借助媒體 API 也可以完成音頻的轉錄,但是沒有辦法做到精細處理。
AudioWorkletNode[6]:用于替代 createScriptProcessor 進行音頻處理,可以解決同步線程處理導致導致的對主線程的壓力,同時可以按 bit 進行音頻信號處理,這里也選擇此種方式進行音頻采樣。
基于 AudioWorkletNode 實現音頻的采樣及采樣時長控制方法:
模塊注冊,這里的模塊加載是通過文件的加載方式,PitchProcessor.js 對應的是根目錄下的文件:
const?audio_ctx?=?new?window.AudioContext({sampleRate:?8000,
});
await?audio_ctx.audioWorklet.addModule("PitchProcessor.js");
創建 AudioWorkletNode,主要用于接收通過
port.message
從 WebAudio 線程傳遞回來的數據信息,從而可以在主線程進行數據處理:
class?PitchNode?extends?AudioWorkletNode?{//?Handle?an?uncaught?exception?thrown?in?the?PitchProcessor.onprocessorerror(err)?{console.log(`An?error?from?AudioWorkletProcessor.process()?occurred:?${err}`);}init(callback)?{this.callback?=?callback;this.port.onmessage?=?(event)?=>?this.onmessage(event.data);}onmessage(event)?{if?(event.type?===?'getData')?{if?(this.callback)?{this.callback(event.result);}}}
}const?node?=?new?PitchNode(audio_ctx,?"PitchProcessor");
處理
AudioWorkletProcessor.process
,也就是 PitchProcessor.js 文件內容:
process(inputs,?outputs)?{const?inputChannels?=?inputs[0];const?inputSamples?=?inputChannels[0];if?(this.samples.length?<?48000)?{this.samples?=?concatFloat32Array(this.samples,?inputSamples);}?else?{this.port.postMessage({?type:?'getData',?result:?this.samples?});this.samples?=?new?Float32Array(0);}return?true;
}
取第一個輸入通道的第一個聲道進行數字信號的收集,收集到符合定義的長度(例如這里的48000)之后通知到主線程進行信號的識別處理。
基于
process
方法可以做很多有意思的嘗試,比如最基礎的白噪音生成等。
音頻指紋提取
提取到音頻信號之后,下一步要做的就是對信號數據進行指紋提取,我們提取到的其實就是一段二進制數據,需要對數據進行傅里葉變換,轉換為頻域信息進行特征表示。具體指紋的提取的邏輯是有一套規整的復雜算法,常規的指紋提取方法:1) 基于頻帶能量的音頻指紋;2)基于landmark的音頻指紋;3)基于神經網絡的音頻指紋,對算法感興趣的可以閱讀相關論文,例如:A Highly Robust Audio Fingerprinting System[7]。整個運算有一定的性能要求,基于 WebAssembly 進行運算,可以獲得更好的 CPU 性能。現如今,C++/C/Rust 都有比較便捷的方式編譯成 WebAssembly 字節碼,這里不再展開。
接下來,當你嘗試通過在插件場景中運行 WASM 模塊初始化的時候,你大概率會遇到如下異常:
Refused?to?compile?or?instantiate?WebAssembly?module?because?'wasm-eval'?is?not?an?allowed?source?of?script?in?the?following?Content?Security?Policy?directive:?"script-src?'self'?'unsafe-inline'?'unsafe-eval'?...
這是因為在使用 WebAssembly 的時候需要遵循嚴格的 CSP 定義,對于 Chrome MV2 可以通過追加 "content_security_policy":"script-src 'self' 'unsafe-eval';"
進行聲明解決。而在 MV3 中,由于更加嚴格的隱私及安全限制,已經不允許這種簡單粗暴的執行方式了。MV3 中,對于插件頁面 CSP 定義中的script-src object-src worker-src
只允許取值為:
self
none
localhost
也就是沒有辦法定義 unsafe-eval 等屬性,所以想單純在插件頁面里直接運行 wasm 已經不可行了。到這似乎已經到了絕路?方法總比問題多,細品文檔,發現文檔有這樣一句描述:
CSP modifications for sandbox have no such new restrictions. —— Chrome插件開發文檔[8]
也就是說這種安全限制在沙盒模式下是沒有的。插件本身可以定義 sandbox[9] 頁面,這種頁面雖然無法訪問 web/chrome API,但是它可以運行一些所謂“不安全”的方法,例如 eval、new Function、WebAssembly.instantiate
等。所以可以借助沙盒頁面進行 WASM 模塊的加載及運行,將計算的結果返回給主頁面,整體的指紋采集的流程就變成,如下圖:
特征匹配
提取到的音頻指紋后,接下來就是到指紋庫里進行音頻檢索。指紋庫可以用散列表實現,每個表項表示相同指紋對應的音樂ID和音樂出現的時間,構建出指紋數據庫。從數據庫中訪問提取的指紋即可獲取匹配的歌曲。當然這只是一個基本流程,具體的算法優化方式各家還是有很大的差異,除了版權原因,算法直接導致了各家匹配的效率和正確率。而插件這里的實現還是以效率優先的方式。
寫在最后
以上大致描述了基于 WebAssembly
與 MV3實現聽歌識曲插件的大致流程。插件雖然靈活易用,但是 Google 也意識到了插件帶來的一些安全、隱私等問題,從而進行了一次大規模的遷移。MV3 協議更加具備隱私和安全性,但也限制了不少功能的實現,在2023年之后會有大批量的插件無法繼續使用。
關于聽歌識曲插件[10]目前已完成的功能包括音頻識別、紅心歌單收藏等,后續還將繼續功能拓展,希望這個小功能可以幫助到你。
參考資料
https://developer.mozilla.org/en-US/[11]
https://developer.chrome.com/docs/apps/[12]
https://www.w3.org/TR/webaudio/#widl-AudioContext-createScriptProcessor-ScriptProcessorNode-unsigned-long-bufferSize-unsigned-long-numberOfInputChannels-unsigned-long-numberOfOutputChannels[13]
https://developer.mozilla.org/zh-CN/docs/WebAssembly/C_to_wasm[14]
http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=152C085A95A4B5EF1E83E9EECC283931?doi=10.1.1.103.2175&rep=rep1&type=pdf[15]
本文發布自網易云音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡云音樂,那就加入我們 grp.music-fe(at)corp.netease.com!
參考資料見閱讀原文
[1]
「云音樂聽歌」: https://chrome.google.com/webstore/detail/%E4%BA%91%E9%9F%B3%E4%B9%90%E5%90%AC%E6%AD%8C/kemcalcncfhmdkgglekijclbomdoohkp?hl=zh-CN
[2]插件官網: https://fn.music.163.com/g/chrome-extension-home-page-beta/
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助4000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 ruochuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持~