Pythonでオーディオスペクトラム表示

PythonでPyAudio、NumPyを使って
マイクから拾った音をリアルタイムでオーディオスペクトラム表示するものを作ってみました。

## 実行例

![Microphone-Test3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538393/98cf9265-31ab-4eac-e034-2a7d6982c74a.gif)
![Microphone-Test4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538393/cd8fecee-940b-0481-2195-1962235be3fc.gif)

## 動作確認環境

* Python : 3.8.10
* NumPy : 1.21.1
* PyAudio :0.2.11
* opencv-python : 4.5.3.56

Windows10環境だと、PyAudioをインストールしようとして
`pip install pyaudio` をしても失敗するので、以下を参考にセットアップ

https://self-development.info/python%e3%81%a7%e9%9f%b3%e5%a3%b0%e9%8c%b2%e9%9f%b3%e3%83%bb%e5%86%8d%e7%94%9f%e3%81%8c%e3%81%a7%e3%81%8d%e3%82%8bpyaudio%e3%81%ae%e3%82%a4%e3%83%b3%e3%82%b9%e3%83%88%e3%83%bc%e3%83%ab/

https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio

## 実装内容について

処理の流れとしては以下のような感じのをループで繰り返しています。

1. PyAudioを使ってマイクからサンプリングした音声データを取得
2. NumPyのFFT(高速フーリエ変換)を使って、音声データを周波数成分を取得
3. 周波数成分の値を範囲毎に合計して、表示用データを作成する
4. OpenCV使って結果を表示

###　ソースコード

```python
import pyaudio
import numpy as np
import cv2

SAMPLE_RATE = 44100             # サンプリングレート
FRAME_SIZE = 2048               # フレームサイズ
INT16_MAX = 32767               # サンプリングデータ正規化用
SAMPLING_SIZE = FRAME_SIZE * 4  # サンプリング配列サイズ
WIDTH = 800     # 表示領域の幅
HEIGHT = 600    # 表示領域の高さ

# 周波数成分を表示用配列に変換する用の行列(spectram_array)作成
#   FFT結果（周波数成分の配列)から、どの要素を合計するかをまとめた行列
spectram_range = [int(22050 / 2 ** (i/10)) for i in range(100, -1,-1)]    # 21Hz～22,050Hzの間を分割
freq = np.abs(np.fft.fftfreq(SAMPLING_SIZE, d=(1/SAMPLE_RATE)))  # サンプル周波数を取得
spectram_array = (freq <= spectram_range[0]).reshape(1,-1)
for index in range(1, len(spectram_range)):
    tmp_freq = ((freq > spectram_range[index - 1]) & (freq <= spectram_range[index])).reshape(1,-1)
    spectram_array = np.append(spectram_array, tmp_freq, axis=0)

# 表示用の変数定義・初期化
part_w = WIDTH / len(spectram_range)
part_h = HEIGHT / 100
img = np.full((HEIGHT, WIDTH, 3), 0, dtype=np.uint8)

# マイク サンプリング開始
audio = pyaudio.PyAudio()
stream = audio.open(format=pyaudio.paInt16, channels=1, rate=SAMPLE_RATE,
                    input=True, input_device_index=1, frames_per_buffer=FRAME_SIZE)

# サンプリング配列(sampling_data)の初期化
sampling_data = np.zeros(SAMPLING_SIZE)
while True:
    # フレームサイズ分データを読み込み
    frame = stream.read(FRAME_SIZE)
    # サンプリング配列に読み込んだデータを追加
    frame_data = np.frombuffer(frame, dtype="int16") / INT16_MAX
    sampling_data = np.concatenate([sampling_data, frame_data])
    if sampling_data.shape[0] > SAMPLING_SIZE:
        # サンプリング配列サイズよりあふれた部分をカット
        sampling_data = sampling_data[sampling_data.shape[0] - SAMPLING_SIZE:]

    # 高速フーリエ変換（周波数成分に変換）
    fft = np.abs(np.fft.fft(sampling_data))

    # 表示用データ配列作成
    #   周波数成分の値を周波数を範囲毎に合計して、表示用データ配列(spectram_data)を作成
    spectram_data = np.dot(spectram_array, fft)

    # 出力処理
    cv2.rectangle(img, (0,0), (WIDTH, HEIGHT), (0,0,0), thickness=-1)   # 出力領域のクリア
    for index, value in enumerate(spectram_data):
        # 単色のグラフとして表示
        cv2.rectangle(img,
                      (int(part_w * (index + 0) + 1), int(HEIGHT)),
                      (int(part_w * (index + 1) - 1), int(max(HEIGHT - value/4, 0))),
                      (255, 0, 0), thickness=-1)
    # 画面表示
    cv2.imshow("Microphone Test", img)

    # 終了キーチェック
    key = cv2.waitKey(1) & 0xFF
    if key == ord('q') or key == ord('Q') or key == 0x1b:
        break

# マイク サンプリング終了処理
stream.stop_stream()
stream.close()
audio.terminate()
```

### おまけ

かなりやっつけですが、出力処理の部分をちょっと細工して、円状に線を引くように変更すると
動画とかでよく見るようなオーディオスペクトラムの形に

```python
    # 出力処理
    cv2.rectangle(img, (0,0), (WIDTH, HEIGHT), (0,0,0), thickness=-1)   # 出力領域のクリア
    for index, value in enumerate(spectram_data):
        # 単色のグラフとして表示
        rad = (2 * np.pi) * (index / len(spectram_data))
        x1 = int(WIDTH / 2 + np.sin(rad) * 80)
        y1 = int(HEIGHT / 2 - np.cos(rad) * 80)
        rad = (2 * np.pi) * (index / len(spectram_data))
        x2 = int(WIDTH / 2 + np.sin(rad) * (80 + value/4))
        y2 = int(HEIGHT / 2 - np.cos(rad) * (80 + value/4))
        cv2.line(img, (x1, y1), (x2, y2), (255, 0, 0), thickness=2)
    # 画面表示
    cv2.imshow("Microphone Test", img)
```