Wizard Notes

Pythonを使った音楽信号分析の技術録、作曲活動に関する雑記

Python:半音ごとに音高を抽出するIIRフィルタバンクの作成と使い方 (librosa.filters.semitone_filterbank)

はじめに

音の高さの分析は,メロディや和音の推定に使われる重要な前処理です.

本ブログではこれまで,音の高さの分析手法として一般的である,定Q変換フーリエ変換を使う手法を紹介してきました.

www.wizard-notes.com

これらの手法は周波数領域で信号を処理する手法ですが,時間領域で各音高を抽出する手法としてIIRフィルタによる半音フィルタバンク (semitone filterbank)があります.

この手法は Pythonの音楽信号分析ライブラリ LibROSA に実装されているため,手軽に利用することができます.

そこで本記事では,LibROSAのIIRフィルタによる半音フィルタバンク (librosa.filters.semitone_filterbank) の使い方を説明します.

librosa.filters.semitone_filterbank の利用方法

引数・返り値

LibROSAのIIRフィルタによる音高抽出フィルタバンクはlibrosa.filters.semitone_filterbankという関数で作成することができます.

semitone_filterbank, sample_rates\
 = librosa.filters.semitone_filterbank(
    center_freqs=None, 
    tuning=0.0, 
    sample_rates=None, 
    flayout='ba')

各引数・返り値は以下のような役割となっています.

  • 返り値
    • semitone_filterbank
      • 各音高(12音平均律)のフィルタ係数
      • semitone_filterbank[k] にk番目のフィルタの係数に関するリスト
      • semitone_filterbank[0]デフォルトの中心周波数は C1=32.7 Hz
    • sample_rates
      • サンプリング周波数のリスト
      • sample_rates[k]はk番目のフィルタにおけるサンプリング周波数
        • semitone_filterbank[k]でフィルタリングする信号のサンプリング周波数はsample_rates[k]にリサンプリングする必要がある
  • 引数
    • tuning=0.0
      • 基準ピッチの周波数
      • A4=440 Hz からのずれを指定
        • tuning = 1.0で基準ピッチは1半音高くなる
    • center_freqs=None
      • 各フィルタの中心周波数
    • sample_rates=None
      • 返り値のsample_ratesと同じ形式
      • 各フィルタのサンプリング周波数
    • flayout='ba'
      • フィルタ係数の格納フォーマット

引数は利用したとしてもtuningくらいだと思います*1

なお,フィルタ係数は以下のように格納されています.

# b[] of iir filter coef (num)
>>> semitone_filterbank[0][0]
array([ 0.003, -0.025,  0.084, -0.166,  0.207, -0.166,  0.084, -0.025,
        0.003])
# a[] of iir filter coef (denom)
>>> semitone_filterbank[0][1]
array([  1.   ,  -7.775,  26.662, -52.653,  65.492, -52.538,  26.545,
        -7.724,   0.991])

半音フィルタバンクの作成

半音フィルタバンクの作成は,

semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank()

と一行で書くことができます.

以下のコードでは,半音フィルタバンクの各フィルタの周波数応答をプロットしています.

import matplotlib.pyplot as plt
import numpy as np
import scipy.signal
import librosa

fig, ax = plt.subplots()

# フィルタバンク作成
semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank()

# 各音高抽出フィルタの周波数応答プロット
for current_sr, current_filter in zip(sample_rates, semitone_filterbank):
    print(f"current_sr: {current_sr}")
    w, h = scipy.signal.freqz(current_filter[0], current_filter[1], worN=2000)
    plt.semilogx((current_sr / (2 * np.pi)) * w, 20 * np.log10(np.abs(h)))

plt.xlim([20, 10e3])
plt.ylim([-60, 3])
plt.title('Magnitude Responses of the Pitch Filterbank')
plt.xlabel('Log-Frequency (Hz)')
plt.ylabel('Magnitude (dB)')

f:id:Kurene:20210921171455p:plain:w400 f:id:Kurene:20210921171429p:plain:w400

このフィルタバンクのサンプリング周波数は以下のようになっています.

これはLibROSAの半音フィルタバンクがマルチレート処理用の実装であるためです.

f:id:Kurene:20210921171541p:plain:w400

マルチレート処理となっている理由ですが,低い音高と高い音高でフィルタ係数のタップ数が同じ場合,フィルタの解像度(Q値)が低域で粗くなってしまいます

また,解像度を等しくするようにタップ数を変えると,低域でタップ数が増え,計算量が多くなります

そこで,マルチレート処理,すなわち低域用の音高抽出フィルタを適用する信号はダウンサンプリングすることで,低い音高も高い音高も同じタップ数・同じ解像度でフィルタリングすることができます

