听歌识曲,顾名思义,用设备“听”歌曲,然后它要告诉你这是首什么歌。而且十之八九它还得把这首歌给你播放出来。这样的功能在qq音乐等应用上早就出现了。我们今天来自己动手做一个自己的听歌识曲
我们设计的总体流程图很简单:
# coding=utf8
import wave
import pyaudio
class recode():
def recode(self, chunk=44100, format=pyaudio.paint16, channels=2, rate=44100, record_seconds=200,
wave_output_filename=”record.wav”):
”’
:param chunk: 缓冲区大小
:param format: 采样大小
:param channels:通道数
:param rate:采样率
:param record_seconds:录的时间
:param wave_output_filename:输出文件路径
:return:
”’
p = pyaudio.pyaudio()
stream = p.open(format=format,
channels=channels,
rate=rate,
input=true,
frames_per_buffer=chunk)
frames = []
for i in range(0, int(rate / chunk * record_seconds)):
data = stream.read(chunk)
frames.append(data)
stream.stop_stream()
stream.close()
p.terminate()
wf = wave.open(wave_output_filename, ‘wb’)
wf.setnchannels(channels)
wf.setsampwidth(p.get_sample_size(format))
wf.setframerate(rate)
wf.writeframes(”.join(frames))
wf.close()
if __name__ == ‘__main__’:
a = recode()
a.recode(record_seconds=30, wave_output_filename=’record_pianai.wav’)
我们录完的歌曲是个什么形式?
如果只看一个声道的话,他是一个一维数组,大概长成这个样子
# coding=utf8
import os
import re
import wave
import numpy as np
import pyaudio
class voice():
def loaddata(self, filepath):
”’
:param filepath: 文件路径,为wav文件
:return: 如果无异常则返回true,如果有异常退出并返回false
self.wave_data内储存着多通道的音频数据,其中self.wave_data[0]代表第一通道
具体有几通道,看self.nchannels
”’
if type(filepath) != str:
print ‘the type of filepath must be string’
return false
p1 = re.compile(‘\.wav’)
if p1.findall(filepath) is none:
print ‘the suffix of file must be .wav’
return false
try:
f = wave.open(filepath, ‘rb’)
params = f.getparams()
self.nchannels, self.sampwidth, self.framerate, self.nframes = params[:4]
str_data = f.readframes(self.nframes)
self.wave_data = np.fromstring(str_data, dtype=np.short)
self.wave_data.shape = -1, self.sampwidth
self.wave_data = self.wave_data.t
f.close()
self.name = os.path.basename(filepath) # 记录下文件名
return true
except:
print ‘file error!’
def fft(self, frames=40):
”’
:param frames: frames是指定每秒钟分块数
:return:
”’
block = []
fft_blocks = []
self.high_point = []
blocks_size = self.framerate / frames # block_size为每一块的frame数量
blocks_num = self.nframes / blocks_size # 将音频分块的数量
for i in xrange(0, len(self.wave_data[0]) – blocks_size, blocks_size):
block.append(self.wave_data[0][i:i + blocks_size])
fft_blocks.append(np.abs(np.fft.fft(self.wave_data[0][i:i + blocks_size])))
self.high_point.append((np.argmax(fft_blocks[-1][:40]),
np.argmax(fft_blocks[-1][40:80]) + 40,
np.argmax(fft_blocks[-1][80:120]) + 80,
np.argmax(fft_blocks[-1][120:180]) + 120,
# np.argmax(fft_blocks[-1][180:300]) + 180,
)) # 提取指纹的关键步骤,没有取最后一个,但是保留了这一项,可以想想为什么去掉了?
def play(self, filepath):
”’
用来做音频播放的方法
:param filepath:文件路径
:return:
”’
chunk = 1024
wf = wave.open(filepath, ‘rb’)
p = pyaudio.pyaudio()
# 打开声音输出流
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=true)
# 写声音输出流进行播放
while true:
data = wf.readframes(chunk)
if data == “”:
break
stream.write(data)
stream.close()
p.terminate()
if __name__ == ‘__main__’:
p = voice()
p.loaddata(‘record_beiyiwang.wav’)
p.fft()
这里面的self.high_point是未来应用的核心数据。列表类型,里面的元素都是上面所解释过的指纹的形式。
数据存储和检索部分
因为我们是事先做好了曲库来等待检索,所以必须要有相应的持久化方法。我采用的是直接用mysql数据库来存储我们的歌曲对应的指纹,这样有一个好处:省写代码的时间
我们将指纹和歌曲存成这样的形式:最终的匹配相似值为3
存储检索部分的实现代码
# coding=utf-8
import os
import mysqldb
import my_audio
class memory():
def __init__(self, host, port, user, passwd, db):
”’
初始化存储类
:param host:主机位置
:param port:端口
:param user:用户名
:param passwd:密码
:param db:数据库名
”’
self.host = host
self.port = port
self.user = user
self.passwd = passwd
self.db = db
def addsong(self, path):
”’
添加歌曲方法,将指定路径的歌曲提取指纹后放到数据库
:param path:路径
:return:
”’
if type(path) != str:
print ‘path need string’
return none
basename = os.path.basename(path)
try:
conn = mysqldb.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd, db=self.db,
charset=’utf8′)
# 创建与数据库的连接
except:
print ‘database error’
return none
cur = conn.cursor()
namecount = cur.execute(“select * from fingerprint.musicdata where song_name = ‘%s'” % basename)
# 查询新添加的歌曲是否已经在曲库中了
if namecount > 0:
print ‘the song has been record!’
return none
v = my_audio.voice()
v.loaddata(path)
v.fft()
cur.execute(“insert into fingerprint.musicdata values(‘%s’,’%s’)” % (basename, v.high_point.__str__()))
# 将新歌曲的名字和指纹存到数据库中
conn.commit()
cur.close()
conn.close()
def fp_compare(self, search_fp, match_fp):
”’
指纹比对方法。
:param search_fp: 查询指纹
:param match_fp: 库中指纹
:return:最大相似值 float
”’
if len(search_fp) > len(match_fp):
return 0
max_similar = 0
search_fp_len = len(search_fp)
match_fp_len = len(match_fp)
for i in range(match_fp_len – search_fp_len):
temp = 0
for j in range(search_fp_len):
if match_fp[i + j] == search_fp[j]:
temp += 1
if temp > max_similar:
max_similar = temp
return max_similar
def search(self, path):
”’
从数据库检索出
:param path: 需要检索的音频的路径
:return:返回列表,元素是二元组,第一项是匹配的相似值,第二项是歌曲名
”’
v = my_audio.voice()
v.loaddata(path)
v.fft()
try:
conn = mysqldb.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd, db=self.db,
charset=’utf8′)
except:
print ‘database error’
return none
cur = conn.cursor()
cur.execute(“select * from fingerprint.musicdata”)
result = cur.fetchall()
compare_res = []
for i in result:
compare_res.append((self.fp_compare(v.high_point[:-1], eval(i[1])), i[0]))
compare_res.sort(reverse=true)
cur.close()
conn.close()
print compare_res
return compare_res
def search_and_play(self, path):
”’
跟上个方法一样,不过增加了将搜索出的最优结果直接播放的功能
:param path: 带检索歌曲路径
:return:
”’
v = my_audio.voice()
v.loaddata(path)
v.fft()
# print v.high_point
try:
conn = mysqldb.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd, db=self.db,
charset=’utf8′)
except:
print ‘database error’
return none
cur = conn.cursor()
cur.execute(“select * from fingerprint.musicdata”)
result = cur.fetchall()
compare_res = []
for i in result:
compare_res.append((self.fp_compare(v.high_point[:-1], eval(i[1])), i[0]))
compare_res.sort(reverse=true)
cur.close()
conn.close()
print compare_res
v.play(compare_res[0][1])
return compare_res
if __name__ == ‘__main__’:
sss = memory(‘localhost’, 3306, ‘root’, ‘root’, ‘fingerprint’)
sss.addsong(‘taiyangzhaochangshengqi.wav’)
sss.addsong(‘beiyiwangdeshiguang.wav’)
sss.addsong(‘xiaozezhenger.wav’)
sss.addsong(‘nverqing.wav’)
sss.addsong(‘the_mess.wav’)
sss.addsong(‘windmill.wav’)
sss.addsong(‘end_of_world.wav’)
sss.addsong(‘pianai.wav’)
sss.search_and_play(‘record_beiyiwang.wav’)
总结
我们这个实验很多地方都很粗糙,核心的算法是从shazam公司提出的算法吸取的“指纹”的思想。希望读者可以提出宝贵建议。
更多用python实现一个音乐检索器的功能相关文章请关注php中文网!