1. 程式人生 > >聽歌識曲--用python實現一個音樂檢索器

聽歌識曲--用python實現一個音樂檢索器

聽歌識曲,顧名思義,用裝置“聽”歌曲,然後它要告訴你這是首什麼歌。而且十之八九它還得把這首歌給你播放出來。這樣的功能在QQ音樂等應用上早就出現了。我們今天來自己動手做一個自己的聽歌識曲
我們設計的總體流程圖很簡單:
image00

錄音部分

我們要想“聽”,就必須先有錄音的過程。在我們的實驗中,我們的曲庫也要用我們的錄音程式碼來進行錄音,然後提取特徵存進資料庫。我們用下面這樣的思路來錄音
image1

# 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')

我們錄完的歌曲是個什麼形式?
如果只看一個聲道的話,他是一個一維陣列,大概長成這個樣子
image00

我們把他按照索引值為橫軸畫出來,就是我們常常看見的音訊的形式。
image2

音訊處理部分

我們在這裡要寫我們的核心程式碼。關鍵的“如何識別歌曲”。想想我們人類如何區分歌曲? 是靠想上面那樣的一維陣列嗎?是靠歌曲的響度嗎?都不是。
我們是通過耳朵所聽到的特有的頻率組成的序列來記憶歌曲的,所以我們想要寫聽歌識曲的話,就得在音訊的頻率序列上做文章。
複習一下什麼是傅立葉變換。博主的《訊號與系統》的課上的挺水,不過在課上雖然沒有記下來具體的變換形式,但是感性的理解還是有的。
傅立葉變換的實質就是把時域訊號變換成了頻域訊號。也就是原本X,Y軸分別是我們的陣列下標和陣列元素,現在變成了頻率(這麼說不準確,但在這裡這樣理解沒錯)和在這個頻率上的分量大小。

image4
image5

上面兩幅圖來自知乎,非常感謝Heinrich寫的文章,原文連結:點我跳轉

怎麼理解頻域這個事情呢?對於我們訊號處理不是很懂的人來說,最重要的就是改變對音訊的構成的理解。我們原來認為音訊就是如我們開始給出的波形那樣,在每一個時間有一個幅值,不同的幅值序列構成了我們特定的聲音。而現在,我們認為聲音是不同的頻率訊號混合而成的,他們每一個訊號都自始至終存在著。並且他們按照他們的投影分量做貢獻。

讓我們看看把一首歌曲轉化到頻域是什麼樣子?
image5

我們可以觀察到這些頻率的分量並不是平均的,差異是非常大的。我們可以在一定程度上認為在圖中明顯凸起的峰值是輸出能量大的頻率訊號,代表著在這個音訊中,這個訊號佔有很高的地位。於是我們就選擇這樣的訊號來提取歌曲的特徵。

但是別忘了,我們之前說的可是頻率序列,傅立葉變換一套上,我們就只能知道整首歌曲的頻率資訊,那麼我們就損失了時間的關係,我們說的“序列”也就無從談起。所以我們採用的比較折中的方法,將音訊按照時間分成一個個小塊,在這裡我每秒分出了40個塊。
在這裡留個問題:為什麼要採用小塊,而不是每秒一塊這樣的大塊?

我們對每一個塊進行傅立葉變換,然後對其求模,得到一個個陣列。我們在下標值為(0,40),(40,80),(80,120),(120,180)這四個區間分別取其模長最大的下標,合成一個四元組,這就是我們最核心的音訊“指紋”。

我們提取出來的“指紋”類似下面這樣

(39, 65, 110, 131), (15, 66, 108, 161), (3, 63, 118, 146), (11, 62, 82, 158), (15, 41, 95, 140), (2, 71, 106, 143), (15, 44, 80, 133), (36, 43, 80, 135), (22, 58, 80, 120), (29, 52, 89, 126), (15, 59, 89, 126), (37, 59, 89, 126), (37, 59, 89, 126), (37, 67, 119, 126)

音訊處理的類有三個方法:載入資料,傅立葉變換,播放音樂。
如下:

# 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:
            raise TypeError, 'the type of filepath must be string'
        p1 = re.compile('\.wav')
        if p1.findall(filepath) is None:
            raise IOError, 'the suffix of file must be .wav'
        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:
            raise IOError, '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.play('the_mess.wav')
    print p.name

這裡面的self.high_point是未來應用的核心資料。列表型別,裡面的元素都是上面所解釋過的指紋的形式。

資料儲存和檢索部分

因為我們是事先做好了曲庫來等待檢索,所以必須要有相應的持久化方法。我採用的是直接用mysql資料庫來儲存我們的歌曲對應的指紋,這樣有一個好處:省寫程式碼的時間

