pythonによるリアルタイムオーディオ出力


##概要
windows版のpythonで、実時間で作成orMIXした音声データを出力するための技術メモです。

#### 想定するアプリケーションと実行環境
想定するアプリケーション：音声合成、音声処理
実行環境：python, windows10
#### 参考情報
https://people.csail.mit.edu/hubert/pyaudio/docs/index.html

#### 実行環境の準備
pyaudioのインストールは、`pip install pyaudio`で行えますが、python 3.7以降では使用できません。Windwos環境であれば、UCIのLaboratory for Fluorescence Dynamics講座の[Christoph Gohlke](https://www.lfd.uci.edu/~gohlke/)さんが、作成された非公式パッケージがあります。こちらをインストールするには、https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio から該当するものをダウンロードし、`pip install <ダウンロードしたファイル>`とします。自己責任ですが。

## 解説
### 予備知識（処理の同期）

リアルタイムデータを扱うソフトウェアでは、「データを処理／生成する部分」と「データを入力／出力する部分」のデータ処理速度が釣り合っていないと、データの欠損や処理の異常停止などが発生し、期待される処理が行えなくなります。このため、生成部分と消費部分の処理タイミングを調整するメカニズムが必要になります。

ブロッキングモードでは、「データを処理／生成する部分」が、データの入力／出力処理を行った場合、「データを入出力する処理を完了する」まで処理を停止させることで、「データを処理／生成する部分」の処理速度を調整します。

コールバックモードでは、「データを入出力する処理が完了した」ときに、あらかじめ登録した「データを処理／生成する部分」の処理を呼び出すことで、「データを処理／生成する部分」の処理速度を調整します。呼び出されるまでは他の処理ができるのが利点です。

### 音声出力プログラムの例
####ブロッキングモード
データを作っては出力ＡＰＩ`sndStrm.write(チャンクデータ)`を呼び出すことを繰り返す。出力バッファがいっぱいになると処理が一時停止することで、処理が同期する。
####コールバックモード
playAudioオブジェクトを作成するときに、コールバックする関数をパラメータ指定して登録する`strm = pyAud.open(..., stream_callback=コールバック関数)`。準備が整ったところで、処理開始を伝える`strm.start_stream()`ことで、入出力処理がスタートし、データが完成／必要となったときに自動的にコールバック関数が呼び出させる。最後に停止処理を行う。

### バッファへの書き込み
format=pyaudio.paInt16の場合は、音声データを符号付16ビット整数に変換してバッファに書き込みます。注意点は、サンプルの抜けや重複があると雑音の原因となるため、ここはしっかり確認すること。

###サンプルソース

```python:サンプル
import pyaudio
import numpy as np
import time

SND_FS = 44100 # 48000
SND_CHUNK = 1024
SND_CH_OUT = 1
# CHUNK time = CNUNK/FS   23.22 ms (43.07Hz)

#=========================================================
# テスト用サウンドデータを作る
# チャンクあたり10個のSIN波データを作る(232Hz @ 44100sps)
#=========================================================
def procSound(dtype=np.int16):
    x = np.linspace(0, 20*np.pi, SND_CHUNK+1)
    y=np.sin(x[0:SND_CHUNK])*256*10
    #----参考　ざっくりかくならこれ（歪みあり） ----
    #x = np.linspace(0, 20*np.pi, SND_CHUNK)
    #y=np.sin(x)*256*10

    # array to buffer
    #  float -> 16bit signed intに変換する
    sndBuf = y.astype(dtype).tobytes()
    return sndBuf

# test
#buf = procSound()
#print(buf)

#=========================================================
# ブロックモードの利用
#=========================================================
def demoBlockingMode():
    pyAud = pyaudio.PyAudio()

    sndStrm = pyAud.open(rate=SND_FS,channels=SND_CH_OUT,format=pyaudio.paInt16,input=False,output=True)

    for cnt in range(100):
        obuf = procSound()
        if sndStrm.is_active :
            sndStrm.write(obuf) # blocking mode. blocks until all the give frames have been played
    sndStrm.stop_stream()
    sndStrm.close()
    pyAud.terminate()

#=========================================================
# callbackモード
#=========================================================

def sndCallback(in_data, frame_count, time_info, status):
    #data = wf.readframes(frame_count)
    data = procSound()
    return (data, pyaudio.paContinue)

def demoCallbackMode():
    pyAud = pyaudio.PyAudio()
    sndStrm = pyAud.open(rate=SND_FS,channels=SND_CH_OUT,format=pyaudio.paInt16,input=False,output=True,
                        stream_callback=sndCallback)
    sndStrm.start_stream()
    time.sleep(3)
    sndStrm.stop_stream()
    sndStrm.close()
    pyAud.terminate()
#=========================================================
# 実行
#=========================================================
#demoBlockingMode()
demoCallbackMode()
```
### 結言
まぁ、コールバックとかみなさまご存じでしょうが念のため。