【Pyaudio】を使って録音と再生をする(Windows)

# 概要
Windowsのデバイスから出力される音声を録音するコードです。

リモート会議の音声の録音などができます。筆者が以前書いた「 [WindowsでWhisperとpysummarizationを使って音声データの文字起こしと要約させてみた](https://qiita.com/TEST_KEN/items/562d51d79da28f055544) 」と組み合わせると、録音した会議を自動で文字起こしし、かつ要約もできるようになると思います。

## 利用するライブラリ

- 録音に使うライブラリ：『pyaudio』というライブラリを使います。

```
datetime
wave
time
pyaudio
```

### ライブラリの入れ方

```pyaudio```の入れ方は```pip install pyaudio```でできます。


### デバイスから出力される音声を録音するための下準備

具体的なコードを掲載する前に、デバイスから出力される音声を録音するためには下準備が必要です。具体的にはループバック録音をできるように設定します。ループバック録音の設定の仕方を次から解説します。

まず設定画面から、音声の設定を開きます
（※筆者はOSの設定を英語にしてしまっているため、英語になっていますが、日本語でも同じようにできます）。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/cb93e0d9-6043-bd17-91d5-c14e607b4551.png)


開いたら少し下の方にスクロールします。すると詳細設定の項目があるのでこちらをクリックします。
![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/7a3bc59b-575f-dc17-6cd9-d73249823d3f.png)

下の画像のようなウインドウがポップアップするので、録音タブを選択します。録音タブを選択したら白いところを右クリックして「無効なデバイスの表示」をクリックします。こうすると隠れていたデバイスが表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/b27602b7-01ec-49c7-e2ed-3be0dbe36bcd.png" width=50%>

ステレオミキサーの上で右クリックし、「有効」を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/2cc7d4ca-5539-4e0d-5ba0-2c5166c75854.png" width=50%>

下の画像のようにステレオミキサーに緑色のチェックマークがつけばOKです。これでループバック録音できるようになりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/685419bb-844a-7a51-9c4f-4d7ab9d6c4cc.png" width=50%>

## コード

コード全文を載せます。

``` python
from datetime import datetime
import wave
import time

import pyaudio


FORMAT        = pyaudio.paInt16
TIME          = 10           # 録音時間[s]
SAMPLE_RATE   = 44100        # サンプリングレート
FRAME_SIZE    = 1024         # フレームサイズ
CHANNELS      = 1            # モノラルかバイラルか
INPUT_DEVICE_INDEX = 0       # マイクのチャンネル
NUM_OF_LOOP   = int(SAMPLE_RATE / FRAME_SIZE * TIME)



WAV_FILE = "./output.wav"


def look_for_audio_input():
    """
    デバイス上でのオーディオ系の機器情報を表示する
    """
    pa = pyaudio.PyAudio()
    for i in range(pa.get_device_count()):
        print(pa.get_device_info_by_index(i))
        print()
    pa.terminate()


def record_and_save():
    """
    デバイスから出力される音声の録音をする
    """
    pa = pyaudio.PyAudio()

    stream = pa.open(format   = FORMAT,
                     channels = CHANNELS,
                     rate     = SAMPLE_RATE,
                     input    = True,
                     input_device_index = INPUT_DEVICE_INDEX,
                     frames_per_buffer  = FRAME_SIZE)

    print("RECORDING...")

    list_frame = []


    for i in range(NUM_OF_LOOP):
        data = stream.read(FRAME_SIZE)
        list_frame.append(data)

    print("RECORDING DONE!")

    # close and terminate stream object "stream"
    stream.stop_stream()
    stream.close()
    pa.terminate()


    wf = wave.open(WAV_FILE, 'wb')
    wf.setnchannels(CHANNELS)
    wf.setsampwidth(pa.get_sample_size(FORMAT))
    wf.setframerate(SAMPLE_RATE)
    wf.writeframes(b''.join(list_frame))
    wf.close()


def play_wav_file():
    """
    音声ファイルの再生
    """

    wf = wave.open(WAV_FILE, 'rb')
    pa = pyaudio.PyAudio()

    stream = pa.open(format   = pa.get_format_from_width(wf.getsampwidth()),
                     channels = wf.getnchannels(),
                     rate     = wf.getframerate(),
                     output   = True)

    # ファイルの読み込み
    print("Read a file")
    data = wf.readframes(FRAME_SIZE)

    # 各バッファで再生はFRAME SIZE分だけ行われる
    print("play the flie")
    is_to_go = True
    while is_to_go:
        stream.write(data)
        data = wf.readframes(FRAME_SIZE)
        is_to_go = len(data) != 0 # "data"がゼロになったらフラグをFalseにしてwhileループを抜ける

    stream.close()
    pa.terminate()


def main():
    look_for_audio_input() # デバイス探し
    record_and_save()      # デバイスから出力される音声を録音する
    time.sleep(3)
    play_wav_file()        # 録音した音声ファイルを再生する


if __name__ == '__main__':
    main()



```

