標籤:
之前一段時間讀到了這篇部落格,其中描述了作者如何用java實現國外著名音樂搜尋工具shazam的準系統。其中所提到的文章又將我引向了關於shazam的一篇論文及另外一篇部落格。讀完之後發現其中的原理並不十分複雜,但是方法對噪音的健壯性卻非常好,出於好奇決定自己用python自己實現了一個簡單的音樂搜尋工具—— Song Finder, 它的核心功能被封裝在SFEngine 中,第三方依賴方面只使用到了 scipy。
工具demo
這個demo在ipython下展示工具的使用,本項目名稱為Song Finder,我把索引、搜尋的功能全部封裝在Song Finder中的SFEngine中。首先是簡單的準備工作:
In [1]: from SFEngine import *In [2]: engine = SFEngine()
在這之後我們對現有歌曲進行索引,我在original目錄下準備了幾十首歌曲(.wav檔案)作為曲庫:
In [3]: engine.index(‘original‘) # 索引該目錄下的所有歌曲
在完成索引之後我們向Song Finder提交一段有背景雜音的歌曲錄音進行搜尋。對於這段《楓》在1分15秒左右的錄音:
工具的返回結果是:
In [4]: engine.search(‘record/record0.wav‘)original/周杰倫-楓 73original/周杰倫-楓 31original/周杰倫-楓 10original/周杰倫-楓 28original/我要快樂 - 張惠妹 28
其中展示的分別是歌曲名稱及片段在歌曲中出現的位置(以秒計),可以看到工具正確找回了歌曲的曲名,也找到了其在歌曲中的正確位置。
而對於這段《童話》在1分05秒左右的背景雜音更加嘈雜的錄音:
工具的返回結果是:
In [5]: engine.search(‘record/record8.wav‘)original/光良 - 童話 67original/光良 - 童話 39original/光良 - 童話 33original/光良 - 童話 135original/光良 - 童話 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
90 行 Python 搭一個音樂搜尋工具