PythonでPyaudioを使ってマイク音声をリアルタイムに保存する方法 – マルチスレッド活用

こんばんは。
ちょっと前の記事↓でPythonでマイク録音の入力音声をパソコンに保存する方法を記載しました。

前回のものはメモリに書き込み用のデータを積み上げていって最後に書き出すようなことをしていました。
ですが、このやり方には欠陥があります。
実は、これではアプリを終了(データを保存する直前)するまでパソコン内のメモリを一定周期で消費していき、パソコンの動作を重くし かつ いつかはパソコンがメモリ不足で落ちてしまいます。

で、これではまずいと思い、今回は一定のメモリ消費で入力音声データをリアルタイムにファイルに書き込んでいくプログラムを作成してみましたので、共有したいと思います。

サンプルコード

ではまずは、いつも通りコード全体を記載します。

import sys
import pyaudio
import wave 
import time
import threading
from logging import NullHandler

wave_f = NullHandler
streamstop = False

def readaudio(filepath):
    global wave_f
    global streamstop
    # Audio インスタンス取得
    audio = pyaudio.PyAudio() 
    stream = audio.open( format = pyaudio.paInt16,
                        rate = 44100,
                        channels = 1, 
                        input_device_index = 1,
                        input = True, 
                        frames_per_buffer = 4096,
                    )
    wave_f = wave.open(filepath, 'wb')
    wave_f.setnchannels(1)
    wave_f.setsampwidth(2)
    wave_f.setframerate(44100 )
    
    print("Start")
    while True:
        if streamstop == False:
            data = stream.read(4096)
            rec_exec(data)
        else:
            break
          
    # 音声を読み出し    
    wave_f.close()
    # Audio デバイスの解放
    stream.stop_stream()
    stream.close()
    audio.terminate()

def main_func(filepath):
    global streamstop
    thread1 = threading.Thread(target=readaudio, args=(filepath,))
    thread1.start()
    
    while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            print("stop")
            streamstop = True
            break
        
    thread1.join()
        
def rec_exec(data):
    global wave_f
    #print(data)
    wave_f.writeframes( (data) )


if __name__ == '__main__':
    args = sys.argv
    if len(args) != 1:
        filepath = args[1]
        main_func( filepath )
    else:
        print("please input rec file")

基本的な、音声の入力とWaveファイルの保存方法は、前回のものとほとんど変わらないので、割愛します。
そもそも、Audioの保存の方法がわからんないよって方は上のページから見てみてください。

今回、ファイルをリアルタイムで書き出しするために、挑戦したのではマルチスレッドです。

マルチスレッドについてと、プログラム解説は自ページに記載します。

マルチスレッドとは

マルチスレッドとは、実行したプロセス内で並行して別のプロセスのようなものを実行させて、平行して処理を行うことを言います。
上記のプログラムでは、 main_func関数とreadaudio関数が平行して動作することになります。

なぜこういったことをするのかというと、 main_func 内でオーディオバッファを常時読み込みするようなことを行うと、 メインのプロセスがそれしかできない状態となり他の処理が何もできなくなります
※例外等のエラーは拾えます。というか吹っ飛んできます。
ですので、別にスレッドというメインのプロセスとは別のプロセスのようなものを立ち上げて、Audioの処理を行うようにしています。
※今回の場合は、キー入力で落ちるようにしているので、スレッドを起こさなくても、大丈夫かと思います。

スレッド制御解説

では実際のスレッドの制御について記載します。
まず、スレッドの機能を使用するためには、threadingライブラリを読み込みます。

import threading

そしてスレッドは以下の流れで使用します。
①スレッドのインスタンスを生成
②スレッドを起動
③スレッドを終了待ち(起動したスレッドが終了するまで待ちます)
スレッドの終了は、起動フラグ等で制御してスレッドから抜けるようにしてください。
今回のコードでは、 streamstop フラグで制御しています。

