Chrome插件:雲音樂聽歌識曲

語言: CN / TW / HK

圖片來源: Chrome插件-雲音樂聽歌

本文作者:空吾

當你用網頁在視頻網站刷視頻的時候,有沒有碰到過一個 BGM 激起你內心的波瀾,而你卻不知道它的名字。此時只能打開手機進行聽歌識曲,而通過一個瀏覽器的插件卻更容易解決這個問題。不需要繁瑣的掏出手機,也不會因為需要外放而干擾他人,更不會因為環境噪音而識別困難。

如果你恰好也有這個需要,不妨試一下雲音樂出品的 Chrome 瀏覽器插件「雲音樂聽歌」,還可以直接進行紅心收藏哦。也可以到插件官網預覽實際運行的效果。

背景

目前 Chrome 商店上存在的聽歌識曲插件,大都是國外出品,國內產品寥寥,對於國內音樂支持較差。既然雲音樂有這個能力,我們希望將這樣的功能覆蓋每一個角落,傳遞音樂美好力量。與此同時市面上的插件大多還是基於 manifest v2 實現(相對於 manifest v3,安全性、性能、隱私性均較差),普遍的做法是將音頻錄製之後直接交給服務端,通過服務端進行指紋提取,徒增服務端計算壓力,增加網絡傳輸。

那麼有沒有辦法既能使用 manifest v3 協議進行功能實現,同時將音頻指紋提取這一計算放在前端呢?

Chrome瀏覽器插件新協議

本文的重心不在如何實現一個瀏覽器插件本身,如果你不瞭解插件本身的開發,可查閲 Google 官方的開發文檔。

特別説明的是,manifest v2(MV2) 即將被廢棄,在 2022 年逐步不接受更新,2023 年將會逐步不能運行,本文所有的內容都是基於更安全、性能更好、隱私更強的 manifest v3(MV3)進行實現。

協議升級對功能的實現方式也會帶來一些變化,因為 MV3 更安全的限制,一些基於 MV2 靈活的實現方式(例如:執行遠程代碼、可以使用 eval、new Function(...) 等不安全方法)將不能使用。而這會對聽歌識曲插件帶來一些實現上的難題。

MV3 協議對插件實現核心影響點:

  • 原有的 Background Page 使用 Service Worker 進行替代,這意味着在 Background Page 不再能進行 Web API 等操作。
  • 遠程代碼託管不再支持,無法進行動態加載代碼,意味着可執行的代碼都需要直接打包到插件中。
  • 內容安全策略調整,不再支持不安全代碼的直接執行。WASM 初始化相關函數無法直接運行。

    聽歌識曲的實現

    聽歌識曲本身技術比較成熟,整體的思路是通過 音頻數字採樣 ,進行音頻 指紋的提取 ,最後將指紋在數據庫進行 匹配 ,特徵值最高的即是所認為識別到的歌曲。

瀏覽器插件中的音頻提取

利用插件進行網頁內的音視頻錄製其實非常簡單,只需要 chrome.tabCapture API 即可實現網頁本身的音頻錄製,獲取到的流數據我們需要針對音頻數據進行採樣,保證計算 HASH 的規則和數據庫數據保持一致。

針對獲取的 stream 流可以進行音頻的轉錄採樣,一般有三種處理方式:

  • createScriptProcessor :此方法用於音頻處理最為簡單,但是此方法已經在 W3C 標準裏標記為廢棄。不建議使用
  • MediaRecorder:藉助媒體 API 也可以完成音頻的轉錄,但是沒有辦法做到精細處理。
  • AudioWorkletNode:用於替代 createScriptProcessor 進行音頻處理,可以解決同步線程處理導致導致的對主線程的壓力,同時可以按 bit 進行音頻信號處理,這裏也選擇此種方式進行音頻採樣。

基於 AudioWorkletNode 實現音頻的採樣及採樣時長控制方法:

  1. 模塊註冊,這裏的模塊加載是通過文件的加載方式,PitchProcessor.js 對應的是根目錄下的文件:

    const audio_ctx = new window.AudioContext({
      sampleRate: 8000,
    });
    await audio_ctx.audioWorklet.addModule("PitchProcessor.js");
  2. 創建 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");
  3. 處理 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

。整個運算有一定的性能要求,基於 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插件開發文檔

也就是説這種安全限制在沙盒模式下是沒有的。插件本身可以定義sandbox 頁面,這種頁面雖然無法訪問 web/chrome API,但是它可以運行一些所謂“不安全”的方法,例如 eval、new Function、WebAssembly.instantiate 等。

所以可以藉助沙盒頁面進行 WASM 模塊的加載及運行,將計算的結果返回給主頁面,整體的指紋採集的流程就變成,如下圖:

對於主頁面和沙盒頁面如何進行數據通信,可以通過在主頁面裏邊加載 iFrame 的方式,藉助iFrame的 contentWindow 和主 window 進行數據聯通,數據流程如下圖:

到這裏完成了基本的音頻的提取及指紋提取的過程,剩下的部分就是通過指紋在數據庫進行特徵匹配。

特徵匹配

提取到的音頻指紋後,接下來就是到指紋庫裏進行音頻檢索。指紋庫可以用散列表實現,每個表項表示相同指紋對應的音樂ID和音樂出現的時間,構建出指紋數據庫。從數據庫中訪問提取的指紋即可獲取匹配的歌曲。當然這只是一個基本流程,具體的算法優化方式各家還是有很大的差異,除了版權原因,算法直接導致了各家匹配的效率和正確率。而插件這裏的實現還是以效率優先的方式。

寫在最後

以上大致描述了基於 WebAssembly 與 MV3實現聽歌識曲插件的大致流程。插件雖然靈活易用,但是 Google 也意識到了插件帶來的一些安全、隱私等問題,從而進行了一次大規模的遷移。MV3 協議更加具備隱私和安全性,但也限制了不少功能的實現,在2023年之後會有大批量的插件無法繼續使用。

關於聽歌識曲插件目前已完成的功能包括音頻識別、紅心歌單收藏等,後續還將繼續功能拓展,希望這個小功能可以幫助到你。

參考資料

本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!