IIRフィルタによる音高フィルタバンクの使い方

LibROSAの半音フィルタバンクを,以下のピアノロールのようにC1からハ長調で鳴るサイン波に適用してみます.

f:id:Kurene:20210922005910p:plain:w400

以下は,(左から順に)0番目から23番目,24番目から47番目,48番目から71番目のフィルタ出力の時間波形のプロットです.

先ほどのピアノロールの音高に一致する半音フィルタで信号が抽出できていることが確認できています.

ただし完全に分離できるわけではなく,隣接するフィルタに信号が漏れていることも確認できます.特に低域で顕著です.

f:id:Kurene:20210922011312p:plain:w500

以下のコードでは,semitone_signalsに各フィルタバンクの出力を格納しています.ただし,プロットのため元のサンプリング周波数にリサンプリングしています.

ここで,リサンプリングはresampy, オーディオファイル書き込みはsoundfileを使っています.

また,resampy.resampleでは計算量を小さくするためfilter='kaiser_fast'としています。

なお,scipy.signal.lfilterを使っていますが,オフライン処理で位相ずれが気になる場合はゼロ位相フィルタ処理scipy.signal.filtfiltを使ってみてください.

import matplotlib.pyplot as plt
import numpy as np
import scipy.signal
import librosa
import resampy
import soundfile as sf

## フィルタバンク作成
semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank()

x, sr_orig = sf.read("semitone_sine.wav")
x = x[:,0] + x[:,1] # to mono

semitone_signals = [] # フィルタ出力
count = 0
for current_sr, current_filter in zip(sample_rates, semitone_filterbank):
    b, a = current_filter # フィルタ係数
    # 入力信号をダウンサンプリング
    y = resampy.resample(x, sr_orig, current_sr, filter='kaiser_fast')
    # フィルタリング
    z = scipy.signal.lfilter(b, a, y)

    zz = resampy.resample(z, current_sr, sr_orig, filter='kaiser_fast')
    semitone_signals.append(zz)
    count += 1

合成(逆変換っぽい処理)

各フィルタバンクの出力は各音高の時間領域信号です.

なので,単純に各フィルタの出力を足せば元の信号っぽい信号を作ることができます

ただし,抽出する音高以外の周波数(C1 31.7Hz 未満,C8 4186Hz より上)はそもそも抽出できていないので復元できません.また,抽出した音高であっても音質に難があるので工夫が必要です*2

以下の動画で、実際に合成した信号を聞いて比較できます*3((filter='kaiser_best'とすると計算量は増えますが音質はもう少し良くなると思います)).

youtu.be

# 時間波形サンプル数は一番短い信号に合わせる
min_len = len(semitone_signals[0]) 
# 加算
sum_semitone_signal = np.sum(np.array([x[0:min_len] for x in semitone_signals]), axis=0)
# 書き込み
sf.write("sum_semitone_signal.wav", sum_semitone_signal, sr_orig)

ある音高の時間波形の抽出

特定の音高の時間波形を取り出すことができます.

sf.write("C3.wav", semitone_signals[24], sr_orig)

まとめ

LibROSAに実装されている,半音ごとに音高を抽出するIIRフィルタバンク(librosa.filters.semitone_filterbank)の作成と使い方を説明しました.

IIR半音フィルタバンクを定Q変換などの周波数領域での音高抽出手法と比較すると,

  • 出力が時間波形
    • 特定の帯域だけの抽出してプロットしたり聞くことができる
    • 位相情報を扱いやすい
    • 時間領域での処理と組み合わせやすい
  • フィルタバンクのカスタマイズが柔軟
    • A1のフィルタだけフィルタリングすることもできる
  • マルチレート処理によってリアルタイムでの利用も不可ではない
  • 合成処理(疑似的な逆変換)もできなくはない

といったメリットがあると思います.

面倒な逆変換処理をせずに抽出/合成信号をプロットしたり聞いたりできるのはこの手法の強みだと思います.

一方で,

  • IIRフィルタなので波形が歪む
  • マルチレート処理で品質を優先すると計算量が大きい
  • タップ数が多いと計算量が大きい
  • 抽出・合成信号の音声品質がイマイチ

といったデメリットがあるので,音楽コンテンツとして十分な音質の信号を作るのには工夫が必要です。

*1:クラシック・民族音楽の分析の時とか

*2:フィルタのタップ数を増やす,残差信号を保持しておくなど

*3:lfilterの場合はエコーが,filtfiltの場合はプリエコーがかかっているように聴こえます.若干filtfiltの方がlfilterよりも音質がよいかもしれません