Pythonでオーディオ入出力を処理する「PyAudio」を試す
PyAudio
PyAudioは、クロスプラットフォームのオーディオ入出力ライブラリPortAudio v19用のPythonバインディングを提供する。PyAudioを使用すると、GNU/Linux、Microsoft Windows、Apple macOSなど、さまざまなプラットフォーム上でPythonを使用してオーディオの再生や録音を簡単に実行できる。
PyAudio は MIT ライセンスで配布されている。
このライブラリは、もともと次のものに触発されて開発された。
- pyPortAudio/fastaudio: PortAudio v18 API の Python バインディング。
- tkSnack: Tcl/Tk および Python 向けのクロスプラットフォームのサウンドツールキット。
インストール
今回はローカルのMacで。Macの場合はportaudioライブラリが必要で、自分はすでにインストール済みだった。インストールされていない場合は、Homebrewでインストール。
$ brew install portaudio
でpyaudioパッケージをインストールする。仮想環境作成の上で実施する。
$ pip install pyaudio
サンプルコードを順に試していく。
再生
以下のwav音源を使用させていただく
https://github.com/pdx-cs-sound/wavs
wavs: sample WAV files
Bart Massey 2024These are sample WAV audio files used in Portland State
University's Computers, Sound and Music course. Licensing
is Creative Commons CC-0 unless otherwise indicated.
collectathon.wav
: Collectathon — Opening Theme by
Tovatronica. License is
CC-BY. https://steviasphere.bandcamp.com/album/collectathon
echomorph.wav
:voice-note.wav
as processed with an
"echomorph" effect.
fifth.wav
: Synthesizer playing a fifth, by Bart Massey.
gc.wav
: Short acoustic guitar sample, by Bart Massey.
noise.wav
: White noise.
overdrive.wav
: 1KHz sine wave with overdrive effect applied.
sine.wav
: 1KHz sine wave.
synth.wav
: Short synthesizer sample, by Bart Massey.
voice-note.wav
: Single sung note, by Bart Massey.
voice.wav
: Short voice sample, by Bart Massey.
レポジトリをクローンしておく
$ git clone https://github.com/pdx-cs-sound/wavs
再生用のスクリプト
import wave
import sys
import pyaudio
CHUNK = 1024
if len(sys.argv) < 2:
print(f'Plays a wave file. Usage: {sys.argv[0]} filename.wav')
sys.exit(-1)
with wave.open(sys.argv[1], 'rb') as wf:
# PyAudioをインスタンス化し、PortAudioシステムリソースを初期化する (1)
p = pyaudio.PyAudio()
# ストリームを開く (2)
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
# WAVEファイルのサンプルを再生する (3)
while len(data := wf.readframes(CHUNK)): # `:=`はPython 3.8 以降が必要
stream.write(data)
# ストリームを閉じる (4)
stream.close()
# PortAudioシステムリソースを解放する (5)
p.terminate()
サンプルの音源の一つをチョイスして再生する
$ python play.py wavs/collectathon.wav
オーディオが再生されればOK
録音
import wave
import sys
import pyaudio
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1 if sys.platform == 'darwin' else 2
RATE = 44100
RECORD_SECONDS = 5
with wave.open('output.wav', 'wb') as wf:
p = pyaudio.PyAudio()
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True)
print('Recording...')
for _ in range(0, RATE // CHUNK * RECORD_SECONDS):
wf.writeframes(stream.read(CHUNK))
print('Done')
stream.close()
p.terminate()
録音開始、5秒間録音されるので適当に喋る
$ python record.py
Recording...
Done
終わったら最初の再生スクリプトで確認
$ python play.py output.wav
オーディオのフルデュプレックス
マイクから入力された音声を読み取り、リアルタイムでその音声をスピーカーに出力する、つまり、音声の入出力を同時に行う。これを「ワイヤー」モードというらしい。
import sys
import pyaudio
RECORD_SECONDS = 5
CHUNK = 1024
RATE = 44100
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(2),
channels=1 if sys.platform == 'darwin' else 2,
rate=RATE,
input=True,
output=True,
frames_per_buffer=CHUNK)
print('* recording')
for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
stream.write(stream.read(CHUNK))
print('* done')
stream.close()
実行
$ python wire.py
自分の声がほんの少しだけ遅れてスピーカーから聞こえてくればOK。
コールバックを使った再生
import wave
import time
import sys
import pyaudio
if len(sys.argv) < 2:
print(f'Plays a wave file. Usage: {sys.argv[0]} filename.wav')
sys.exit(-1)
with wave.open(sys.argv[1], 'rb') as wf:
# 再生用コールバックを定義 (1)
def callback(in_data, frame_count, time_info, status):
data = wf.readframes(frame_count)
# len(data) が要求された frame_count より小さい場合、
# PyAudio は自動的にストリームが終了したと見なし、ストリームが停止する。
return (data, pyaudio.paContinue)
# PyAudioをインスタンス化し、PortAudioシステムリソースを初期化(2)
p = pyaudio.PyAudio()
# コールバックを使用してストリームを開く (3)
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True,
stream_callback=callback)
# ストリームが終了するまで待つ (4)
while stream.is_active():
time.sleep(0.1)
# ストリームを閉じる (5)
stream.close()
# PortAudioシステムリソースを解放 (6)
p.terminate()
再生して確認
$ python play_callback.py wavs/collectathon.wav
コールバックを使ったオーディオのフルデュプレックス
import time
import sys
import pyaudio
DURATION = 5 # seconds
def callback(in_data, frame_count, time_info, status):
return (in_data, pyaudio.paContinue)
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(2),
channels=1 if sys.platform == 'darwin' else 2,
rate=44100,
input=True,
output=True,
stream_callback=callback)
start = time.time()
while stream.is_active() and (time.time() - start) < DURATION:
time.sleep(0.1)
stream.close()
p.terminate()
実行
$ python wire_callback.py
自分の声がほんの少しだけ遅れてスピーカーから聞こえてくればOK。
チャネルのマッピング
これはMacのみのサンプルらしい。左右のチャネルを指定しつつ再生するサンプル。
import wave
import sys
import pyaudio
CHUNK = 1024
if len(sys.argv) < 2:
print(f'Plays a wave file. Usage: {sys.argv[0]} filename.wav')
sys.exit(-1)
# 標準の左・右のステレオ
# channel_map = (0, 1)
# チャネルを入れ替えた右・左のステレオ
# channel_map = (1, 0)
# オーディオなし
# channel_map = (-1, -1)
# 左チャネルのオーディオ --> 左のスピーカー; 右チャネルはオーディオなし
# channel_map = (0, -1)
# 右チャネルのオーディオ --> 右のスピーカー; 左チャネルはオーディオなし
# channel_map = (-1, 1)
# 左チャネルのオーディオ --> 右のスピーカー
# channel_map = (-1, 0)
# 右チャネルのオーディオ --> 左のスピーカー
channel_map = (1, -1)
# などなど
try:
stream_info = pyaudio.PaMacCoreStreamInfo(
flags=pyaudio.PaMacCoreStreamInfo.paMacCorePlayNice,
channel_map=channel_map)
except AttributeError:
print(
'Could not find PaMacCoreStreamInfo. Ensure you are running on macOS.')
sys.exit(-1)
print('Stream Info Flags:', stream_info.flags)
print('Stream Info Channel Map:', stream_info.channel_map)
with wave.open(sys.argv[1], 'rb') as wf:
p = pyaudio.PyAudio()
stream = p.open(
format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True,
output_host_api_specific_stream_info=stream_info)
# ストリームを再生
while len(data := wf.readframes(CHUNK)): # `:=`はPython 3.8 以降が必要
stream.write(data)
stream.close()
p.terminate()
再生
$ python channel_map.py wavs/collectathon.wav
左のスピーカーからしか音が聞こえない。
設定を変えて試してみると違いがわかる。
APIドキュメントの例が処理の流れの説明としてまとまってる
ブロッキングモード
PyAudio を使用するには、まず pyaudio.PyAudio() (1) を使用して PyAudio をインスタンス化し、PortAudio のシステムリソースを取得する。
オーディオの録音または再生を行うには、pyaudio.PyAudio.open() (2) を使用して、希望するオーディオパラメータを持つ希望するデバイス上のストリームを開く。これにより、オーディオの再生または録音を行うための pyaudio.PyAudio.Stream が設定される。
オーディオの再生は、pyaudio.PyAudio.Stream.write() を使用してストリームにオーディオデータを書き込むか、pyaudio.PyAudio.Stream.read() を使用してストリームからオーディオデータを読み込むことで行う。 (3)
「ブロッキングモード」では、pyaudio.PyAudio.Stream.write() または pyaudio.PyAudio.Stream.read() の各関数は、すべてのフレームが再生/録音されるまでブロックする。代替のアプローチとして、以下で説明する「コールバックモード」がある。このモードでは、PyAudio がユーザー定義関数を呼び出して、録音したオーディオを処理したり、出力オーディオを生成したりする。
ストリームを閉じるには、pyaudio.PyAudio.Stream.close() を使用する。 (4)
最後に、pyaudio.PyAudio.terminate() を使用してPortAudioセッションを終了し、システムリソースを解放する。 (5)
コールバックモード
コールバックモードでは、PyAudioは、再生に必要な新しいオーディオデータが必要になった場合、および/または、新しい録音済みオーディオデータが利用可能になった場合に、ユーザー定義のコールバック関数を呼び出す(1)。PyAudioはコールバック関数を別のスレッドで呼び出す。コールバック関数は以下のシグネチャを持つ必要がある。callback(<input_data>, <frame_count>, <time_info>, <status_flag>)。出力ストリームの場合は、出力するオーディオデータのフレーム数frame_countを含むタプルを返し、再生または録音する予定のフレームがまだあるかどうかを示すフラグを返す必要がある。(入力専用ストリームの場合は、戻り値のオーディオデータ部分は無視される。)
ストリームが開かれると(3)、オーディオストリームの処理が開始される。これにより、コールバック関数が pyaudio.paComplete または pyaudio.paAbort を返すか、pyaudio.PyAudio.Stream.stop または pyaudio.PyAudio.Stream.close が呼び出されるまで、コールバック関数が繰り返し呼び出される。コールバック関数が frame_count 引数(2)よりも少ないフレーム数を返した場合、それらのフレームが再生された後にストリームが自動的に閉じられることに注意。
ストリームをアクティブに保つには、メインスレッドが生きている状態を維持する必要がある。例えば、スリープ状態にする(4)などである。上記の例では、一旦ウェーブファイル全体が読み込まれると、wf.readframes(frame_count)は最終的に要求されたフレーム数よりも少ない値を返す。ストリームは停止し、whileループ(4)は終了する。
mp3等を使う場合にはpydubを使えば良い