1. 程式人生 > >用Python實現一個簡易的“聽歌識曲”demo(一)

用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!

4. 參考文獻