Pedalboard によるエレクトロボイス生成(ソースコードと実行結果)

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install numpy pyaudio pedalboard soundfile

Pedalboard によるエレクトロボイス生成プログラム

概要

本プログラムは、音声信号処理により、入力音声をエレクトロボイスとして変換する。マイクからの音声入力を連続的に取得し、複数のデジタル信号処理エフェクトを適用することで、ロボット音声のような音響効果を生成する。

主要技術

参考文献

[1] Sobot, P. (2021). Pedalboard: A Python library for adding effects to audio. Spotify Research. https://github.com/spotify/pedalboard

[2] Bencina, R., & Burk, P. (2001). PortAudio - an Open Source Cross Platform Audio API. In Proceedings of the International Computer Music Conference (pp. 263-266).

ソースコード


# プログラム名: Pedalboard によるエレクトロボイス生成プログラム
# 特徴技術名: Pedalboard(Spotify製オーディオエフェクトライブラリ)
# 出典: Sobot, P. (2021). Pedalboard: A Python library for audio effects. Spotify. https://github.com/spotify/pedalboard
# 特徴機能: エフェクトチェーンによる音声変換(リアルタイム/ファイル処理対応)
# 学習済みモデル: 使用なし(DSPベースのエフェクト処理)
# 方式設計:
#   - 関連利用技術: NumPy(配列演算)、PyAudio(音声入出力)、soundfile(ファイル読み書き)
#   - 入力と出力: 入力: マイク/音声ファイル/動画ファイル、出力: エレクトロボイスに変換された音声
#   - 処理手順: 1.入力ソース選択 2.音声取得 3.NoiseGateでノイズ除去 4.PitchShiftでピッチ変更 5.Distortionでロボット感付与 6.Chorusで金属的響き追加 7.Gainで音量調整 8.Compressorで最終調整 9.出力
#   - 前処理、後処理: 前処理: 入力正規化とノイズゲート、後処理: クリッピング防止のための振幅制限
#   - 追加処理: RMSベースの無音検出による処理バイパス(マイク入力時のみ)
#   - 調整を必要とする設定値: pitch_shift_semitones(ピッチシフト量、-12〜12半音)、distortion_drive_db(歪み量、0〜20dB)、noise_gate_threshold_db(ノイズゲート閾値、-60〜-20dB)、silence_threshold(無音判定閾値、0.0001〜0.01)
# 将来方策: pitch_shift_semitonesをMIDIコントローラーで制御可能にすることで、演奏中のリアルタイム音程変化を実現
# その他の重要事項: マイク入力時はレイテンシ実現のため、CHUNKサイズ1024サンプルで設定。ファイル処理時は全体を一括処理
# 前準備: pip install numpy pyaudio pedalboard soundfile

import numpy as np
import pyaudio
from pedalboard import Pedalboard, PitchShift, Distortion, Chorus, NoiseGate, Gain, Compressor
import threading
import soundfile as sf
import urllib.request
import tempfile
import os
import time
from datetime import datetime

# 音声パラメータ
RATE = 44100
CHUNK = 1024
CHANNELS = 1
FORMAT = pyaudio.paFloat32

# サンプル音声URL
SAMPLE_AUDIO_URL = 'https://github.com/librosa/librosa-test-data/raw/main/test1_44100.wav'

# エフェクトパラメータ(調整可能な設定値)
PITCH_SHIFT_SEMITONES = -5.0   # ピッチシフト量(-12〜12半音、負値で低音化)
DISTORTION_DRIVE_DB = 12.0     # 歪み量(0〜20dB)
CHORUS_RATE_HZ = 1.5           # コーラス速度(0.1〜10Hz)
CHORUS_DEPTH = 0.3             # コーラス深度(0.0〜1.0)
CHORUS_MIX = 0.5               # コーラスミックス量(0.0〜1.0)
NOISE_GATE_THRESHOLD_DB = -40.0 # ノイズゲート閾値(-60〜-20dB)
GAIN_DB = -6.0                 # 最終ゲイン調整(歪み後の音量調整)

# コンプレッサーパラメータ
COMPRESSOR_THRESHOLD_DB = -15.0  # コンプレッサー閾値
COMPRESSOR_RATIO = 3.0            # コンプレッサー比率
COMPRESSOR_ATTACK_MS = 5.0        # アタック時間(ミリ秒)
COMPRESSOR_RELEASE_MS = 50.0      # リリース時間(ミリ秒)