以下がその処理部分です。

    thread1 = threading.Thread(target=readaudio, args=(filepath,))   ・・・ ①
    thread1.start()                                                  ・・・ ②
    
    while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            print("stop")
            streamstop = True
            break
        
    thread1.join()                                                   ・・・ ③

インスタンス生成時には、第1引数にスレッド起動時に呼び出す関数を設定し、第2引数にオプションをしています。
ここでは args=(filepath,) というものを設定していますが、これはスレッド起動と同時に渡される引数を設定しています。引数が不要であれば、これは記載不要です。

実際のスレッドのメイン関数は以下です。filepathが引数になっています。

def readaudio(filepath):

そして、起動の②と終了の③の間が、メインプロセスの処理となります。
ここで何かしらのイベントをまったり、メインの別の処理を行ったりします。
今回は、無限ループさせてtime.sleepで1秒待ちをしながら、終了イベントのキー入力を待つようになっています。
※ここをスリープなしで、無限ループさせるとCPUの処理を使いCPU使用率が100%となってしまうので、必ずsleep等の待ち処理を入れてください。

Wave保存処理

あとは、基本的に前回の記事と同じ処理をしていて、waveの部分だけ処理を分離させています。
↓の部分がファイルの生成と設定

 wave_f = wave.open(filepath, 'wb')
    wave_f.setnchannels(1)
    wave_f.setsampwidth(2)
    wave_f.setframerate(44100 )

↓が、読み出ししたデータの保存

def rec_exec(data):
    global wave_f
    #print(data)
    wave_f.writeframes( (data) )

そして↓が、最終の後始末をしています。


    # 音声を読み出し    
    wave_f.close()

あとは、処理を終了するために、streamstopというフラグをグローバル変数で持っていて、キー入力と同時にフラグを立てて、スレッドを終了するようにしています。

フラグの読み出しとスレッドの終了

↓の★部分で、フラグを立ってています。

while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            print("stop")
            streamstop = True                  ★
            break

以下の★部分で常時フラグを監視し、フラグが立っていれば、else節にいき処理を終了するようにしています。

   print("Start")
    while True:
        if streamstop == False:                ★
            data = stream.read(4096)
            rec_exec(data)
        else:
            break

最後にグローバル変数の使い方です。

グローバル変数使い方

今回、処理の都合で、2つのグローバル変数を使用しましたので、簡単にですがグローバル変数の使い方について説明しておきます。

まず、ファイルの上部に変数を記載します。この時にPythonは型を宣言しない言語になりますので、何かしらの値をいれてあげないとエラーとなりますので、注意してください。

今回で言えば、以下がグローバル変数の宣言部分となります。

wave_f = NullHandler
streamstop = False

そして、関数内で使用する場合は以下のように、global xxxxと、グローバルを使用しますよって、宣言してから使用します。
※読み出ししかしない場合は問題ない場合がほとんどですが、書き込みをする場合は必ずこの”global”記載が必要となります。
こちらもPythonの言語の特性上、型を宣言しないスクリプト言語なので、globalだと宣言してあげないと、新たなローカル変数として値をとってしまいます

    global wave_f
    global streamstop

まとめ

これで、常時ファイルへの書き込みを行うようになりますので、メモリが枯渇するようなことはなくなりました
代わりに、録音時間があまりにも長くなると、HDDの容量次第であふれることはあると思いますので、そこは注意してください。
おそらく実使用では、こういった使い方をするようになると思いますので、ぜひ今回の記事を参考にしてみてください。

Pythonについて勉強したい人は以下がおすすめです。私も持っていてたまに眺めて勉強していますものですのでぜひ購入して学習してみてください。

にいやん

出身 : 関西 居住区 : 関西 職業 : 組み込み機器エンジニア (エンジニア歴13年) 年齢 : 38歳(2022年11月現在) 最近 業務の効率化で噂もありPython言語に興味を持ち勉強しています。 そこで学んだことを記事にして皆さんとシェアさせていただければと思いブログをはじめました!! 興味ある記事があれば皆さん見ていってください!! にほんブログ村