用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

演示视频如下:

width="560" height="315" src="https://v.youku.com/v_show/id_XMzgxNzIwODQ0OA==" allowfullscreen="">

3. 项目地址

https://github.com/wblgers/music_retrieve
将你的音乐库放入文件夹music_base,后缀名支持.wav;将你的待识别的歌曲片段放入文件夹music_test;运行代码librosa_music.py进行搜索库的创建,运行代码librosa_main.py进行识别,也支持直接打开麦克风录音完成识别。具体的细节可以看代码实现。

喜欢的话可以点个star!

4. 参考文献

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/

  • 28
    点赞
  • 153
    收藏
    觉得还不错? 一键收藏
  • 32
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值