跳轉到

8.1 音高偵測與調音器#

概念說明#

調音器(tuner)是每個樂手口袋裡的聲音演算法:對準樂器發出的單音,告訴你「偏低多少音分」。它是音高偵測(2.3、5.3 節)最直接的消費級應用,但與語音 \(f_0\) 追蹤相比有獨特需求:極高的頻率精度(±1 音分以內)與對樂器泛音結構的穩健性。

音分(cent)是音高的對數單位:一個八度(頻率 2 倍)等於 1200 音分,每半音 100 音分。給定偵測頻率 \(f\) 與最接近的標準音 \(f_{std}\) :

\[ \text{cents} = 1200 \log_2\!\left(\frac{f}{f_{std}}\right) \]

±1 音分對應頻率相對誤差僅 0.058%——在 A4 = 440 Hz 上是 ±0.25 Hz。這決定了演算法的精度需求:DFT 的頻率解析度 \(f_s/N\) (8 kHz、4096 點 ≈ 2 Hz)遠遠不夠,必須依靠拋物線內插(對峰值鄰點做二次曲線擬合,精度可達解析度的 1/10–1/100)或直接在時域精測週期。

調音器的標準管線(以 YIN 演算法為主流):

  1. 取 100–200 ms 視窗(夠長以容納低音吉他的 82 Hz 週期數個)。
  2. 計算平均方差距離函式(YIN 的 \(d(\tau)\) ): \(d(\tau) = \sum_{n} (x[n] - x[n+\tau])^2\) ,等價於「去直流的自相關」形式,對振幅漂移更穩健。
  3. 累積均值正規化: \(d'(\tau) = d(\tau) / \left[\frac{1}{\tau}\sum_{j \le \tau} d(j)\right]\) ,使「第一個降到門檻以下的谷」被優先選中——這是 YIN 防止倍頻錯誤(把 220 報成 440)的關鍵。
  4. 找第一個低於門檻(如 0.1)的區域極小值 \(\tau_0\) 。
  5. 拋物線內插精化 \(\tau_0\) ,得 \(f_0 = f_s/\hat\tau\) 。
  6. 換算音分、顯示。

另外,HPS(5.3 節)與 McLeod Pitch Method(MPM,NSDF + 峰值挑選)也是調音器常用演算法;MPM 在真實樂器(含瞬態)上表現尤佳。

演算法步驟:YIN 調音器#

  1. 錄音取窗(建議 \(f_s = 8\) –48 kHz、128 ms),去直流。
  2. 計算 \(d(\tau)\) , \(\tau \in [\tau_{min}, \tau_{max}]\) (對應 60–1200 Hz)。
  3. 計算累積均值正規化 \(d'(\tau)\) 。
  4. 由小到大掃描,取第一個 \(d'(\tau) < 0.1\) 的極小值;若無,取全域最小值並檢查是否可信( \(d' < 0.5\) )。
  5. 拋物線內插:以谷點及兩鄰點擬合二次曲線,取頂點為 \(\hat\tau\) 。
  6. \(f_0 = f_s/\hat\tau\) ;對照標準音(十二平均律: \(f_{std} = 440 \cdot 2^{(n-69)/12}\) , \(n\) 為 MIDI 音號),輸出音名與音分偏差。

Python 範例#

import numpy as np

fs = 44100

def yin_pitch(x, fs, fmin=60, fmax=1200, thresh=0.1):
    x = x - x.mean()
    W = len(x)//2
    taus = np.arange(int(fs/fmax), int(fs/fmin))
    d = np.array([np.sum((x[:W] - x[k:k+W])**2) for k in taus])
    # 累積均值正規化
    d_prime = d.copy()
    d_prime[1:] = d[1:] * np.arange(1, len(d)) / np.maximum(
        np.cumsum(d)[:-1], 1e-12)
    # 找第一個低於門檻的谷
    k = None
    for i in range(1, len(d_prime)-1):
        if d_prime[i] < thresh and d_prime[i] <= d_prime[i+1]:
            k = i; break
    if k is None:
        k = int(np.argmin(d_prime))
        if d_prime[k] > 0.5: return None, 0.0
    # 拋物線內插
    a, b, c = d_prime[k-1], d_prime[k], d_prime[k+1]
    shift = 0.5*(a - c)/(a - 2*b + c + 1e-12)
    tau = taus[k] + shift
    return fs/tau, 1 - d_prime[k]

NOTE_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
def to_note(f, a4=440.0):
    n = round(69 + 12*np.log2(f/a4))
    cents = 1200*np.log2(f / (a4*2**((n-69)/12)))
    return f"{NOTE_NAMES[n%12]}{n//12 - 1}", cents

# 模擬樂器音:440 Hz 基頻 + 泛音(吉他弦的衰減諧波)
t = np.arange(0, 0.2, 1/fs)
for true_f0, label in [(440.0, "A4 準確"), (436.8, "A4 偏低約13音分"),
                        (82.41, "E2 低音弦")]:
    x = sum(1/r*np.exp(-3*t)*np.sin(2*np.pi*true_f0*r*t) for r in range(1, 8))
    f0, conf = yin_pitch(x, fs)
    note, cents = to_note(f0)
    print(f"目標 {label:16s} 真值 f0={true_f0:7.2f} Hz -> "
          f"偵測 {f0:7.2f} Hz, 判定 {note:4s} ({cents:+6.1f} 音分), 信心 {conf:.3f}")

輸出:

目標 A4 準確          真值 f0= 440.00 Hz -> 偵測 440.00 Hz, 判定 A4  (  +0.0 音分), 信心 0.999
目標 A4 偏低約13音分 真值 f0= 436.80 Hz -> 偵測 436.80 Hz, 判定 A4  ( -12.6 音分), 信心 0.999
目標 E2 低音弦       真值 f0=  82.41 Hz -> 偵測  82.40 Hz, 判定 E2  (  -0.2 音分), 信心 0.999

三個案例(含泛音豐富的衰減弦音、低至 82 Hz 的低音弦)的偵測誤差都在 ±0.3 音分內——遠優於人耳 ±5 音分的分辨能力。第二例刻意設定 13 音分的偏差(調音器螢幕會顯示指針偏左),演算法也精確量出。若移除拋物線內插,44100/整數樣本的最粗解析度在 440 Hz 附近就有 ±16 音分的誤差——內插是調音器精度的生命線。

複雜度分析#

\(W\) 為半窗長、 \(K\) 為延遲搜尋數: \(d(\tau)\) 直接計算為 \(O(WK)\) ,是瓶頸;以 128 ms 窗、44.1 kHz、60–1200 Hz 計, \(W \approx 2800\) 、 \(K \approx 700\) ,約 200 萬次乘減——單次約幾毫秒,調音即時無虞。YIN 原論文給出 \(O(WK)\) 的差分遞迴可再省一半;亦可用 FFT 計算自相關後轉換為 \(d(\tau)\) ,降至 \(O(W \log W)\) ,適合連續追蹤。MPM/HPS 同為 \(O(N\log N)\) 量級。調音器工程的真正難點在暫態處理(撥弦瞬間雜訊大,需跳過前 30–50 ms)與八度錯誤防護(YIN 的累積正規化即為此設計),而非計算速度。連續追蹤模式(如 chromatic tuner 對滑音)需再加上中值濾波與滯後邏輯避免顯示跳動。

練習#

  1. 移除拋物線內插,量測 440–453 Hz 範圍內各整數頻率的偵測誤差(音分),驗證「±16 音分」的量化誤差,並計算內插後的誤差上限。
  2. 在訊號中加入第二個同時發聲的音(純五度 660 Hz),測試 YIN 是否鎖定基音;再改用 HPS(5.3 節)比較,說明多音場景下調音器為何建議「一次彈一根弦」。
  3. 實作「自動標準音」:輸入交響樂團的 442 Hz 基準,讓 to_note 以 442 為 A4 重算,並檢驗 440 Hz 的音在 442 基準下顯示約 −8 音分。