本文檔針對一個音訊指紋(Music Fingerprint)演算法類別,名為 FingerprintIdentifier。此演算法類似於知名的 Shazam 音樂檢索原理,在本類別中,大致包含下列步驟:
- 頻譜峰值偵測(Sparse Constellation/Peaks)
- 指紋產生(Combinatorial Hash / Landmark Hash)
- 相似度檢測(Offset Histogram / Minimum match threshold)
此類別由 Python 與 librosa 完成,並適用於小型或片段式音訊比對。
此演算法參考自
- Shazam 音樂指紋核心機制 Shazam 相關技術論文(Columbia University): https://www.ee.columbia.edu/~dpwe/papers/Wang03-shazam.pdf
在該論文中,Shazam 演算法架構大致可分為:
- Spectrogram peak detection
- Combinatorial hashing (對 peak 做配對)
- Searching & Scoring (依 offset histogram 找線索)
簡介
FingerprintIdentifier 用於辨識兩段音訊是否「相同音訊(音樂)片段」,核心概念是:
- 對音訊做短時傅立葉轉換(STFT)
- 偵測 2D 頻譜峰值 → 形成星座圖 (constellation map)
- 對峰值做 Landmark Hash → (freqA, freqB, Δt)
- 比對 offset histogram → 取得最高對齊分數 → 若≥ min_count → 視為匹配
適用範圍:
- 小型或片段式音訊(數秒 ~ 數十秒)比對
- 較高雜訊耐受度
- 若要辨識大檔案時多半搭配分段 / streaming
def __init__(self,
sr=16000,
n_fft=2048,
hop_length=512,
peak_threshold=-30.0,
peak_neighborhood=3,
fan_value_frames=5,
min_count=8):
參數:
sr
(int):預設為 16000 Hz,音訊採樣率 (samples/秒),做 STFT 時假設音訊為此取樣率。n_fft
(int):預設為 2048,STFT 所用之 FFT window size。影響頻率解析度,越大頻率解析度越高,但時間解析度較低。。hop_length
(int):預設為 512,STFT 的 hop size(兩幀之間的位移)。影響時間軸解析度(越小解析度越佳)peak_threshold
(float):預設為 -30.0,在 dB 頻譜中,低於此值的點不視為 peak。避免雜訊峰。peak_neighborhood
(int):預設為 3,2D peak detection 之鄰域大小。檢測局部最大時,以 freq±3、time±3 做局部搜尋。fan_value_frames
(int):單位為幀數,預設為 5,建立 Landmark Hash 時,每個 anchor peak 向後查看多少 frames 之內的 peak 搭配成 pair;對應約 5×hop_length 的時間門檻。min_count
(int)*:預設為 8,在 offset histogram 中,若最大值≥此 → 視為成功匹配。數字越大 → false positive 越低,但漏判機率也上升。
def detect_peaks_2d(self, S_db):
描述:
偵測頻譜圖中的 2D 峰值,亦即該點的 dB 值不僅高於 peak_threshold
,還需要是它所在之「鄰域 (freq±N, time±N)」裡的最大值。
參數:
S_db
(ndarray):以 dB 為單位的 2D 頻譜圖,大小為 (頻率, 時間)。
返回值:
List[Tuple[int, int, float]]
:峰值列表,每個峰值的格式為 (頻率, 時間, dB 值)。
大致運作原理:
- 遍歷頻譜圖的每個點
(f, t)
。 - 確定該點的鄰域範圍(根據
peak_neighborhood
)。 - 檢查該點是否為局部極大值,且超過
peak_threshold
。 - 若符合條件,將該點加入峰值列表。
範例: [ [25, 30, 20], [22, 35, 28], [18, 22, 30] ] 例如在此矩陣中,35 為局部最大值,它的值比周圍的點都高
程式區塊說明
freq_len, time_len = S_db.shape
peaks = []
- S_db.shape: 獲取頻譜圖的大小,freq_len 為頻率維度的長度,time_len 為時間維度的長度。
- peaks: 用於存儲所有偵測到的峰值。
for t in range(time_len):
for f in range(freq_len):
val = S_db[f, t]
if val < self.peak_threshold:
continue
- 外層和內層雙層迴圈,遍歷每個頻譜點
(f, t)
。 val = S_db[f, t]
: 提取該點的能量值(以 dB 為單位)。if val < self.peak_threshold
: 濾除低於閾值的點,加速計算。
fmin = max(0, f - self.peak_neighborhood)
fmax = min(freq_len, f + self.peak_neighborhood + 1)
tmin = max(0, t - self.peak_neighborhood)
tmax = min(time_len, t + self.peak_neighborhood + 1)
local_patch = S_db[fmin:fmax, tmin:tmax]
- 設置局部範圍,確保在頻率和時間方向上不越界。
local_patch
: 提取(f, t)
周圍的能量值子矩陣。
if val >= np.max(local_patch):
peaks.append((f, t, val))
- 判斷該點是否是
local_patch
中的局部極大值。 - 如果是,將
(f, t, val)
加入peaks
列表。
def build_fingerprint(self, peaks):
描述: 將偵測到的峰值生成指紋結構,指紋用於快速匹配。
參數:
peaks
(List[Tuple[int, int, float]]):由 detect_peaks_2d 偵測到的峰值列表。
返回值:
- defaultdict:指紋結構,鍵為 (freqA, freqB, dt),值為時間偏移量列表 [timeA1, timeA2, ...]。
運作原理:
- 將峰值列表按時間排序,保證指紋生成順序一致。
- 使用兩個變數(
i
和j
)將每個峰值與其後的峰值進行配對,時間差需在fan_value_frames
範圍內。 - 對合法配對生成
hashkey
,記錄其時間偏移量timeA
。
程式區塊說明
peaks_sorted = sorted(peaks, key=lambda x: x[1])
n_peaks = len(peaks_sorted)
hash_dict = defaultdict(list)
peaks_sorted
: 按時間對峰值進行排序,保證處理順序一致。hash_dict
: 使用字典存儲指紋結構,鍵為 hashkey,值為時間偏移量列表。
for i in range(n_peaks):
freqA, timeA, valA = peaks_sorted[i]
if j < i+1:
j = i+1
- 外層迴圈遍歷每個峰值
(freqA, timeA)
作為錨點。 j = i+1
: 確保內層迴圈只配對後面的峰值,避免重複配對。
while j < n_peaks:
freqB, timeB, valB = peaks_sorted[j]
dt = timeB - timeA
if dt > self.fan_value_frames:
break
if dt > 0:
hashkey = (freqA, freqB, dt)
hash_dict[hashkey].append(timeA)
j += 1
- 配對範圍限制: 只有時間差
dt
在fan_value_frames
範圍內才配對,超出則跳出內層迴圈。 - 生成指紋鍵:
(freqA, freqB, dt)
作為唯一的hashkey
。 - 存儲偏移量: 每個
hashkey
都與錨點的時間偏移timeA
關聯。
def identify(self, ref_audio, sample_audio):
描述: 比較參考音檔與樣本音檔,通過計算匹配的偏移數量,判斷是否匹配。
參數:
ref_audio
(ndarray):參考音檔的波形數據。sample_audio
(ndarray):樣本音檔的波形數據。
返回值:
- Tuple[bool, int]:包含以下兩部分:
is_match
(bool):若匹配則為 True,否則為 False。best_count
(int):最高匹配數量。
運作原理:
-
參考音檔處理:
- 計算 STFT,轉換為 dB 頻譜圖。
- 偵測峰值並生成指紋。
-
樣本音檔處理:
- 計算 STFT,轉換為 dB 頻譜圖。
- 偵測峰值並生成偏移直方圖。
-
匹配計算:
- 比較樣本峰值和參考指紋的
hashkey
。 - 根據偏移直方圖的最高匹配數量判斷是否匹配。
- 比較樣本峰值和參考指紋的
程式區塊說明 第一步:處理長音檔(參考音檔)
D_ref = librosa.stft(ref_audio, n_fft=self.n_fft, hop_length=self.hop_length, center=False)
S_ref = np.abs(D_ref)
S_db_ref = librosa.amplitude_to_db(S_ref, ref=np.max)
librosa.stft
:
- 對參考音檔執行短時傅里葉變換(STFT),將時間域信號轉換為頻譜圖。
- 參數:
- n_fft:FFT 點數,影響頻率分辨率。
- hop_length:幀間距,影響時間分辨率。
- center=False:禁止對信號進行填充,以防止中心化導致數據不連續。
- 結果:D_ref 是一個複數矩陣,表示頻率和時間的頻譜。
np.abs
:
- 計算頻譜的幅度(取模),去掉相位信息。
- 結果:S_ref 是幅度矩陣。
librosa.amplitude_to_db
:
- 將幅度轉換為分貝(dB),以對數尺度表示信號強度。
- 結果:
S_db_ref
是以 dB 為單位的頻譜圖。
peaks_ref = self.detect_peaks_2d(S_db_ref)
ref_fp = self.build_fingerprint(peaks_ref)
第二步:處理短音檔(樣本音檔)
D_samp = librosa.stft(sample_audio, n_fft=self.n_fft, hop_length=self.hop_length, center=False)
S_samp = np.abs(D_samp)
S_db_samp = librosa.amplitude_to_db(S_samp, ref=np.max)
- 與參考音檔處理步驟相同,將樣本音檔轉換為 dB 頻譜圖。
peaks_samp = self.detect_peaks_2d(S_db_samp)
- 偵測樣本音檔的峰值,得到峰值列表。
offset_map = defaultdict(int)
- 初始化偏移直方圖,用於記錄偏移量及其匹配數量。
第三步:偏移直方圖計算
peaks_samp_sorted = sorted(peaks_samp, key=lambda x: x[1])
n_samp_peaks = len(peaks_samp_sorted)
- 將樣本峰值列表按時間排序,確保處理順序一致。
for i in range(n_samp_peaks):
freqA, timeA, valA = peaks_samp_sorted[i]
if j < i + 1:
j = i + 1
while j < n_samp_peaks:
freqB, timeB, valB = peaks_samp_sorted[j]
dt = timeB - timeA
if dt > self.fan_value_frames:
break
if dt > 0:
hashkey = (freqA, freqB, dt)
sample_offset = timeA
if hashkey in ref_fp:
ref_offsets = ref_fp[hashkey]
for ref_offset in ref_offsets:
offset_diff = ref_offset - sample_offset
offset_map[offset_diff] += 1
j += 1
- 使用兩個變數(
i
和j
)配對樣本峰值。 - 根據
fan_value_frames
限制配對的時間範圍。 - 若樣本的
hashkey
與參考指紋中的hashkey
匹配,計算偏移量offset_diff
並記錄在直方圖中。
第四步:判斷匹配結果
if not offset_map:
return (False, 0)
- 若偏移直方圖為空,表示沒有找到匹配,直接返回。
best_off, best_count = max(offset_map.items(), key=lambda x: x[1])
is_match = (best_count >= self.min_count)
return (is_match, best_count)
- 找到偏移直方圖中最大峰值對應的匹配數量
best_count
。 - 判斷
best_count
是否超過門檻min_count
,確定是否匹配。