我們將指紋和歌曲存成這樣的形式:
image6
順便一說:為什麼各個歌曲前幾個的指紋都一樣?(當然,後面肯定是千差萬別的)其實是音樂開始之前的時間段中沒有什麼能量較強的點,而由於我們44100的取樣率比較高,就會導致開頭會有很多重複,別擔心。
我們怎麼來進行匹配呢?我們可以直接搜尋音訊指紋相同的數量,不過這樣又損失了我們之前說的序列,我們必須要把時間序列用上。否則一首歌曲越長就越容易被匹配到,這種歌曲像野草一樣瘋狂的佔據了所有搜尋音訊的結果排行榜中的第一名。而且從理論上說,音訊所包含的資訊就是在序列中體現,就像一句話是靠各個短語和詞彙按照一定順序才能表達出它自己的意思。單純的看兩個句子裡的詞彙重疊數是完全不能判定兩句話是否相似的。我們採用的是下面的演算法,不過我們這只是實驗性的程式碼,演算法設計的很簡單,效率不高。建議想要做更好的結果的同學可以使用改進的DTW演算法。

我們在匹配過程中滑動指紋序列,每次比對模式串和源串的對應子串,如果對應位置的指紋相同,則這次的比對相似值加一,我們把滑動過程中得到的最大相似值作為這兩首歌的相似度。
舉例:
曲庫中的一首曲子的指紋序列:[fp13, fp20, fp10, fp29, fp14, fp25, fp13, fp13, fp20, fp33, fp14]
檢索音樂的指紋序列: [fp14, fp25, fp13, fp17]
比對過程:
image8
image9
image10
image11
image12
image13
image14
image15
最終的匹配相似值為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:
            raise TypeError, 'path need string'
        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: 按照相似度排序後的列表,元素型別為tuple,二元組,歌曲名和相似匹配值
        '''
        #先計算出來我們的音訊指紋
        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:
            raise IOError, 'DataBase error'
        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()
        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', '', '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_pianai.wav')

總結

我們這個實驗很多地方都很粗糙,核心的演算法是從shazam公司提出的演算法吸取的“指紋”的思想。希望讀者可以提出寶貴建議。

相關推薦

--python實現一個音樂檢索

聽歌識曲,顧名思義,用裝置“聽”歌曲,然後它要告訴你這是首什麼歌。而且十之八九它還得把這首歌給你播放出來。這樣的功能在QQ音樂等應用上早就出現了。我們今天來自己動手做一個自己的聽歌識曲 我們設計的總體流程圖很簡單: 錄音部分 我們要想“聽”,就必須先有錄音的過程。在我們的實驗中,我們的曲庫也要用我們的錄

python這個騷操作可以瞭解一下!

音訊指紋識別的目的是確定音訊的數字“摘要”。從而與音訊樣本進行比對得出它出自哪首歌曲,像現在QQ音樂、網易雲音樂等各大音樂軟體都有此功能,它根據歌曲的前兩到五秒識別音樂歌名。今天我們用python來實現這一騷操作。 進群進群:700341555可以獲取Python各類入門學習資料! 這是我的

Python實現一個SVM分類策略

支援向量機(SVM)是什麼意思? 正好最近自己學習機器學習,看到reddit上 Please explain Support Vector Machines (SVM) like I am a 5 year old 的帖子,一個字贊!於是整理一下和大家分享。(如有錯歡迎指教!) 什麼

Python實現一個簡易的“”demo(一)

0. 背景   最近兩年,“聽歌識曲”這個應用在國內眾多的音樂類APP火熱上線,比如網易雲音樂,QQ音樂。使用者可以通過這個功能識別當前環境里正在播放的歌曲名字,聽起來很酷。其實“聽歌識曲”這個想法最早是由一家叫Shazam的國外公司提出的。   - 20

酷狗音樂PC端怎麽使用功能?

com .cn 聽歌識曲 自動 align blog 當我 到你 htm 生活中很多時候會聽到一些美妙的音樂,耳熟或者動聽卻不知道它的名字。就像第一眼看到你心動的那個她卻不知她叫什麽。移動端有酷狗音樂的聽歌識曲。現在PC端也有了相同的功能,每當我們看到一部精彩

python實現一個命令行文本編輯

screen alt 保存 模型 既然 ffffff 圖片 單行 pda “這看起來相當愚蠢”——題記   不過我整個人都很荒誕,何妨呢?貼一張目前的效果圖   看起來很舒服,不是麽?即使一切都是個幌子:光標只能在最後,按一下上下左右就會退出,一行超出75個字符

Python實現一個大數據搜索及源代碼

Python編程語言 Python案例講解 Python基礎精講 在日常生活中,大家了解搜索引擎如百度、360、搜狗、谷歌等,搜索是大數據領域裏常見的需求。Splunk和ELK分別是該領域在非開源和開源領域裏的領導者。本文利用很少的Python代碼實現了一個基本的數據搜索功能,試圖讓大家理解大數據

類方法實現python實現一個簡單的單詞本,添加/查找/刪除單詞。

end code div keys style 成功 move print utf 1.實現一個簡單的單詞本,功能: ①添加單詞,當所添加的單詞已存在時,讓用戶知道 ②查找單詞,當查找的單詞不存在時,讓用戶知道 ③刪除單詞,當刪除的單詞不存在時,讓用戶知道 以上

【人工智慧】Python實現一個簡單的人臉識別,原來我和這個明星如此相似

近幾年來,興起了一股人工智慧熱潮,讓人們見到了AI的能力和強大,比如影象識別,語音識別,機器翻譯,無人駕駛等等。總體來說,AI的門檻還是比較高,不僅要學會使用框架實現,更重要的是,需要有一定的數學基礎,如線性代數,矩陣,微積分等。 幸慶的是,國內外許多大神都已經給我們造好“輪子”,我們可以直接來使用某些模型

【人工智能】Python實現一個簡單的人臉識別,原來我和這個明星如此相似

數值 但是 智能 深度學習 lib python 數學 三方 python實現 近幾年來,興起了一股人工智能熱潮,讓人們見到了AI的能力和強大,比如圖像識別,語音識別,機器翻譯,無人駕駛等等。總體來說,AI的門檻還是比較高,不僅要學會使用框架實現,更重要的是,需要有一定的數

Python 實現一個大資料搜尋引擎

搜尋是大資料領域裡常見的需求。Splunk和ELK分別是該領域在非開源和開源領域裡的領導者。本文利用很少的Python程式碼實現了一個基本的資料搜尋功能,試圖讓大家理解大資料搜尋的基本原理。   布隆過濾器 (Bloom Filter)   第一步我們先要實現一個

Python實現一個簡單的——人臉相似度對比

近幾年來,興起了一股人工智慧熱潮,讓人們見到了AI的能力和強大,比如影象識別,語音識別,機器翻譯,無人駕駛等等。總體來說,AI的門檻還是比較高,不僅要學會使用框架實現,更重要的是,需要有一定的數學基礎,如線性代數,矩陣,微積分等。 幸慶的是,國內外許多大神都已經給我們造好“輪子”,我們可以直

【很有趣】Python實現一個簡單的人臉識別,原來我和這個明星如此相似

近幾年來,興起了一股人工智慧熱潮,讓人們見到了AI的能力和強大,比如影象識別,語音識別,機器翻譯,無人駕駛等等。總體來說,AI的門檻還是比較高,不僅要學會使用框架實現,更重要的是,需要有一定的數學基礎,如線性代數,矩陣,微積分等。 幸慶的是,國內外許多大神都已經給我們造好“輪子”,我們可

Python實現一個大資料搜尋引擎

搜尋是大資料領域裡常見的需求。Splunk和ELK分別是該領域在非開源和開源領域裡的領導者。本文利用很少的Python程式碼實現了一個基本的資料搜尋功能,試圖讓大家理解大資料搜尋的基本原理。 布隆過濾器 (Bloom Filter) 第一步我們先要實現一個布隆過濾器。 布隆

python實現一個迴文數

判斷一個整數是否是迴文數。迴文數是指正序(從左向右)和倒序(從右向左)讀都是一樣的整數。 示例 1: 輸入: 121 輸出: true 示例 2: 輸入: -121 輸出: false 解釋: 從左向右讀, 為 -121 。 從右向左讀, 為 121- 。因此它不是一個迴文數。 示例

python實現一個執行緒池

# !/usr/bin/env python # -*- coding:utf-8 -*- # ref_blog:http://www.open-open.com/home/space-5679-do-blog-

Python 實現一個簡單的postman功能

用Python 實現一個簡單的postman功能 import os import requests import json import defaultdict as default_dict class PostMan: __instance = None

Python實現一個簡單的檔案傳輸協議

寫個東西並非無聊或者練手,而是厭煩了每次都得重頭寫。我已經不是第一次碰到下面的情況:遠端到一臺可以連線內網的機器,結果發現其環境極為惡劣,沒有scp。最誇張的一次,我見過一臺機器連man都沒裝。所幸裝了ssh可以讓我遠端。但沒有scp怎麼傳檔案呢?ftp?試了幾個命令,沒有

Python實現一個類Unix的tail命令

Usage: python tail.py [OPTION] [FILE] Print the last 10 lines of each FILE to standard output. With

【龍書筆記】Python實現一個簡單數學表示式從中綴到字尾語法的翻譯器(採用遞迴下降分析法)

上篇筆記介紹了語法分析相關的一些基礎概念,本篇筆記根據龍書第2.5節的內容實現一個針對簡單表示式的字尾式語法翻譯器Demo。 備註:原書中的demo是java例項,我給出的將是邏輯一致的Python版本的實現。在簡單字尾翻譯器程式碼實現之前,還需要介紹幾個基本概念。1. 自