### 解説

まずはpyaudioを使うにあたって設定しておくパラメータを設定しています。

``` python
FORMAT        = pyaudio.paInt16
TIME          = 10           # 録音時間[s]
SAMPLE_RATE   = 44100        # サンプリングレート
FRAME_SIZE    = 1024         # フレームサイズ
CHANNELS      = 1            # モノラルかバイラルか
INPUT_DEVICE_INDEX = 0       # マイクのチャンネル
NUM_OF_LOOP   = int(SAMPLE_RATE / FRAME_SIZE * TIME)
```

- ```FORMAT```：pyaudioで録音、再生する時のフォーマットを指定しています。ここは、そういうものだと思って使う形で良いと思います。
- ```TIME```：録音する時間です。単位は秒です。
- ```SAMPLE_RATE```：サンプリングレートです。サンプリングレートについて説明するのはここの記事の本筋から外れてしまうのですが、コンピュータがアナログデータを取り扱うにあたってどのくらいの頻度でデータを取得するのか、を設定する数値です。大きくすればするほどより高音質に録音/再生できますが、ここをいじるのはあまりお勧めしません。
- ```FRAME_SIZE```：pyaudioではFRAME_SIZE単位で録音を実行します。そのサイズを指定しています。
- ```CHANNELS```：モノラルかバイラルかを設定します。
- ```INPUT_DEVICE_INDEX```：ループバック録音のデバイスのインデックスです。これは利用している端末毎に違うため、自分で探す必要があります。詳細については後述します。
- ```NUM_OF_LOOP```：FRAESIZEを何回ループさせれば```TIME```秒の録音と一致するのかを計算しています。

その次は、```look_for_audio_input()```関数で先ほど設定したループバック録音デバイスを探します。多分インデックスがゼロかイチのものがデバイスとなっているはずです。※これは利用している端末毎に異なっているため、自分で探す必要があります。

筆者の環境では、ループバック録音のデバイスのインデックスがゼロだったので、```INPUT_DEVICE_INDEX = 0```としています。


```record_and_save()```は録音と録音した音声をwavファイルで保存する関数です。```FRAME_SIZE```の長さのチャンクを```NUM_OF_LOOP```回ループさせることで指定した時間の長さだけ録音するようにしています。

```play_wav_file()```関数では、上述の関数で録音したwavファイルを読み込んで再生する関数です。while ループのところはFRAME_SIZE単位でデータを読み込んで、```data```の長さがゼロになったらループを抜けるようにしています。


## まとめ

pyaudioというライブラリを使って簡単にパソコンからの音声を録音&再生できました。

ただ、面倒くさい点として、（仕様上どうしようもないのかと思いますが、）
- ループバック録音のデバイスの設定をしないといけないこと
- そのデバイスのインデックスが不定のため、　手動でインデックスを探す必要があること
- macOSではループバック録音のデバイスの設定ができない（アプリを入れればできるみたいです）

といったことが挙げられます。

また録音時間について、pyaudioではメモリ上にデータを保持する仕様になっているらしく長時間の録音が難しいです。これについては無限に録音できるようにコードを書いている人もいるので、回避方法はあります。ただ、わざわざ自分でそういった処理を書かないといけないのはやや面倒くさいと感じました。
（回避方法をザクっと書いてみると、30秒単位でストリームオブジェクトの生成&破棄&音声データを保存させる処理を無限ループに放り込むというやり方でした。ほかに良い方法を知っている方がいればぜひ教えていただけると嬉しいです）