当サイトにはアフィリエイト広告が含まれます。なおレビューは私の感想を書いており、内容を指示するご依頼はお断りしています

PyAudioでPythonの音声を複数のデバイスに同時出力!仮想ケーブルとモノラルtoステレオ変換で自由自在

「Pythonで再生する音声を、YouTubeとは別の出力デバイスに送りたい!」 「さらに、イヤホンを挿してもPythonの音声だけは仮想ミキサーに送りたい!」

こんな悩み、ありませんか?

Pythonのオーディオライブラリ PyAudio を使えば、これが実現できます。今回は、以下のポイントに絞って、Pythonの音声を複数のデバイスに同時出力する方法を解説します。

  1. 特定のオーディオデバイスを指定して音声を出力する方法
  2. VB-Cableなどの仮想オーディオケーブルを使って、Pythonの音声を仮想ミキサーに送る方法
  3. モノラル音源をステレオ出力デバイスで適切に再生するための変換処理

これをマスターすれば、ゲーム音とPythonの通知音を分けたり、配信ソフトウェアに特定の音声だけを送ったりと、オーディオのルーティングが格段に自由になりますよ!


ステップ1: PyAudioのインストールとデバイスの確認

まずはPyAudioをインストールしましょう。コマンドプロンプトやターミナルで、次のコマンドを実行するだけです。

pip install pyaudio numpy

もし、Windowsでエラーが出る場合は、Unofficial Windows Binaries for Python Extension Packages から対応する .whl ファイルをダウンロードしてインストールする方法もあります。また、numpy は後述の音声変換で使うので、一緒にインストールしておきましょう。

次に、利用可能なオーディオデバイスのリストを確認します。これにより、それぞれのデバイスに割り当てられた デバイスID を知ることができます。

import pyaudio

p = pyaudio.PyAudio()

print("利用可能なオーディオデバイス:")
for i in range(p.get_device_count()):
    dev_info = p.get_device_info_by_index(i)
    # 最大出力チャンネルが0より大きいデバイスは、音声を出力できる可能性のあるデバイスです
    if dev_info['maxOutputChannels'] > 0:
        print(f"  デバイスID: {dev_info['index']}, 名前: {dev_info['name']}, 最大出力チャンネル: {dev_info['maxOutputChannels']}")

p.terminate()

このリストから、PyAudioで音声を出したいデバイス(例:CABLE Input (VB-Audio Virtual Cable)スピーカー (Realtek(R) Audio) など)のIDを控えておきましょう。


ステップ2: VB-Cableで仮想ルーティングを構築

Pythonの音声をステレオミキサーなど、他のアプリケーションの入力として使いたい場合、VB-Cable のような仮想オーディオケーブルが非常に役立ちます。

VB-Cableをインストールすると、システムに「CABLE Output」(仮想スピーカー)と「CABLE Input」(仮想マイク)というデバイスが追加されます。Pythonの音声は「CABLE Output」に出力し、その音声をステレオミキサーや録音ソフトウェアが「CABLE Input」から拾う、という流れを作ることができます。


ステップ3: Pythonで音声を同時再生する関数を作成

いよいよPythonコードです。以下のコードを audio_player.py という名前で保存しましょう。

この関数は、WAVファイルのパスと、音声を出力したいデバイス名のリストを受け取ります。モノラル音源をステレオ出力デバイスに送る際には、NumPyを使って自動的にステレオ変換を行います。

import pyaudio
import wave
import numpy as np

