Closed10

Pythonでオーディオ入出力を処理する「PyAudio」を試す

kun432kun432

https://people.csail.mit.edu/hubert/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 向けのクロスプラットフォームのサウンドツールキット。
kun432kun432

インストール

今回はローカルのMacで。Macの場合はportaudioライブラリが必要で、自分はすでにインストール済みだった。インストールされていない場合は、Homebrewでインストール。

$ brew install portaudio

でpyaudioパッケージをインストールする。仮想環境作成の上で実施する。

$ pip install pyaudio
kun432kun432

サンプルコードを順に試していく。

再生

以下のwav音源を使用させていただく

https://github.com/pdx-cs-sound/wavs

wavs: sample WAV files
Bart Massey 2024

These 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

再生用のスクリプト

play.py
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

kun432kun432

録音

record.py
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
kun432kun432

オーディオのフルデュプレックス

マイクから入力された音声を読み取り、リアルタイムでその音声をスピーカーに出力する、つまり、音声の入出力を同時に行う。これを「ワイヤー」モードというらしい。

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。

kun432kun432

コールバックを使った再生

play_callback.py
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
kun432kun432

コールバックを使ったオーディオのフルデュプレックス

wire_callback.py
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。

kun432kun432

チャネルのマッピング

これはMacのみのサンプルらしい。左右のチャネルを指定しつつ再生するサンプル。

channel_map.py
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

左のスピーカーからしか音が聞こえない。

設定を変えて試してみると違いがわかる。

kun432kun432

APIドキュメントの例が処理の流れの説明としてまとまってる

https://people.csail.mit.edu/hubert/pyaudio/docs/

ブロッキングモード

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)は終了する。

このスクラップは1ヶ月前にクローズされました