Pythonでエフェクターを作成してみた

# はじめに
本記事で扱うエフェクターとは、音に対して何らかの効果を与える機器を指します。  
例えば、
* 山びこを再現（ディレイ）
* コンサートホールの中のような響きを再現（リバーブ）
* 複数人でしゃべっているような音にする（コーラス）

のようにできることは本当に様々です。  
試しに島村楽器さんのページで確認したところ、1万5000件程度(!!)ヒットしました。  
https://store.shimamura.co.jp/ec/Facet?category_0=11080000000

もしあなたがロックバンドのライブに行く機会があったら、ギタリスト／ベーシストさんの足元に注目してみてください。  
小さな四角い箱がいくつか並んでいて、足で操作されているところを見かけるかと思います。  
その箱1つ1つをエフェクターと呼び、ギターやベースの音を変化させているのです。  

本記事ではこのエフェクターを、Pythonを用いてソフトウェアとして作成します。

## 今回扱う内容
先述の通り、エフェクターはかなりの数存在します。  
そのため今回は、以下の内容に絞って記載します。  

* Pythonを用いたwavファイルの再生／一時停止／停止処理の作成
* 歪みエフェクト「ディストーション」の作成

# 環境
今回用いた環境は以下の通りです。  
* macOS Sonoma 14.3.1
* Python 3.11.8

# 実装
## 全体コード

今回作成した全体コードは、以下に配置しています。  
https://github.com/imura-nalgo/py-effector

## インストール
今回は「PyAudio」でwavファイルを扱います。  
以下のコマンドを実行して、PyAudioをインストールします。  
```
brew update
brew install portaudio
brew link --overwrite portaudio
pip install pyaudio
```

## wavファイルの再生
Playerクラスを作成して、wavファイルを扱う処理をまとめました。  
一般的なオーディオプレイヤーに近い操作感となるよう設計しました。  

キモとなるのは以下のコードです。
```python
def __run(self):
    while self.status == Status.PLAY:
        frames = self.w.readframes(CHUNK)

    # ==== 加工処理 ====            
    data = np.frombuffer(frames, dtype=np.int16)

    if self.board is not None:
        data = self.board.effect(data)
    # ==== 加工処理 ====

    if len(data) > 0:
        self.stream.write(data.tobytes())
```

* `w`(wavファイルオブジェクト)からデータを読み込んで、`stream`に流すと音が出ます。
  * `w`には音声データ(音声波の振幅値をごく短い時間間隔でサンプリングした配列)が入っています。
* この音声データをstreamに渡すまでに加工することで、音に効果を与えられます。
  * すなわち、下記ソースの`board.effect()`に加工処理を実装していくことで、エフェクターを作成していきます。

## 音声データの加工
### エフェクター親クラスの作成
これからエフェクターを実装するとき、各々クラスを作成する予定です。  
ただ無秩序に作りたくないので、親クラスで必ず実装が必要なメソッドを定義し、これを継承することとします。  

```python
from abc import ABCMeta, abstractmethod
import numpy as np

from parameters.parameters import Parameters

class Effector(metaclass=ABCMeta):
    def get_name(self):
        return self.name

    @abstractmethod
    def set_parameters(self, parameters: Parameters):
        """
        パラメータを設定する

        引数:
            parameters: Parameters
                パラメータ
        """
        pass

    @abstractmethod
    def effect(self, input: np.ndarray) -> np.ndarray:
        """
        入力の音声信号に対して効果を加える

        引数:
            input: np.ndarray
                音声信号
        
        戻り値:
            output: np.ndarray
                加工した音声信号
        """
        raise NotImplementedError()
```

### エフェクターボードの作成
エフェクターは複数を組み合わせて使用することが一般的です。  
そのためまずは、エフェクターをリストで管理するクラス「EffectorBoard」を実装しました。  
ギタリストさん達がエフェクターを入れるケースのことを「エフェクターボード」と呼ぶので、ここから取った名前です。

```python
import numpy as np
from effectors.effector import Effector

class EffectorBoard:
    def __init__(self, effectors: list[Effector]) -> None:
        self.effectors = effectors

    def print_effector_chain(self) -> None:
        print(" -> ".join([e.get_name() for e in self.effectors]))
    
    def effect(self, input: np.ndarray) -> np.ndarray:
        output = input
        for effector in self.effectors:
            output = effector.effect(output)

        return output
```
* 複数のEffectorの加工処理を順番に実行します。  
* 先ほどエフェクターの親クラスを定義したのは、複数エフェクターの加工処理が`effector.effect(output)`だけで済むというのも理由の1つです。  

### 歪みエフェクト「ディストーション」の作成
今回は入門編として、簡単な歪みエフェクト「ディストーション」を作成してみます。  
エレキギターに対して特に使われることが多く、ロック等でよく聞く激しい音の正体です。

考え方を以下の図に示します。  
音を増幅し、はみ出した部分をバッサリカットしていくと歪んだ音になっていきます。  
![distortion.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3341817/825b558d-4645-fbc0-fc49-7db837fea427.png)


この考え方を実装したのが以下です。
```python
from effectors.effector import Effector
import numpy as np

from parameters.distortion_parameters import DistortionParameters

class Distortion(Effector):
    def __init__(self) -> None:
        self.name = "Distortion"
        self.drive = 10
    
    def set_parameters(self, parameters: DistortionParameters):
        self.drive = parameters.drive
    
    def effect(self, input: np.ndarray) -> np.ndarray:
        """
        ディストーションを実装する

        引数:
            input: np.ndarray
                音声信号

        戻り値:
            output: np.ndarray
                加工した音声信号
        """

        # inputはnp.int16でわたってくる。
        # driveをかけた際にオーバーフローが発生するのを防ぐため、一旦np.int64に変換する。
        output = input.astype(np.int64) * self.drive
        output = np.minimum(32767, output)
        output = np.maximum(-32768, output)

        return output.astype(np.int16)
```

effectメソッドにディストーション処理を実装しました。  
先述した考え方の通り実装していることがわかるかと思います。

### 試聴
それっぽいギターの音源を用意し、エフェクターをかける前後で聴き比べてみました。  
以下が試聴ページになります。  
https://imura-nalgo.github.io/effector-trial/

かけた後はなんとなくギターの音っぽくなっているのではないでしょうか。  

## 今後
* より多くの種類のエフェクターを実装してみたいです。
* GUIを実装してみたいです。