最近两年,“听歌识曲”这个应用在国内众多的音乐类APP火热上线,比如网易云音乐,QQ音乐。用户可以通过这个功能识别当前环境里正在播放的歌曲名字,听起来很酷。其实“听歌识曲”这个想法最早是由一家叫Shazam的国外公司提出的。
- 2008年,Shazam率先在ios和android上发布了APP,并且整合了iTunes/Amazon’s MP3 store歌曲购买服务;
- 2013年,Shazam成为年度十大最受欢迎的手机应用;
- 2017年12月,苹果公司宣布以4亿美元收购Shazam,将“听歌识曲”整合在iTunes里,加大自己在音乐服务领域的竞争力,以对抗Apple Music最大的竞争对手Spotify。
历史就先讲到这里,回到正题。今天我们要做的是做一个简易的“听歌识曲”,在这篇博客中,我不会讲过多算法的细节,只是完全从代码的角度来讲述实现过程。
那么我们怎样才能实现听歌识曲呢?以下两个要素是必要的:
1. 对歌曲进行特征提取。一般来说,鲁棒性高并且容易分别的特征存在于音频文件的频谱。从音乐的角度来讲,一首歌曲的旋律,节奏,韵律都属于这类特征。
2. 搜索库的构建。对歌曲的识别应该是在一个音乐歌曲库里进行搜索,选择和待识别歌曲最相似的作为匹配歌曲输出。
在这个demo实现中,我们选取最简单的一个特征来进行识别——节奏,可以很确定的是,每首歌的节奏都会有所不同,不大可能出现100%一致的两首歌曲;同样,可能会存在一些节奏很类似的歌曲,也许节奏点的重合度达到80%以上。
综上,所以我认为“节奏”只能作为一个初步的特征识别的过滤,原因如下:节奏差别很大的两首歌肯定不同;在噪声的影响下,节奏差别很小的两首歌很难确定是否相同。对于本文中提及的实现“听歌识曲”的简易demo,用节奏(beat)作为歌曲的特征是完全可行的,但是要做很复杂很精确的“听歌识曲”应用,应该加入其它的特征(比如音频指纹)做更加细致的特征区分。
我们用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
演示视频如下:
width="560" height="315" src="https://v.youku.com/v_show/id_XMzgxNzIwODQ0OA==" allowfullscreen="">https://github.com/wblgers/music_retrieve
将你的音乐库放入文件夹music_base
,后缀名支持.wav
;将你的待识别的歌曲片段放入文件夹music_test
;运行代码librosa_music.py
进行搜索库的创建,运行代码librosa_main.py
进行识别,也支持直接打开麦克风录音完成识别。具体的细节可以看代码实现。
喜欢的话可以点个star!
https://en.wikipedia.org/wiki/Shazam_(application)
http://librosa.github.io/librosa/generated/librosa.beat.beat_track.html#librosa.beat.beat_track
https://labrosa.ee.columbia.edu/projects/beattrack/