10. リアルタイムサウンド処理①(サンプルプログラムの分析)

 I2Sデジタルマイクを搭載したデジタル聴力補助ツールのプロトタイプが完成しました。
 Raspberry Pi Zeroを音声信号プロセッサーとして機能させるには、マイクの音声をリアルタイムで処理再生するソフトウェアが必要になります。今回はPortAudioのサンプルプログラムを検討して、そのプログラム構造を分析し、今後の開発に必要なことなどを把握します。



1.開発方法について

 リアルタイムサウンド処理用のライブラリーとして、オープンソースの「PortAudio」を使用します。ライブラリーはC言語で記述されているので、プログラム開発言語はC言語を使用することになります。
 PortAudioのexamplesフォルダーにはたくさんのサンプルプログラムが収納されていて、すでにいくつかは「7.USBミニマイクで動作確認」などで試したとおりです。さらに開発の手がかりを得るために、他のサンプルプログラムを検討することにしました。マイク入力を扱っているものとしては、paex_record.cとpaex_read_write_wire.cの2本があります。それぞれのコードを読んで動作を把握し、適正なマイク音量が得られるように入力デバイスをソフトウェアボリュームに切り替えて実行してみました。
・paex_record.c

 マイク入力データを配列に格納してからその内容をファイルに記録し、記録された内容を再生するものです。
 プログラム構造はコールバック手法によっています。
 10秒間だけ録音して引き続き再生しますが、音声が小刻みに途切れる現象が発生しました。

・paex_read_write_wire.c

 入力を出力に直接渡すことにより、全二重ブロッキングI/Oをテストするものです。
 10秒間だけマイクの音声を即時再生します。かなり頻繁に「Underflow」が発生して処理が中断されます。


 実行形式のpaex_read_write_wireは、起動するといきなりunderflowエラーが発生してしまったり、動いても途中でunderflowにより処理が中断されるというやっかいものです。しかし論理が簡単で音質にも問題がないため、ブロッキングI/O方式を採用することにしました。
 そこで、以降ではpaex_read_write_wire.cのコードを分析して、リアルタイム処理ロジック作成のポイントを把握することにします。


2.サンプルプログラムの分析(データ構造)

 paex_read_write_wire.cで使用されているPortAudio固有のデータ型を調べます。必要に応じてヘッダーファイルportaudio.hを参照します。
①PaStreamParameters

 入力または出力ストリームのパラメータを収納する構造体です。ここでストリームとは、連続データを「流れ」としてとらえて入出力するものであり、その操作のための抽象データ型を意味します。例えば、入力ストリームでデータを読み込み、出力ストリームでデータを書き出します。
 portaudio.hで次のように定義されています。