# 無音検出・処理パラメータ
SILENCE_THRESHOLD = 0.001      # 無音判定閾値
CLIPPING_LIMIT = 0.95          # クリッピング防止の最大値

# 処理停止フラグと録音バッファ
stop_processing = False
recording_buffer = []

# エフェクトチェーンの構築
effect_board = Pedalboard([
    NoiseGate(                                               # ノイズゲート
        threshold_db=NOISE_GATE_THRESHOLD_DB,
        ratio=10.0,
        attack_ms=1.0,
        release_ms=100.0
    ),
    PitchShift(semitones=PITCH_SHIFT_SEMITONES),           # ピッチシフト
    Distortion(drive_db=DISTORTION_DRIVE_DB),              # 歪み
    Chorus(                                                 # コーラス(金属的響き)
        rate_hz=CHORUS_RATE_HZ,
        depth=CHORUS_DEPTH,
        mix=CHORUS_MIX
    ),
    Gain(gain_db=GAIN_DB),                                  # ゲイン調整
    Compressor(                                             # 最終段コンプレッサー
        threshold_db=COMPRESSOR_THRESHOLD_DB,
        ratio=COMPRESSOR_RATIO,
        attack_ms=COMPRESSOR_ATTACK_MS,
        release_ms=COMPRESSOR_RELEASE_MS
    )
])

def audio_callback(in_data, frame_count, time_info, status):
    global stop_processing, recording_buffer

    if stop_processing:
        return (None, pyaudio.paComplete)

    # 入力データをnumpy配列に変換
    audio_data = np.frombuffer(in_data, dtype=np.float32)

    # 無音検出(RMS計算)
    rms = np.sqrt(np.mean(audio_data**2))

    # 無音時は適切なサイズのバッファを返す
    if rms < SILENCE_THRESHOLD:
        silence_buffer = np.zeros(frame_count, dtype=np.float32)
        # 無音も録音バッファに追加
        recording_buffer.append(silence_buffer.copy())
        return (silence_buffer.tobytes(), pyaudio.paContinue)

    try:
        # Pedalboardでエフェクト処理
        processed_audio = effect_board(audio_data, sample_rate=RATE)

        # クリッピング防止
        processed_audio = np.clip(processed_audio, -CLIPPING_LIMIT, CLIPPING_LIMIT)

        # 録音バッファに追加
        recording_buffer.append(processed_audio.copy())

        # 処理済みデータを返す
        return (processed_audio.tobytes(), pyaudio.paContinue)

    except Exception:
        # エラー時は元の入力データをそのまま返す
        recording_buffer.append(audio_data.copy())
        return (in_data, pyaudio.paContinue)

def process_file(input_path, output_path):
    """ファイル全体を処理"""
    print(f'ファイルを読み込んでいます: {input_path}')

    # 音声ファイルを読み込み
    audio_data, sample_rate = sf.read(input_path, dtype='float32')

    # ステレオの場合はモノラルに変換
    if len(audio_data.shape) > 1:
        audio_data = np.mean(audio_data, axis=1)

    # サンプルレートが異なる場合は警告
    if sample_rate != RATE:
        print(f'警告: サンプルレート{sample_rate}Hzのファイルを{RATE}Hzとして処理します')

    print('エフェクト処理を実行しています...')

    # エフェクト処理
    processed_audio = effect_board(audio_data, sample_rate=RATE)

    # クリッピング防止
    processed_audio = np.clip(processed_audio, -CLIPPING_LIMIT, CLIPPING_LIMIT)

    # ファイルに保存
    sf.write(output_path, processed_audio, RATE)
    print(f'処理済みファイルを保存しました: {output_path}')

    return processed_audio

def play_audio_file(file_path):
    """音声ファイルを再生"""
    audio_data, sample_rate = sf.read(file_path, dtype='float32')

    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT,
                    channels=1 if len(audio_data.shape) == 1 else audio_data.shape[1],
                    rate=sample_rate,
                    output=True)

    # float32のバイトデータに変換して再生
    stream.write(audio_data.astype(np.float32).tobytes())

    stream.stop_stream()
    stream.close()
    p.terminate()

def download_sample():
    """サンプル音声をダウンロード"""
    print(f'サンプル音声をダウンロードしています: {SAMPLE_AUDIO_URL}')
    with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file:
        urllib.request.urlretrieve(SAMPLE_AUDIO_URL, tmp_file.name)
        return tmp_file.name