def play_audio_to_devices(wav_file_path, target_device_names=None, chunk_size=1024):
    """
    指定されたWAVファイルを複数のオーディオデバイスへ同時に再生します。
    モノラル音声の場合、ステレオ出力デバイス向けに自動的にステレオ変換を試みます。

    Args:
        wav_file_path (str): 再生したいWAVファイルのパス。
        target_device_names (list, optional): 音声を再生したいデバイス名のリスト。
                                              Noneの場合、見つかったすべての出力デバイスに再生を試みます。
                                              例: ['CABLE Input (VB-Audio Virtual Cable)', 'スピーカー (Realtek(R) Audio)']
        chunk_size (int, optional): 一度に読み込むオーディオフレームの数。デフォルトは1024。
    """
    p = pyaudio.PyAudio()

    # --- デバイスIDの検索 ---
    found_device_ids = {}
    print("利用可能なオーディオデバイスを検索中...")
    for i in range(p.get_device_count()):
        dev_info = p.get_device_info_by_index(i)
        
        if dev_info['maxOutputChannels'] > 0:
            if target_device_names:
                for name_to_find in target_device_names:
                    if name_to_find in dev_info['name']:
                        found_device_ids[name_to_find] = dev_info['index']
                        print(f"'{name_to_find}' のID: {dev_info['index']}")
                        break
            else:
                found_device_ids[dev_info['name']] = dev_info['index']
                print(f"デバイスID: {dev_info['index']}, 名前: {dev_info['name']}")

    p.terminate()

    # --- WAVファイルの元のフォーマット情報を取得 ---
    try:
        wf_original = wave.open(wav_file_path, 'rb')
        original_audio_format = p.get_format_from_width(wf_original.getsampwidth())
        original_channels = wf_original.getnchannels()
        original_rate = wf_original.getframerate()
        original_sample_width = wf_original.getsampwidth()
        wf_original.close()
    except wave.Error as e:
        print(f"エラー: WAVファイル '{wav_file_path}' を開けませんでした。{e}")
        return

    print(f"\nWAVファイル情報:")
    print(f"  ファイル: {wav_file_path}")
    print(f"  フォーマット: {original_audio_format} (Width: {original_sample_width} bytes)")
    print(f"  チャンネル数: {original_channels}")
    print(f"  サンプリングレート: {original_rate} Hz")

    # --- 複数のストリームでの再生 ---
    p = pyaudio.PyAudio()

    stream_configs = []

    for dev_name, dev_id in found_device_ids.items():
        try:
            wf_stream = wave.open(wav_file_path, 'rb')
            
            if "CABLE Input" in dev_name:
                target_channels_for_stream = original_channels
            elif "Realtek" in dev_name or "ヘッドホン" in dev_name or "スピーカー" in dev_name or "NVIDIA High Definit" in dev_name:
                target_channels_for_stream = 2
            else:
                target_channels_for_stream = original_channels

            stream = p.open(format=original_audio_format,
                            channels=target_channels_for_stream,
                            rate=original_rate,
                            output=True,
                            output_device_index=dev_id,
                            frames_per_buffer=chunk_size)
            stream_configs.append((stream, wf_stream, target_channels_for_stream, original_sample_width))
            print(f"  '{dev_name}' (ID: {dev_id}) への再生ストリームを開きました。")
        except Exception as e:
            print(f"  エラー: '{dev_name}' (ID: {dev_id}) のストリームを開けませんでした: {e}")

    if not stream_configs:
        print("再生可能な出力ストリームが見つかりませんでした。")
        p.terminate()
        return

    max_frames = max(wf_obj.getnframes() for _, wf_obj, _, _ in stream_configs)
    
    for _, wf_obj, _, _ in stream_configs:
        wf_obj.rewind()

    current_frame = 0
    try:
        while current_frame < max_frames:
            for stream, wf_obj, target_channels_stream, sample_width_stream in stream_configs:
                
                data = wf_obj.readframes(chunk_size)
                
                if not data:
                    empty_data = b'\x00' * chunk_size * target_channels_stream * sample_width_stream
                    stream.write(empty_data)
                    continue 

                if original_channels == 1 and target_channels_stream == 2:
                    audio_array = np.frombuffer(data, dtype=np.int16)
                    stereo_array = np.repeat(audio_array.reshape(-1, 1), 2, axis=1)
                    data = stereo_array.tobytes()
                
                stream.write(data)
            
            current_frame += chunk_size
            
        print(f"\n'{wav_file_path}' の同時再生が完了しました。")

    except Exception as e:
        print(f"再生中にエラーが発生しました: {e}")

    finally:
        for stream, wf_obj, _, _ in stream_configs:
            if stream.is_active():
                stream.stop_stream()
            stream.close()
            wf_obj.close()
        p.terminate()

# スクリプトが直接実行された場合のテストコード
if __name__ == "__main__":
    TEST_WAV_FILE = "output.wav" 

    # 例: CABLE Output と Realtekスピーカーに同時に再生
    # デバイス名はあなたの環境に合わせて正確に指定してください
    devices_to_play_to = ['CABLE Input (VB-Audio Virtual Cable)', 'スピーカー (Realtek(R) Audio)']
    play_audio_to_devices(TEST_WAV_FILE, target_device_names=devices_to_play_to)

    # 他の例 (コメントを外して試してみてください)
    # play_audio_to_devices(TEST_WAV_FILE) # すべての出力デバイスに再生を試みる
    # play_audio_to_devices(TEST_WAV_FILE, target_device_names=['ヘッドホン (Oculus Virtual Audio Dev'])

ステップ4: 他のPythonファイルから呼び出す

この play_audio_to_devices 関数は、他のPythonスクリプトから簡単に呼び出すことができます。例えば、main_app.py というファイルを作成し、次のように記述します。

main_app.pyaudio_player.py、そして再生したいWAVファイル(例: your_sound_effect.wav)を同じディレクトリに置いて、python main_app.py で実行してみてください。

from audio_player import play_audio_to_devices
import time

my_audio_file = "your_sound_effect.wav" # 再生したいWAVファイル名を指定

print("PyAudio再生機能のテストを開始します。")

# Pythonの音声をCABLE Outputと、PCのスピーカーに同時に再生
# デバイス名は、あなたの環境で確認した正確なものに置き換えてください
target_devices = ['CABLE Input (VB-Audio Virtual Cable)', 'スピーカー (Realtek(R) Audio)']
play_audio_to_devices(my_audio_file, target_device_names=target_devices)

print("\n再生が完了しました。")
time.sleep(1) 
print("アプリケーションが終了します。")

ポイントまとめ

  • デバイスIDの動的な取得: ハードコードされたIDに依存せず、デバイス名で検索することで、環境が変わっても柔軟に対応できます。
  • 複数のストリームを同時管理: 各デバイスに独立したPyAudioストリームを開き、同じ音源を同時に書き込むことで、複数の出力先への同時再生を実現します。
  • モノラル音源のステレオ変換: Realtekスピーカーなど、多くの物理デバイスはステレオ再生を想定しています。モノラルのWAVファイルを再生する際に、NumPyを使って音声を左右チャンネルに複製することで、音質の劣化を防ぎ、自然な聞こえ方になります。
  • VB-Cableの活用: 仮想オーディオケーブルを使うことで、Pythonの音声を物理的なスピーカーに出すことなく、仮想的な「マイク」入力として別のアプリケーション(配信ソフト、録音ソフトなど)に送ることができます。

これで、イヤホンを挿していても、Pythonの音声は仮想ケーブル経由で目的のアプリケーションに、YouTubeなどはイヤホンから、といった高度なオーディオルーティングが実現できます。

ぜひこのカスタマイズされたオーディオ環境を構築してみてくださいね!