typedef struct PaStreamParameters {  PaDeviceIndex device; // 0~Pa_GetDeviceCount()-1の有効なデバイス番号 int channelCount; // 1~maxInputChannelsまでのサウンドのチャンネル数 PaSampleFormat sampleFormat; // PortAudio内部で取り扱うサンプルフォーマット PaTime suggestedLatency; // レイテンシーの要求値 void *hostApiSpecificStreamInfo; // ホスト環境API固有のデータ構造体へのポインター } PaStreamParameters;

②PaStream

 Streamを参照できる型を示します。次のように、PsStreamの実態は void型です。
typedef void PaStream;

③PaError

 PortAudioのエラーステータスです。エラーコードまたは「paNoError」が格納されます。
 次のように、PaErrorの実態はint型と同型です。
typedef int PaError;

④PaDeviceInfo

 PortAudioデバイスの情報と機能を提供する構造体です。デバイスは、入力、出力、または入出力をサポートしています。portaudio.hで次のように定義されています。
typedef struct PaDeviceInfo { int structVersion; // this is struct version 2 const char *name; PaHostApiIndex hostApi; // note this is a host API index, not a type id int maxInputChannels; // 最大入力チャンネル数 int maxOutputChannels; // 最大出力チャンネル数 /* 双方向の性能としての既定の遅延 */ PaTime defaultLowInputLatency; PaTime defaultLowOutputLatency; /* 堅牢な非対話型アプリケーション(サウンドファイルの再生など)*/ PaTime defaultHighInputLatency; PaTime defaultHighOutputLatency; double defaultSampleRate; } PaDeviceInfo;


3.サンプルプログラムの分析(API関数)

 PortAudioが提供するアプリケーションプログラミングインターフェイス(API)は、3つの抽象化されたグループで構成されています。
○ホストAPI

プラットフォーム固有のネイティブオーディオAPIです。これによって LinuxのALSAなどのHost APIと直接対話することなく、PortAudioを簡単に使用できます。

○オーディオデバイス

 オーディオデバイスには、名前とサポートされているサンプルレート、サポートされている入力および出力チャネルの数などの固有の機能があります。PortAudioは、利用可能なデバイスを列挙し、デバイスの機能を照会する機能を提供しています。

○オーディオストリーム

 デバイスからのアクティブなオーディオ入出力を管理します。ストリームには半二重(入力または出力)と全二重(同時入力および出力)があります。ストリームは、特定のサンプル形式、バッファサイズ、および内部バッファリングレイテンシーとサンプルレートで動作します。これらのパラメーターは、ストリームを開くときに指定します。オーディオデータは、ユーザーが提供する非同期コールバック関数を介して、または同期読み取り/書き込み関数を呼び出すことにより、ストリームとアプリケーションの間で通信します。


 以下では、paex_read_write_wire.cで使用されているAPIを列記します。

①Pa_Initialize

 ライブラリー初期化関数。PortAudioを使用する前にこれを呼び出します。
PaError Pa_Initialize( void );

②Pa_GetDefaultInputDevice

 デフォールトの入力デバイスのインデックスを取得します。
PaDeviceIndex Pa_GetDefaultInputDevice( void );

③Pa_GetDefaultOutputDevice

 デフォールトの出力デバイスのインデックスを取得します。
PaDeviceIndex Pa_GetDefaultOutputDevice( void );

④Pa_GetDeviceInfo

 PaDeviceInfo構造体へのポインターを取得します。
PaDeviceInfo* Pa_GetDeviceInfo ( PaDeviceIndex device );

⑤Pa_OpenStream

 入力、出力、または入出力でストリームを開きます。
PaError Pa_OpenStream( PaStream** stream, // 開かれたストリームのポインター const PaStreamParameters *inputParameters, // 入力デバイス情報構造体へのポインター const PaStreamParameters *outputParameters, // 出力デバイス情報構造体へのポインター double sampleRate, // 目的のサンプルレート unsigned long framesPerBuffer, // コールバック関数に渡されたフレーム数 PaStreamFlags streamFlags, // ストリーミング動作を変更するフラグ PaStreamCallback *streamCallback, // 入出力コールバック関数へのポインター void *userData ); // コールバック関数に渡すポインター

⑥Pa_StartStream

 オーディオ処理を開始します。
PaError Pa_StartStream( PaStream *stream );

⑦Pa_WriteStream

 データを出力ストリームに書き込みます。バッファ全体の書き込みが終わるまで制御が戻りません。
PaError Pa_WriteStream( PaStream* stream, const void *buffer, unsigned long frames );

⑧Pa_ReadStream

 入力ストリームからデータを読み込みます。バッファが一杯になるまで制御が戻りません。
PaError Pa_ReadStream( PaStream* stream, void *buffer, unsigned long frames );

⑨Pa_StopStream

 オーディオ処理を終了します。保留中のすべてのバッファが再生されるまで待機します。
PaError Pa_StopStream( PaStream *stream );

⑩Pa_Terminate

 ライブラリーの終了宣言。これによって割り当てられた資源が解放されます。Pa_Initialise()と対になるように対応させる必要があります。
PaError Pa_Terminate( void );

⑪Pa_AbortStream

 保留中のバッファの完了を待たずに、直ちにオーディオ処理を終了します。
PaError Pa_AbortStream( PaStream *stream );

⑫Pa_CloseStream

 オーディオストリームを閉じます。アクティブな場合は、Pa_AbortStream()が呼ばれたように保留中のバッファをすべて破棄します。
PaError Pa_CloseStream( PaStream *stream );


4.サンプルプログラムの分析(処理フロー)

 PortAudioサンプルプログラムの大半は、音声入力や音声出力を独立したコールバック関数として記述し、バックグラウンドでPortAudioによって呼び出されるという少々複雑な構造をとります。これに対してブロッキングI/Oは、すべての処理手続きがmain()関数に記述され、ファイルの入出力をするようなシンプルでわかりやすい流れで処理が進みます。
 以下では、paex_read_write_wire.cがどのように構成され、どのように処理が進行するのかを細かく見ることにしましょう。

(1)マクロとデータ定義部

 基本的な定数とデータ型はdefineマクロで定義されています。また処理で使用するデータは、main関数の先頭で定義されています。それぞれの意味はコメントの通りです。
#define SAMPLE_RATE (44100) // サンプルレート(Hz) #define FRAMES_PER_BUFFER (512) // バッファのフレーム数 #define NUM_SECONDS (10) // 作動時間(秒) #define PA_SAMPLE_TYPE paFloat32 // サンプルのデータ型 #define SAMPLE_SIZE (4) // サンプルサイズ #define SAMPLE_SILENCE (0.0f) // 無音サンプルデータ
PaStreamParameters inputParameters, outputParameters; // 入出力ストリームパラメータ構造体 PaStream *stream = NULL; // Streamへのポインター PaError err; // エラーステータス const PaDeviceInfo* inputInfo; // 入力デバイス情報構造体へのポインター const PaDeviceInfo* outputInfo; // 出力デバイス情報構造体へのポインター char *sampleBlock = NULL; // バッファーへのポインター int i; int numBytes; // バッファーサイズ int numChannels; // チャンネル数


(2)主処理
 以下のコードでは、日本語コメントで処理内容を解説します。これらはもとのソースコードにはありません。
 また、単純な情報のコンソール表示は省略しています。
 ①ライブラリーの初期化と入出力デバイスのパラメータの設定

// ライブラリを初期化する err = Pa_Initialize(); if( err != paNoError ) goto error2; // 既定の入力デバイスのデバイス番号を取得し、 // その番号でデバイス情報構造体へのポインターを取得する inputParameters.device = Pa_GetDefaultInputDevice(); /* default input device */ inputInfo = Pa_GetDeviceInfo( inputParameters.device ); // 既定の出力デバイスのデバイス番号を取得し、 // その番号でデバイス情報構造体へのポインターを取得する outputParameters.device = Pa_GetDefaultOutputDevice(); /* default output device */ outputInfo = Pa_GetDeviceInfo( outputParameters.device ); // チャンネル数を取得する numChannels = inputInfo->maxInputChannels < outputInfo->maxOutputChannels ? inputInfo->maxInputChannels : outputInfo->maxOutputChannels; // 入力デバイスのチャンネル数、サンプルフォーマット、レイテンシーを設定する // ホストAPIへのポインターはNULLを設定する inputParameters.channelCount = numChannels; inputParameters.sampleFormat = PA_SAMPLE_TYPE; inputParameters.suggestedLatency = inputInfo->defaultHighInputLatency ; inputParameters.hostApiSpecificStreamInfo = NULL; // 出力デバイスのチャンネル数、サンプルフォーマット、レイテンシーを設定する // ホストAPIへのポインターはNULLを設定する outputParameters.channelCount = numChannels; outputParameters.sampleFormat = PA_SAMPLE_TYPE; outputParameters.suggestedLatency = outputInfo->defaultHighOutputLatency; outputParameters.hostApiSpecificStreamInfo = NULL;


 ②入出力モードでストリームを開く

err = Pa_OpenStream( &stream, // 開かれたストリームへのポインター &inputParameters, // 入力デバイス構造体へのポインター &outputParameters, // 出力デバイス構造体へのポインター SAMPLE_RATE, // 目的のサンプルレート FRAMES_PER_BUFFER, // バッファのフレーム数 paClipOff, // we won't output out of range samples so don't clipping them. NULL, // no callback, use blocking API NULL ); // no callback, so no callback userData if( err != paNoError ) goto error2;


 ③所要バッファサイズの確保する

 所要バッファサイズを計算してメモリー上に動的に確保します。
// メモリーサイズ = 512 * 2 * 4 = 4,096 byte numBytes = FRAMES_PER_BUFFER * numChannels * SAMPLE_SIZE ; sampleBlock = (char *) malloc( numBytes ); if( sampleBlock == NULL ) { printf("Could not allocate record array.\n"); goto error1; } // バッファにサイレンス値を充満する memset( sampleBlock, SAMPLE_SILENCE, numBytes );


 ④オーディオ処理の開始

 開かれたストリームを指定してオーディオ処理を開始します。
err = Pa_StartStream( stream ); if( err != paNoError ) goto error1; printf("Wire on. Will run %d seconds.\n", NUM_SECONDS); fflush(stdout);


 ⑤サンプル(音声データ)の入出力処理

 ほぼ設定時間だけ(ここでは10秒)、バッファから出力ストリームに書き込み、入力ストリームからバッファへ読み込みます。
for( i=0; i<(NUM_SECONDS*SAMPLE_RATE)/FRAMES_PER_BUFFER; ++i ) { // You may get underruns or overruns if the output is not primed by PortAudio. err = Pa_WriteStream( stream, sampleBlock, FRAMES_PER_BUFFER ); if( err ) goto xrun; err = Pa_ReadStream( stream, sampleBlock, FRAMES_PER_BUFFER ); if( err ) goto xrun; }


 ⑥終結処理

// オーディオ処理を終了し、保留中のバッファが再生されるまで待つ err = Pa_StopStream( stream ); if( err != paNoError ) goto error1; // 動的に確保したメモリーを解放する free( sampleBlock ); // すべての割り当て資源を解放する Pa_Terminate(); return 0;


(3)エラー処理

xrun: // ストリーム処理(Pa_WriteStream, Pa_ReadStream)でエラーが発生した場合の処理 printf("err = %d\n", err); fflush(stdout); if( stream ) { Pa_AbortStream( stream ); // バッファの状況に関わりなく即処理を終了 Pa_CloseStream( stream ); // オーディオストリームを閉じてバッファを破棄 } // 動的に確保したメモリーを解放して free( sampleBlock ); // すべての割り当て資源を解放する Pa_Terminate(); if( err & paInputOverflow ) fprintf( stderr, "Input Overflow.\n" ); if( err & paOutputUnderflow ) fprintf( stderr, "Output Underflow.\n" ); return -2; error1: // メモリー割当て、ストリームの開始・終了処理でエラーが発生した場合の処理 free( sampleBlock ); error2: // Pa_Initialize(), Pa_OpenStream()でエラーが発生した場合の処理 if( stream ) { Pa_AbortStream( stream ); Pa_CloseStream( stream ); } Pa_Terminate(); fprintf( stderr, "An error occured while using the portaudio stream\n" ); fprintf( stderr, "Error number: %d\n", err ); fprintf( stderr, "Error message: %s\n", Pa_GetErrorText( err ) ); return -1;


5.分析結果と課題

 以上の分析結果から、サンプルプログラムpaex_read_write_wire.cの細かい動作を把握することができました。このことから以下の点が明らかになりました。
①ソフトウェアボリュームの取得

 十分なマイク音量を確保するためにソフトウェアボリュームを使用する必要があります。このため、主処理①の初期化処理において、「既定の入力デバイス番号の取得」に代えて「ソフトウェアボリュームデバイスの取得」処理を新設する必要があります。

②音声入出力処理の改変

 主処理⑤の入出力処理については次の点で改変が必要です。
  ・アンダーフロー対策が必要。
  ・処理時間を拘束されない無限ループの形成が必要。
  ・これに伴う終了判定処理が必要。
  ・将来のデジタルフィルター処理を考えると、音声データの入出力間に変換処理を設置できる構造が必要。

③転用可能な部分

 主処理②~④と⑥は定型処理であり、そのままロジックの転用が可能です。


 これらの具体的な検討は次回で行いますが、サンプルで頻発しているアンダーフローをどうするかは難題になりそうです。
 paex_read_write_wire.cは、入力ストリーム(つまりマイク)の音声データを10秒間にわたって出力ストリームに書き込む(つまりイヤフォンで再生する)プログラムですが、そのままコンパイルして実行すると、「err = -9980 Output Underflow.」エラーで処理が打ち切られてほとんど実行できません。
 30回実行した場合の発生状況は次の通りです。

 ・最後まで完動した7回23.3%
 ・起動で即エラーになった19回63.3%
 ・途中でエラーになった4回13.3%

 エラー発生時に即終了するロジックを変更して、発生時刻とメモリー容量の変化を表示して続行するようにしてみました。実行結果は以下の通りです。10回の実行で完動したのはわずか3回だけです。メモリー残容量は、コンパイルした直後の実行を除き毎回同じ値であり、このことから、アンダーフロー発生時に処理を継続してもメモリーリークの心配はないことがわかりました。
*** Start ****** 19:04:01 30064/442816 KB
Output Underflow 19:04:01 30064/442816 KB 即エラー
Output Underflow 19:04:02 30064/442816 KB
Output Underflow 19:04:02 30064/442816 KB
*** End ****** 19:04:11 30056/442816 KB
 
*** Start ****** 19:04:15 30180/442816 KB
*** End ****** 19:04:25 30180/442816 KB 完動
 
*** Start ****** 19:04:28 30180/442816 KB
Output Underflow 19:04:28 30180/442816 KB 即エラー
*** End ****** 19:04:38 30180/442816 KB
 
*** Start ****** 19:04:40 30180/442816 KB
*** End ****** 19:04:50 30180/442816 KB 完動
 
*** Start ****** 19:04:53 30180/442816 KB
Output Underflow 19:04:53 30180/442816 KB 即エラー
Output Underflow 19:04:54 30180/442816 KB
*** End ****** 19:05:03 30180/442816 KB
 
*** Start ****** 19:05:08 30180/442816 KB
Output Underflow 19:05:09 30180/442816 KB 1秒遅れ
*** End ****** 19:05:18 30180/442816 KB
 
*** Start ****** 19:05:27 30180/442816 KB
Output Underflow 19:05:27 30180/442816 KB 即エラー
*** End ****** 19:05:37 30180/442816 KB
 
*** Start ****** 19:05:39 30180/442816 KB
*** End ****** 19:05:49 30180/442816 KB 完動
 
*** Start ****** 19:05:52 30180/442816 KB
Output Underflow 19:05:52 30180/442816 KB 即エラー
Output Underflow 19:05:53 30180/442816 KB
*** End ****** 19:06:02 30180/442816 KB
 
*** Start ****** 19:06:05 30180/442816 KB
Output Underflow 19:06:05 30180/442816 KB 即エラー
*** End ****** 19:06:15 30172/442816 KB



 今回は長くなってしまったのでここでひと区切りとします。次回で、このプロジェクトのためのリアルタイムオーディオのロジックを作成する予定です。お楽しみに!