def monitor_keyboard():
    """キーボード入力を監視"""
    global stop_processing
    input('Enterキーを押すと録音を停止します...\n')
    stop_processing = True

def realtime_microphone():
    """マイクからのリアルタイム処理"""
    global stop_processing, recording_buffer
    stop_processing = False
    recording_buffer = []

    print('マイクに向かって話すと、エレクトロボイスに変換されます')
    print('録音を停止するには Enterキーを押してください')
    print('-' * 50)

    try:
        p = pyaudio.PyAudio()

        stream = p.open(format=FORMAT,
                        channels=CHANNELS,
                        rate=RATE,
                        input=True,
                        output=True,
                        frames_per_buffer=CHUNK,
                        stream_callback=audio_callback)

        stream.start_stream()

        # キーボード監視スレッドを開始
        keyboard_thread = threading.Thread(target=monitor_keyboard)
        keyboard_thread.daemon = True
        keyboard_thread.start()

        # メインループ
        while stream.is_active() and not stop_processing:
            time.sleep(0.1)

        stream.stop_stream()
        stream.close()
        p.terminate()

        # 録音データがある場合は保存オプションを提示
        if recording_buffer:
            print('\n録音を停止しました')
            save_choice = input('録音した音声を保存しますか? (y/n): ').strip().lower()

            if save_choice == 'y':
                # タイムスタンプ付きのファイル名を生成
                timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
                output_filename = f'electro_voice_recording_{timestamp}.wav'

                # カスタムファイル名の入力オプション
                custom_name = input(f'ファイル名を入力してください(Enterでデフォルト: {output_filename}): ').strip()
                if custom_name:
                    if not custom_name.endswith('.wav'):
                        custom_name += '.wav'
                    output_filename = custom_name

                # 録音データを結合して保存
                recorded_audio = np.concatenate(recording_buffer)
                sf.write(output_filename, recorded_audio, RATE)
                print(f'録音を保存しました: {output_filename}')

                # 再生オプション
                play_choice = input('保存したファイルを再生しますか? (y/n): ').strip().lower()
                if play_choice == 'y':
                    print('再生中...')
                    play_audio_file(output_filename)
                    print('再生完了')

    except Exception as e:
        print(f'エラーが発生しました: {e}')
        return False

    return True

# メイン処理
print('エレクトロボイス生成システム')
print('=' * 50)
print('入力ソースを選択してください:')
print('0: 音声ファイル・動画ファイル')
print('1: マイク(リアルタイム処理・録音可能)')
print('2: サンプル音声ファイル')
print('=' * 50)

choice = input('選択 (0/1/2): ').strip()

print(f'\n現在のエフェクト設定:')
print(f'  ピッチシフト: {PITCH_SHIFT_SEMITONES}半音')
print(f'  歪み: {DISTORTION_DRIVE_DB}dB')
print(f'  ノイズゲート: {NOISE_GATE_THRESHOLD_DB}dB')
print('')

if choice == '1':
    # マイク入力(リアルタイム処理)
    success = realtime_microphone()
    if success:
        print('\nマイク入力の処理を終了しました')

elif choice == '0':
    # ユーザー指定のファイル
    input_path = input('入力ファイルのパスを入力してください: ').strip()
    if not os.path.exists(input_path):
        print('ファイルが見つかりません')
        exit(1)

    output_path = input('出力ファイル名を入力してください(例: output.wav): ').strip()
    if not output_path.endswith('.wav'):
        output_path += '.wav'

    try:
        process_file(input_path, output_path)

        # 再生確認
        play_choice = input('\n処理済みファイルを再生しますか? (y/n): ').strip().lower()
        if play_choice == 'y':
            print('再生中...')
            play_audio_file(output_path)
            print('再生完了')

    except Exception as e:
        print(f'エラーが発生しました: {e}')
        exit(1)

elif choice == '2':
    # サンプル音声ファイル
    try:
        # サンプルをダウンロード
        temp_input = download_sample()
        output_path = 'electro_voice_output.wav'

        # 処理実行
        process_file(temp_input, output_path)

        # 一時ファイルを削除
        os.unlink(temp_input)

        # 再生確認
        play_choice = input('\n処理済みファイルを再生しますか? (y/n): ').strip().lower()
        if play_choice == 'y':
            print('再生中...')
            play_audio_file(output_path)
            print('再生完了')

    except Exception as e:
        print(f'エラーが発生しました: {e}')
        exit(1)

else:
    print('無効な選択です')
    exit(1)

print('\nエレクトロボイス生成システムを終了しました')