90 行 Python 搭一個音樂搜索工具

以前一段時間讀到了這篇博客,其中描述了做者如何用java實現國外著名音樂搜索工具shazam的基本功能。其中所提到的文章又將我引向了關於shazam的一篇論文另一篇博客。讀完以後發現其中的原理並不十分複雜,可是方法對噪音的健壯性卻很是好,出於好奇決定本身用python本身實現了一個簡單的音樂搜索工具—— Song Finder, 它的核心功能被封裝在SFEngine 中,第三方依賴方面只使用到了 scipyhtml

工具demo

這個demo在ipython下展現工具的使用,本項目名稱爲Song Finder,我把索引、搜索的功能所有封裝在Song Finder中的SFEngine中。首先是簡單的準備工做:java

In [1]: from SFEngine import * In [2]: engine = SFEngine() 

在這以後咱們對現有歌曲進行索引,我在original目錄下準備了幾十首歌曲(.wav文件)做爲曲庫:python

In [3]: engine.index('original') # 索引該目錄下的全部歌曲

在完成索引以後咱們向Song Finder提交一段有背景噪音的歌曲錄音進行搜索。對於這段《楓》在1分15秒左右的錄音:git

工具的返回結果是:github

In [4]: engine.search('record/record0.wav') original/周杰倫-73 original/周杰倫-31 original/周杰倫-10 original/周杰倫-28 original/我要快樂 - 張惠妹 28 

其中展現的分別是歌曲名稱及片斷在歌曲中出現的位置(以秒計),能夠看到工具正確找回了歌曲的曲名,也找到了其在歌曲中的正確位置。web

而對於這段《童話》在1分05秒左右的背景噪音更加嘈雜的錄音:後端

工具的返回結果是:wordpress

In [5]: engine.search('record/record8.wav') original/光良 - 童話 67 original/光良 - 童話 39 original/光良 - 童話 33 original/光良 - 童話 135 original/光良 - 童話 69 

能夠看到儘管噪音很是嘈雜,可是工具仍然能成功識別所對應的歌曲並對應到歌曲的正確位置,說明工具在噪音較大的環境下有良好的健壯性!函數

項目主頁: Github工具

Song Finder原理

給定曲庫對一個錄音片斷進行檢索是一個徹徹底底的搜索問題,可是對音頻的搜索並不像對文檔、數據的搜索那麼直接。爲了完成對音樂的搜索,工具須要完成下列3個任務:

  • 對曲庫中的全部歌曲抽取特徵
  • 以相同的方式對錄音片斷提取特徵
  • 根據錄音片斷的特徵對曲庫進行搜索,返回最類似的歌曲及其在歌曲中的位置

特徵提取?離散傅立葉變換!

爲了對音樂(音頻)提取特徵,一個很直接的想法是獲得音樂的音高的信息,而音高在物理上對應的則又是波的頻率信息。爲了獲取這類信息,一個很是直接的額作法是使用離散傅葉變化對聲音進行分析,即便用一個滑動窗口對聲音進行採樣,對窗口內的數據進行離散傅立葉變化,將時間域上的信息變換爲頻率域上的信息,使用scipy的接口能夠很輕鬆的完成。在這以後咱們將頻率分段,提取每頻率中振幅最大的頻率:

def extract_feature(self, scaled, start, interval): end = start + interval dst = fft(scaled[start: end]) length = len(dst)/2 normalized = abs(dst[:(length-1)]) feature = [ normalized[:50].argmax(), \ 50 + normalized[50:100].argmax(), \ 100 + normalized[100:200].argmax(), \ 200 + normalized[200:300].argmax(), \ 300 + normalized[300:400].argmax(), \ 400 + normalized[400:].argmax()] return feature 

這樣,對於一個滑動窗口,我提取到了6個頻率做爲其特徵。對於整段音頻,咱們重複調用這個函數進行特徵抽取:

def sample(self, filename, start_second, duration = 5, callback = None): start = start_second * 44100 if duration == 0: end = 1e15 else: end = start + 44100 * duration interval = 8192 scaled = self.read_and_scale(filename) length = scaled.size while start < min(length, end): feature = self.extract_feature(scaled, start, interval) if callback != None: callback(filename, start, feature) start += interval 

其中44100爲音頻文件自身的採樣頻率,8192是我設定的取樣窗口(對,這樣hardcode是很不對的),callback是一個傳入的函數,須要這個參數是由於在不一樣場景下對於所獲得的特徵會有不一樣的後續操做。

匹配曲庫

在獲得歌曲、錄音的大量特徵後,如何進行高效搜索是一個問題。一個有效的作法是創建一個特殊的哈希表,其中的key是頻率,其對應的value是一系列(曲名,時間)的tuple,其記錄的是某一歌曲在某一時間出現了某一特徵頻率,可是以頻率爲key而非以曲名或時間爲key。

表格。。

這樣作的好處是,當在錄音中提取到某一個特徵頻率時,咱們能夠從這個哈希表中找出與該特徵頻率相關的歌曲及時間!

固然有了這個哈希表還不夠用,咱們不可能把全部與特徵頻率相關的歌曲都抽出來,看看誰命中的次數多,由於這樣會徹底無視歌曲的時序信息,並引入一些錯誤的匹配。

咱們的作法是,對於錄音中在t時間點的一個特徵頻率f,從曲庫找出全部與f相關的(曲名,時間)tuple,例如咱們獲得了

[(s1, t1), (s2, t2), (s3, t3)]

咱們使用時間進行對齊,獲得這個列表

[(s1, t1-t), (s2, t2-t), (s3, t3-t)]

記爲

[(s1, t1`), (s2, t2`), (s3, t3`)]

咱們對全部時間點的全部特徵頻率均作上述操做,獲得了一個大列表:

[(s1, t1`), (s2, t2`), (s3, t3`), ..., (sn, tn`)]

對這個列表進行計數,能夠看到哪首歌曲的哪一個時間點命中的次數最多,並將命中次數最多的(曲名,時間)對返回給用戶。

不足

這個小工具是一個幾個小時寫成的hack,有許都地方須要改進,例如:

  • 目前只支持了wav格式的曲庫及錄音
  • 全部數據都放在內存中,曲庫體積增大時須要引入更好的後端存儲
  • 索引應該並行化,匹配也應該並行化,匹配的模型實際上是典型的map-reduce。

項目主頁

Github

相關文章
相關標籤/搜索