用Python實現一個簡易的“聽歌識曲”demo(一)
0. 背景
最近兩年,“聽歌識曲”這個應用在國內眾多的音樂類APP火熱上線,比如網易雲音樂,QQ音樂。使用者可以通過這個功能識別當前環境里正在播放的歌曲名字,聽起來很酷。其實“聽歌識曲”這個想法最早是由一家叫Shazam的國外公司提出的。
- 2008年,Shazam率先在ios和android上釋出了APP,並且整合了iTunes/Amazon’s MP3 store歌曲購買服務;
- 2013年,Shazam成為年度十大最受歡迎的手機應用;
- 2017年12月,蘋果公司宣佈以4億美元收購Shazam,將“聽歌識曲”整合在iTunes裡,加大自己在音樂服務領域的競爭力,以對抗Apple Music最大的競爭對手Spotify。
歷史就先講到這裡,回到正題。今天我們要做的是做一個簡易的“聽歌識曲”,在這篇部落格中,我不會講過多演算法的細節,只是完全從程式碼的角度來講述實現過程。
1. “聽歌識曲”原理
那麼我們怎樣才能實現聽歌識曲呢?以下兩個要素是必要的:
1. 對歌曲進行特徵提取。一般來說,魯棒性高並且容易分別的特徵存在於音訊檔案的頻譜。從音樂的角度來講,一首歌曲的旋律,節奏,韻律都屬於這類特徵。
2. 搜尋庫的構建。對歌曲的識別應該是在一個音樂歌曲庫裡進行搜尋,選擇和待識別歌曲最相似的作為匹配歌曲輸出。
在這個demo實現中,我們選取最簡單的一個特徵來進行識別——節奏,可以很確定的是,每首歌的節奏都會有所不同,不大可能出現100%一致的兩首歌曲;同樣,可能會存在一些節奏很類似的歌曲,也許節奏點的重合度達到80%以上。
綜上,所以我認為“節奏”只能作為一個初步的特徵識別的過濾,原因如下:節奏差別很大的兩首歌肯定不同;在噪聲的影響下,節奏差別很小的兩首歌很難確定是否相同。對於本文中提及的實現“聽歌識曲”的簡易demo,用節奏(beat)作為歌曲的特徵是完全可行的,但是要做很複雜很精確的“聽歌識曲”應用,應該加入其它的特徵(比如音訊指紋)做更加細緻的特徵區分。
2. 程式碼實現
我們用python來實現整個demo,需要安裝的依賴庫有以下:
- librosa,音樂訊號分析的python庫
- dtw,衡量時間序列的相似度
- numpy,數值計算庫
首先用librosa庫來提取歌曲的節奏點,並建立搜尋庫:
import librosa
import os
import numpy as np
audioList = os.listdir('music_base')
raw_audioList = {}
beat_database = {}
for tmp in audioList:
audioName = os .path.join('music_base', tmp)
if audioName.endswith('.wav'):
y, sr = librosa.load(audioName)
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
beat_frames = librosa.feature.delta(beat_frames)
beat_database[audioName] = beat_frames
其中最關鍵的兩行程式碼是:
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
beat_frames = librosa.feature.delta(beat_frames)
第一行程式碼是呼叫librosa的beat_track
對歌曲時間序列進行節奏點的跟蹤,返回的beat_frames即為節奏點的時間座標,需要特別注意的是第二行程式碼,我們對提取出的節奏時間序列進行差分,即最終儲存的特徵是是連續前後兩個節奏點時間座標的差值。為什麼要這麼做?原因在於,在對環境歌曲進行識別時,我們並不知道這首歌的起始點在哪裡,也許使用者開啟這個功能時,歌曲已經播放一半時間了,那麼去匹配絕對的節奏點的時間座標
是沒有意義的。但是,節奏的間隔
卻是不變的。
然後將每首歌的特徵和歌曲名字存放到一個字典中,以供測試識別時可以快速查詢:
np.save('beatDatabase.npy', beat_database)
最後,我們開啟一首歌,通過電腦的麥克風對環境歌曲進行錄製,然後同樣地提取它的節奏間隔特徵,並且音樂庫的所有歌曲分別進行序列匹配,輸出與它最相似的歌曲:
# -*- coding: utf-8 -*-
from dtw import dtw
from numpy.linalg import norm
from numpy import array
import numpy as np
import librosa
import pyaudio
import wave
all_data = np.load('beatDatabase.npy')
beat_database = all_data.item()
sr = 44100
chunk = sr
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16,
channels=1,
rate=sr,
input=True,
frames_per_buffer=chunk)
frames = []
for i in range(0, int(sr / chunk * 30)):
data = stream.read(chunk)
frames.append(data)
stream.stop_stream()
stream.close()
p.terminate()
#
wf = wave.open('test.wav', 'wb')
wf.setnchannels(1)
wf.setsampwidth(p.get_sample_size(pyaudio.paInt16))
wf.setframerate(sr)
wf.writeframes(b''.join(frames))
wf.close()
# testAudio = "test_music/record_jayzhou.wav"
testAudio = "test.wav"
y, sr = librosa.load(testAudio)
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
beat_frames = librosa.feature.delta(beat_frames)
x = array(beat_frames).reshape(-1, 1)
compare_result = {}
for songID in beat_database.keys():
y = beat_database[songID]
y = array(y).reshape(-1, 1)
dist, cost, acc, path = dtw(x, y, dist=lambda x, y: norm(x - y, ord=1))
print('Minimum distance found for ', songID.split("\\")[1], ": ", dist)
compare_result[songID] = dist
matched_song = min(compare_result, key=compare_result.get)
print(matched_song)
其中,需要注意的一點,對於時間序列的匹配我們選取的演算法是dtw(動態時間規整),對應的程式碼段如下。其中y
是音樂庫裡歌曲songID
對應的特徵,x
是當前麥克風捕捉到的音樂段的特徵,呼叫函式dtw
對兩者按照最小均方誤差的標準進行匹配,返回的dist
用來表徵兩個時間序列的距離,距離越小則相似度越高。
y = beat_database[songID]
y = array(y).reshape(-1, 1)
dist, cost, acc, path = dtw(x, y, dist=lambda x, y: norm(x - y, ord=1))
具體的細節可以閱讀Python庫dtw的示例程式碼:
https://github.com/pierre-rouanet/dtw/blob/master/examples/simple%20example.ipynb
短短不到100行程式碼,我們就完成了一個很酷的“聽歌識曲”demo。我們用周杰倫的范特西專輯來進行測試,效果如下:
D:\Developer\python\anaconda3\python.exe D:/learning/music_retrieve/librosa_main.py
Minimum distance found for 周杰倫 - 對不起.mp3 : 0.035980221058757346
Minimum distance found for 周杰倫 - 爸 我回來了.mp3 : 0.2417422867513621
Minimum distance found for 周杰倫 - 雙截棍.mp3 : 5.815719207579681
Minimum distance found for 周杰倫 - 愛在西元前.mp3 : 1.5796865581675672
Minimum distance found for 周杰倫 - 忍者.mp3 : 4.666914682539685
Minimum distance found for 周杰倫 - 開不了口.mp3 : 0.059177365668093604
Minimum distance found for 周杰倫 - 上海 一九四三.mp3 : 2.13738962472406
Minimum distance found for 周杰倫 - 簡單愛.mp3 : 6.958281998631065
Minimum distance found for 周杰倫 - 威廉古堡.mp3 : 14.53719958202717
Minimum distance found for 周杰倫 - 安靜.mp3 : 14.806564551422317
Matched song is: music_base\周杰倫 - 對不起.mp3
Process finished with exit code 0
演示視訊如下:
3. 專案地址
https://github.com/wblgers/music_retrieve
將你的音樂庫放入資料夾music_base
,字尾名支援.wav
;將你的待識別的歌曲片段放入資料夾music_test
;執行程式碼librosa_music.py
進行搜尋庫的建立,執行程式碼librosa_main.py
進行識別,也支援直接開啟麥克風錄音完成識別。具體的細節可以看程式碼實現。
喜歡的話可以點個star!