90 行 Python 搭一个音乐搜索工具

以前一段时间读到了这篇博客,其中描述了做者如何用java实现国外著名音乐搜索工具shazam的基本功能。其中所提到的文章又将我引向了关于shazam的一篇论文另一篇博客。读完以后发现其中的原理并不十分复杂,可是方法对噪音的健壮性却很是好,出于好奇决定本身用python本身实现了一个简单的音乐搜索工具—— Song Finder, 它的核心功能被封装在SFEngine 中,第三方依赖方面只使用到了 scipyhtml

工具demo

这个demo在ipython下展现工具的使用,本项目名称为Song Finder,我把索引、搜索的功能所有封装在Song Finder中的SFEngine中。首先是简单的准备工做:java

In [1]: from SFEngine import * In [2]: engine = SFEngine() 

在这以后咱们对现有歌曲进行索引,我在original目录下准备了几十首歌曲(.wav文件)做为曲库:python

In [3]: engine.index('original') # 索引该目录下的全部歌曲

在完成索引以后咱们向Song Finder提交一段有背景噪音的歌曲录音进行搜索。对于这段《枫》在1分15秒左右的录音:git

工具的返回结果是:github

In [4]: engine.search('record/record0.wav') original/周杰伦-73 original/周杰伦-31 original/周杰伦-10 original/周杰伦-28 original/我要快樂 - 張惠妹 28 

其中展现的分别是歌曲名称及片断在歌曲中出现的位置(以秒计),能够看到工具正确找回了歌曲的曲名,也找到了其在歌曲中的正确位置。web

而对于这段《童话》在1分05秒左右的背景噪音更加嘈杂的录音:后端

工具的返回结果是:wordpress

In [5]: engine.search('record/record8.wav') original/光良 - 童话 67 original/光良 - 童话 39 original/光良 - 童话 33 original/光良 - 童话 135 original/光良 - 童话 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

相关文章
相关标签/搜索