在講算法之前,上一些前人的資料。
http://coding-geek.com/how-shazam-works/
https://laplacian.wordpress.com/2009/01/10/how-shazam-works/
http://royvanrijn.com/blog/2010/06/creating-shazam-in-java/
當然歷史也有點久遠了,如果你有心去百度一下 shazam 算法,
你會發現這類的博客也是不少的。
當然這顯得我現在寫這個,好像有點多余。
這里我就快速過一下算法的思路。
假設你有一首歌,換句話說,你有一堆數據。
你可以通過各種各樣的方式,對它的內容進行抽象表達。
舉個例子,對音樂而言,就是歌名,類型,時長等等。
對於一本書而言就是目錄,標題,價位之類的屬性。
但是有時候我們會忘記具體內容,
只知道大概的印象,這個時候想要找到對應的那個東西就比較困難了。
舉個例子,Beyond 樂隊的《喜歡你》這首歌,百度上一堆 問“黑鳳梨” blabla的。。
而音樂檢索算法就是為了提供比較人性化的方式幫忙 搜索音樂。
而shazam 這家公司就是第一個吃螃蟹的"人"。
上面提供的鏈接里都提到了shazam 算法的思路,需要細節了解的可以移步上面的鏈接。
shazam 算法分為以下步驟:
1.進行fft變換
2.切分5個頻段,取頻段中比較有代表性的信息,一般為該頻段中強度最大值。
3.將取到的5個點,拼接起來算個字符串hash作為該段音樂的特征
4.以此類推對整個音頻重復1,2,3步驟
最終拿到整個音頻的所有hash信息。
后面檢索音頻也就是簡單的建立hash庫,然后撞hash數量,評分。
hash命中越多就認為越相似。
上圖,感受一下,其實我感覺看圖也不是很直觀,哈哈哈哈。
整個算法非常簡單,
最核心的點是 切分5個頻段,
用上了時序信息去算哈希。
對於有時序的數據,肯定要用上時序性維度,不然是有失偏頗的。
之余圖片,就要用空間性維度,之余視頻,時間和空間都要有。
這個算法簡單粗暴,也有效。
嚴格意義上講,這個算法的泛化能力有待商榷。
改進的思路和方向也挺多的。
例如:
1.降低精度,下采樣
(之於圖像就是縮小圖片等)
2.還分為5個頻段,但是提取更加具有代表性的特征,
可以采用一些圖像思路,例如模糊之后增強
(之於圖像一般是計算角點等,詳情參考sift)
3.加入更多的時序維度,擴展更多的時序關聯,例如臨近特征關鍵點的差距
(之於圖像,就是采用卷積提取空間特征等)
4.音量歸一化,拉伸音頻的分貝值
(之於圖像就是直方圖拉伸,自動增強,白平衡等)
當然還有很多方法可以進一步拓展,其實核心目標就是控制變量因素。
盡可能的讓數據處在先驗條件的區間內計算。
其實說難也不難,說簡單也不簡單。
有另一個音頻檢索算法就是做了控制變量達到更加強大的魯棒性。
他就是,dejavu
算法細節參見:http://willdrevo.com/fingerprinting-and-audio-recognition-with-python/
不過dejavu其中有一個地方的思路,我認為不妥,就是在最后算hash特征的時候采用sha-1算法。
我認為可以直接采用最后算出的值改為int16 直接拼合起來就可以了,可以降低算法的復雜度。
dejavu用到了一些圖像算法,主要就是用於提取更加具有代表性的特征。
具體算法細節這里就不展開了,有興趣的朋友可以去好好學習一下。
當然除了以上提到的兩個算法之外,還有其他的一些實現,不過都是換湯不換葯的節奏。
當然,我本人業余時間在研究自己構思的一個音頻檢索算法,還在開展中,
算法復雜度當然會更高,但是效果和后續檢索准確度會大有提升。
上面提到的shazam和dejavu,本人以純c 原汁原味實現之。
嗯,shazam的算法,開源給大家學習之。
代碼來也:
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include "fft.h" //ref:https://raw.githubusercontent.com/cxong/tinydir/master/tinydir.h #include "tinydir.h" #include "timing.h" #define DR_WAV_IMPLEMENTATION //ref:https://raw.githubusercontent.com/mackron/dr_libs/master/dr_wav.h #include "dr_wav.h" int16_t *wavRead_int16(char *filename, uint32_t *sampleRate, uint64_t *totalSampleCount) { unsigned int channels; int16_t *buffer = drwav_open_and_read_file_s16(filename, &channels, sampleRate, totalSampleCount); if (buffer == 0) { fprintf(stderr, "ERROR\n"); exit(1); } if (channels == 2) { int16_t *bufferSave = buffer; for (int i = 0; i < *totalSampleCount; i += 2) { *bufferSave++ = (int16_t) ((buffer[i] + buffer[i + 1]) >> 1); } *totalSampleCount = *totalSampleCount >> 1; } return buffer; } unsigned long hash(unsigned char *str) { unsigned long hash = 5381; int c; while (c = *str++) hash = ((hash << 5) + hash) + c; return hash; } int generateHashes(char *input_file, int **hashtable, int songid, size_t N, int freqbandWidth, int maxElems) { printf("reading %s \n", input_file); uint32_t sampleRate = 0; uint64_t samplesize = 0; int16_t *pcmdata = wavRead_int16(input_file, &sampleRate, &samplesize); float *inputBuffer = (float *) calloc(sizeof(float), N); fft_complex *outBuffer = (fft_complex *) calloc(sizeof(fft_complex), N); int sect = 0; int cnt = 0; int numHashes = 0; for (int i = 0; i < samplesize; i++) { if (sect < N) { inputBuffer[sect] = (float) pcmdata[i]; sect++; } else { sect = 0; i -= 1; cnt++; fft_plan plan = fft_plan_dft_r2c_1d(N, inputBuffer, outBuffer, 0); fft_execute(plan); fft_destroy_plan(plan); int freq1 = 0, freq2 = 0, freq3 = 0, freq4 = 0, freq5 = 0; int pt1 = 0, pt2 = 0, pt3 = 0, pt4 = 0, pt5 = 0; int freqbandWidth2 = freqbandWidth * 2; int freqbandWidth3 = freqbandWidth * 3; int freqbandWidth4 = freqbandWidth * 4; int freqbandWidth5 = freqbandWidth * 5; int freqbandWidth6 = freqbandWidth * 6; for (int k = freqbandWidth; k < freqbandWidth6; k++) { int freq = (outBuffer[k].real > 0) ? (int) outBuffer[k].real : (int) (0 - outBuffer[k].real); int Magnitude = (int) (log10f((freq + 1)) * 1000); if (k >= freqbandWidth && k < freqbandWidth2 && Magnitude > freq1) { freq1 = Magnitude; pt1 = k; } else if (k >= freqbandWidth2 && k < freqbandWidth3 && Magnitude > freq2) { freq2 = Magnitude; pt2 = k; } else if (k >= freqbandWidth3 && k < freqbandWidth4 && Magnitude > freq3) { freq3 = Magnitude; pt3 = k; } else if (k >= freqbandWidth4 && k < freqbandWidth5 && Magnitude > freq4) { freq4 = Magnitude; pt4 = k; } else if (k >= freqbandWidth5 && k < freqbandWidth6 && Magnitude > freq5) { freq5 = Magnitude; pt5 = k; } } char buffer[50]; sprintf(buffer, "%d%d%d%d%d", pt1, pt2, pt3, pt4, pt5); unsigned long hashresult = hash(buffer) % maxElems; int key = (int) hashresult; if (key < 0) printf("Invalid key %d\n", key); hashtable[key][songid]++; numHashes++; } } free(pcmdata); free(inputBuffer); free(outBuffer); return numHashes; } int main(int argc, char *argv[]) { printf("Audio Processing\n"); printf("shazam audio hash\n"); printf("blog: http://cpuimage.cnblogs.com/\n"); int N = 512; int freqbandWidth = 50; int maxSongs = 10; size_t maxElems = 200000; int **hashTable; int i = 0, n = 0; float count = 0; int numsongs = 0; char filenames[maxSongs + 1][_TINYDIR_FILENAME_MAX]; int filesizes[maxSongs + 1]; int songScores[maxSongs + 1]; float songMatch[maxSongs + 1]; printf("running... \n"); if (argc < 2) { printf("no excerpt file to open \n"); exit(1); } double start_total = now(); hashTable = (int **) calloc(maxElems, sizeof(int *)); for (i = 0; i < maxElems; i++) hashTable[i] = (int *) calloc(maxSongs + 1, sizeof(int)); printf("Generating hashes for original files.. \n"); tinydir_dir dir; tinydir_open(&dir, "data"); while (dir.has_next) { tinydir_file file; tinydir_readfile(&dir, &file); if (file.is_reg) { numsongs++; double startTime = now(); filesizes[numsongs] = generateHashes(file.path, hashTable, numsongs, N, freqbandWidth, maxElems); size_t time_interval = (size_t) (calcElapsed(startTime, now()) * 1000); songScores[numsongs] = 0; printf("%d:%d hashes for %s\n", numsongs, filesizes[numsongs], file.path); printf("Time taken: %d seconds %d milliseconds\n", time_interval / 1000, time_interval % 1000); strcpy(filenames[numsongs], file.name); } tinydir_next(&dir); } tinydir_close(&dir); printf("Generating hashes for recorded file.. \n"); generateHashes(argv[1], hashTable, 0, N, freqbandWidth, maxElems); printf("Calculating score.. \n"); for (i = 0; i < maxElems; i++) { if (hashTable[i][0] > 0) { for (n = 1; n <= maxSongs; n++) { if (hashTable[i][n] >= hashTable[i][0]) songScores[n] = songScores[n] + hashTable[i][0]; else songScores[n] = songScores[n] + hashTable[i][n];; } } } for (i = 1; i <= numsongs; i++) { songMatch[i] = ((float) songScores[i]) / ((float) filesizes[i]); printf("Score for %s = %f\n", filenames[i], songMatch[i]); if (songMatch[i] > count) { count = songMatch[i]; n = i; } } printf("Best Score: %s\n", filenames[n]); for (i = 0; i < maxElems; i++) free(hashTable[i]); free(hashTable); size_t msec = (size_t) (calcElapsed(start_total, now()) * 1000); printf("Total time taken: %d seconds %d milliseconds\n", msec / 1000, msec % 1000); return 0; }
項目地址:
https://github.com/cpuimage/shazam
稍微說明一下:
在對應文件下建一個“data”的文件夾,存放需要進行計算hash備檔的音頻文件。
然后 直接傳一個文件名過去,先計算"data"下所有文件的hash,然后計算傳的目標文件的hash。
計算hash碰撞,輸出相似度得分。
例如:
shazam_demo.exe 有沒有.wav
輸出:
running...
Generating hashes for original files..
reading data/馮心怡 - 曖昧(Cover 薛之謙).wav
1:4881 hashes for data/馮心怡 - 曖昧(Cover 薛之謙).wav
Time taken: 0 seconds 268 milliseconds
reading data/薛之謙 - 別.wav
2:3370 hashes for data/薛之謙 - 別.wav
Time taken: 0 seconds 186 milliseconds
reading data/薛之謙 - 曖昧.wav
3:4879 hashes for data/薛之謙 - 曖昧.wav
Time taken: 0 seconds 271 milliseconds
reading data/薛之謙 - 有沒有.wav
4:3938 hashes for data/薛之謙 - 有沒有.wav
Time taken: 0 seconds 214 milliseconds
reading data/趙大雄 - 有沒有(Cover 薛之謙).wav
5:3937 hashes for data/趙大雄 - 有沒有(Cover 薛之謙).wav
Time taken: 0 seconds 215 milliseconds
Generating hashes for recorded file..
reading 有沒有.wav
Calculating score..
Score for 馮心怡 - 曖昧(Cover 薛之謙).wav = 0.028478
Score for 薛之謙 - 別.wav = 0.025519
Score for 薛之謙 - 曖昧.wav = 0.026645
Score for 薛之謙 - 有沒有.wav = 1.000000
Score for 趙大雄 - 有沒有(Cover 薛之謙).wav = 0.036830
Best Score: 薛之謙 - 有沒有.wav
Total time taken: 1 seconds 413 milliseconds
這個工程只是用來練手學習思路,現在它的使命已經結束。
偷偷說一句,掃頭像,有打賞,就有猛料。
以上,權當拋磚引玉。
獨樂樂,不如眾樂樂。
若有其他相關問題或者需求也可以郵件聯系俺探討。
郵箱地址是:
gaozhihan@vip.qq.com