Pythonでイコライザーを作成してみた

## はじめに
この記事は以前投稿した記事の続きです。  
[Pythonでエフェクターを作成してみた](https://qiita.com/h-imura/items/a0b8db9f95ef6a3e9d02)  

今回は新たなエフェクター（イコライザー）を実装しました。  
ちなみに前回投稿時に比べてGUIもさらっと追加していますが、こちらの説明はまたの機会としたいと思います。  

## イコライザーとは？
イコライザー（Equalizer）とは、音声の周波数帯ごとに音量調節ができるエフェクターです。  
低音を大きくしたい場合は低音の周波数帯の音量を上げたり、キンキン聞こえる場合は高音の周波数帯の音量を下げるというような使い方をします。  
Macのミュージックアプリには標準搭載されていて、意外と皆さんがお使いの音楽プレイヤーにもついているかもしれません。  
[Macの「ミュージック」で音質を調節する](https://support.apple.com/ja-jp/guide/music/museb684a3de/mac)

もともとは録音マイクやスタジオ等、環境による音質の差異を均質化（= Equalize）する目的で使われていたそうです。  
これが「イコライザー」という名前なのだとか。  

## 今回の完成品とソースコード
いきなりですが、今回作成したイコライザーの画面です。  
200Hz, 400Hz, ... ,12800Hzでそれぞれに音量を増減することができます。  
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/c32a7b38-1e94-4b15-adff-575d41ea77f9.png">
ソースコードは以下に置いています。  
https://github.com/imura-nalgo/py-effector

## ピーキングフィルタの実装
フィルタとは、ある周波数を基準に音量を増減することができるものです。  
一般には、音声だけでない幅広い分野の信号を処理するために用いられます。  
先述した200Hz～12800Hzそれぞれにフィルタを作成することで、イコライザーにします。  

フィルタにはいろいろな種類がありますが、その中の「ピーキングフィルタ」を実装します。  
ただ、ちゃんとやろうとすると数学の知識が必要となって辛いです。  
今回はそれらを抜きに説明していただいている、以下の記事を参考しました。  
[簡単なデジタルフィルタの実装](https://www.utsbox.com/?page_id=523)

ざっくりとした実装手順は次のとおりです。
* 周波数、音量をインプットとするフィルタ係数を作成する。
* フィルタ係数、音声データをインプットとするフィルタを作成する。

以下実際のコードです。まずはフィルタ係数作成部分。  
```python:effectors/equalizer.py
    def _calc_peaking_coefficients(
        self, bw: float, gain: float, f: float, sample_rate: float
    ) -> np.ndarray:
        # 計算式は以下を参照
        # https://www.utsbox.com/?page_id=523
        # bw: 帯域幅[octave]
        # gain: 音量[db]
        # f: 周波数[Hz]
        # sample_rate: サンプリングレート
        omega = 2.0 * np.pi * f / (sample_rate * globals.CHANNELS)
        alpha = np.sin(omega) * np.sinh(np.log(2.0) / 2.0 * bw * omega / np.sin(omega))
        A = 10 ** (gain / 40.0)

        a = np.array([1.0 + alpha / A, -2.0 * np.cos(omega), 1.0 - alpha / A])
        b = np.array([1.0 + alpha * A, -2.0 * np.cos(omega), 1.0 - alpha * A])

        return b, a
```

参考記事にあるピーキングフィルタの計算式をほぼまるっと実装した形になります。  
次はフィルタの部分です。  
```python:effectors/equalizer.py
from scipy import signal as sg
(中略)
def filter(self, input: np.ndarray[np.int16]) -> np.ndarray:
    output, self.zi = sg.lfilter(self.b, self.a, input, zi=self.zi)
    return output
```

こちらはscipyで用意されていた関数を実行しました。  
`self.b`, `self.a`が前述のフィルタ係数、`input`が音声データです。`self.zi`はフィルタの初期状態を保管しておく変数です。  
`input`には全ての音声データではなく、細かく分割されたチャンクデータが入ってきます。  
よってチャンクごとにフィルタ関数が実行されるのですが、前後のフィルタ結果をなめらかにつなげるために初期状態が必要となります。  
この初期状態なしで動作を確認したときは、ノイズが発生してしまいました。  
（このあたりのお話は「過渡応答」がキーワードになります）  

## テスト
200Hz, 400Hz, 800Hz, 3200Hz, 12800Hzの正弦波を合成した音源を作成しました。  
周波数分布を確認すると以下になります。横軸が周波数[Hz]で、縦軸が音量[dB]です。  
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/5550455a-30b0-43b4-8adc-d09b3c9c2be1.png">
ちなみに音にすると以下の感じ(⚠️とてもうるさいので音量注意です⚠️)  。  
https://imura-nalgo.github.io/effector-trial/equalizer.html#sin

この音源に対して、イコライザーで400Hzの音量を上げます。  
<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/4da8801b-7e91-4a2d-be07-98198500d436.png">
すると周波数分布は以下になります。   
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/ed019b9e-30b7-4eb7-b2c5-806a9f19e6d9.png">   
400Hzの部分のみ音量が増加していることがわかると思います。  

:::note warn
正確には400Hzの近傍(200Hz, 800Hz)も若干音量が増加しています。  
近傍の周波数への影響は、フィルタ係数のインプットであるbw(帯域幅)で調整することができます。  
:::

反対に、400Hzの音量を減らした場合は以下のようになります。  
<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/bff05a90-06b0-4f02-8950-21b661b1423e.png">  
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/ec64286e-2bfd-4763-97fc-f70e7d95ac12.png">  

## 聞いてみる
実際の楽曲に対してイコライザーをかけてみました。  
https://imura-nalgo.github.io/effector-trial/equalizer.html

200Hzは低域の周波数となるため、カットするとスカスカな印象になると思います。  
反対に6400Hzは中・高域の周波数のため、カット版はこもった印象になっていると思います。

## まとめ
Pythonを使ってイコライザーを実装することができました。  
前回のディストーションと比べて難しい内容となりましたが、その分やりがいがあって楽しかったです。