1.スケルトンの構造
リアルタイムサウンド処理の骨格になる処理フローを下図のようにまとめてみました。準備処理の多くはサンプルプログラムから流用できますが、ブルーのボックスは変更またはコードの追加が発生します。中核になる音声入出力の部分は全面的に書き直し、今後のデジタルフィルター処理の追加に対応できる構造にする必要があります。
もっとも大きく変わるのはデータ構造です。サンプルプログラムではストリーム用のバッファ領域を次のように計算してサイズを求め、動的にメモリー割り付けをしていました。
必要サイズ = バッファのフレーム数 × チャンネル数 × 1サンプルのサイズ
sampleBlock = (char *)malloc(必要サイズ);
音声データの入出力はこのバッファ領域を使用して行っていましたが、将来フィルター処理を追加すると、入力データに変換処理をして結果を求めることになるので、出力用のバッファ(Playback buffer)を別に設定しておくほうが好都合です。これをグローバル変数として次のように定義することにします。
float buffer[バッファのフレーム数][2];
1サンプル4バイト(32ビット)の浮動小数点データを単位として、2チャンネルステレオ処理を前提にしているのでこのような定義になります。
2.新たな関数の作成と追加コード
アンダーフロー/オーバーフローエラーの監視や汎用のソフトウェアボリューム・デバイスの取得、素通しフィルターなど4つの関数を新設します。また準備処理のコード中に、チェック用の若干のコードを追加します。
①ソフトウェアボリューム・デバイスの取得関数
以前に述べたように、十分なマイク音量を確保するためにソフトウェアボリュームを使用します。ALSAの状態やインストール環境に影響されずに(つまりどのデバイス番号に割り当てられていてもかまわないように)、ソフトウェアボリュームのデバイス番号を取得する関数を新設します。
/* getSoftwareVolumeDevice : ソフトウェアボリュームのデバイス番号を取得する [返却] int : デバイス番号、見つからなければ (-1) [注意] 事前にPa_Initialize()が実行されていること。 */ int getSoftwareVolumeDevice() { int i, numDevices; const PaDeviceInfo *deviceInfo; numDevices = Pa_GetDeviceCount(); for(i=0; i<numDevices; i++) { deviceInfo = Pa_GetDeviceInfo(i); if (strcmp(deviceInfo->name, "dmic_sv") == 0) { return i; } } return -1; } |
②テスト用の関数と若干のコード
処理の開始と終了、アンダーフロー/オーバーフローエラーの発生時などに、ログ出力する関数を設置します。状態と発生時刻、メモリー残容量と総容量をログファイルに書き出します。外部変数 int logging;を参照して、値が1の場合だけ動作します。
/* writeLog : ログ情報をファイルに書き出す [引数] char : ログ種別 [内容] 種別、時刻、メモリー残容量/総容量 [注意] 外部変数を参照 (logging == 1)の場合のみログ出力する */ void writeLog(char type) { FILE *fp; char kind [20]= ""; struct sysinfo info; if (logging == 1) { sysinfo(&info); unsigned int allmem = info.totalram/1024; unsigned int freemem = info.freeram/1024; if ((fp = fopen("testR2.log", "a+")) == NULL) { printf("Can't open logging file!\n"); exit(-1); } if (type == 'o' ) strcpy(kind, "Input Overflow "); else if (type == 'u') strcpy(kind, "Output Underflow "); else if (type == 'b') strcpy(kind, "*** Begin ****** "); else if (type == 's') strcpy(kind, "*** Start ****** "); else if (type == 'e') strcpy(kind, "*** End ****** "); else if (type == 'f') strcpy(kind, "*** Final ****** "); else strcpy(kind, "???????????????? "); time_t timer = time(NULL); struct tm *date = localtime(&timer); fprintf(fp, "%s %02d:%02d:%02d %d/%d KB\n", kind, date->tm_hour, date->tm_min, date->tm_sec, freemem, allmem); if (type == 'f') fprintf(fp, "\n"); fclose(fp); } } |
この関数を、主処理main()の必要箇所で呼び出します。またこれとは別に、グローバル変数に int test_modeを設置し、この値が1ならデバイス情報やバッファサイズなどの動作基本情報を表示するコードを追加しています。
③キー入力を検知する低位関数
キー入力を解析するために、それに先行してキータッチがあったかどうかを検出したいケースがあります。キー入力がなければ、知らぬふりして処理を続行したい場合に必要になります。WindowsのC言語にはkbhit()という関数があるのですが、Linuxにはありません。
幸いにも、HOTNEWSサイトの「マニアックなプログラミング」トリッキーコードネットに収録されている『Linuxでのkbhit関数』を見つけて、そのまま利用させていただくことにしました。感謝します! コード冒頭のコメントは当方で記述したものです。
/* kbhit : キー入力を検知する [返却] 0: 入力なし、 1: 入力あり [出所] Tricky-Code.net https://hotnews8.net/programming/tricky-code/c-code03 */ int kbhit(void) { struct termios oldt, newt; int ch; int oldf; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); oldf = fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); ch = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); fcntl(STDIN_FILENO, F_SETFL, oldf); if (ch != EOF) { ungetc(ch, stdin); return 1; } return 0; } |
④フィルターなしのフィルター適用処理関数
音声入力をそのまま素通しするのも一種のフィルターと考えれば、入力バッファの内容をプレイバックバッファへ単純に転送する関数にまとめることができます。音声情報は、入力ストリームから{左・右・左・右・・・・}の順に32ビット浮動小数点型で取り出し、出力バッファへ転送しています。
次項でみるように、この関数呼び出しは音声入力再生処理ループの「フィルター適用処理」部に配置して、データ変換を実行させます。
/* applyPassThroughFilter : フィルターなし(素通し)適用処理 [引数] float* ptr : 入力バッファへのポインター float outptr[][]: 出力バッファへ配列 */ void applyPassThroughFilter(float *ptr, float outptr[][2]) { float sl, sr; for (int i=0; i<FRAMES_PER_BUFFER; i++) { sl = *ptr++; sr = *ptr++; outptr[i][0] = sl; outptr[i][1] = sr; } } |
3.音声入力再生処理の組み立て
音声入力再生処理はシンプルな無限ループ内に記述します。問題のアンダーフロー/オーバーフローですが、発生してもメモリーリークなどでリソースを食いつぶすことはないようなので、処理を続行させることにしました。アンダーフロー発生時にわずかに「シャリッ」といったノイズが出ることもありますが、しばらくはこの状態で観察することにします。
/* 音声入力再生処理 */ while (1) { // エラーが発生しても処理を続行させる err = Pa_WriteStream(stream, buffer, FRAMES_PER_BUFFER); if( err ) { printf("*"); fflush(stdout); writeLog('u'); } err = Pa_ReadStream(stream, sampleBlock, FRAMES_PER_BUFFER); if( err ) { printf("$"); fflush(stdout); writeLog('o'); } /* (フィルター適用処理) */ applyPassThroughFilter((float*)sampleBlock, buffer); /* (キータッチ判定) */ char key; if (!kbhit()) continue; /* (キー判定とフィルター準備処理) */ key = tolower(getchar()); if (key == ' ') { writeLog('e'); break; } } |
○ストリームのハンドリング
Pa_WriteStream()、Pa_ReadStream()それぞれの実行でエラーが発生すると、ログ情報を書き出して次へ進みます。なお、アンダーフローが発生すると「*」を、オーバーフローが発生すると「$」をそれぞれコンソールに表示します。
○フィルター適用処理
Pa_ReadStream()で入力バッファsampleBlockに読み込まれた音声データは、次にPa_WriteStream()でプレイバックバッファbufferから再生されますが、この間に、フィルター適用処理部を設定します。この部分に各種のデジタルフィルタリング関数の呼び出しを配置して、音声信号の変換処理が行われることになります。現時点では、先の「素通しフィルター」applyPassThroughFilterの呼び出しだけが記述されていますが、その他のフィルタリング実行関数コールもこの位置に追加する予定です。
○キータッチ処理
キータッチがなければ、continueで無限ループ先頭からの処理が続行されます。
○キー判定と準備処理
キータッチが発生すると、タッチされたキーの値を取得して小文字に変換します。ここで入力キーに応じて、指示されたフィルターの準備処理を行う予定ですが、現時点ではスペースキーなら終了ログ情報を書き出して無限ループから脱出させています。脱出すれば終了処理を実行し、終了のログ情報を書き出します。
4.スケルトンプログラムの評価
冒頭の[Download]ボタンでスケルトンのソースコードをダウンロードしてください。Pi Zeroの適当なフォルダーにコピーしてコンパイルし実行します。
$ gcc htskelton.c -lportaudio $ ./a.out : ALSAメッセージは省略 : Software volume device = 4 Buffer size: 4096 bytes Input device # 4. Name: dmic_sv LL: 0.00580499 s HL: 0.0348299 s Output device # 0. Name: bcm2835 ALSA: IEC958/HDMI (hw:0,1) LL: 0.00160998 s HL: 0.0348299 s Num channels = 2. Frames/Buffer = 512. Sample size = 4. Buffer size = 4096 Start htskelton! *** Stop htskelton |
Startメッセージに続いてアスタリスク「*」が三つ表示されています。アンダーフローが発生していることが分かります。起動するとしばしばアンダーフローが発生しますが、その後に連続して発生することは少なく、1~2時間経過しても正常に動作しているようです。スペースキーを押下すると停止メッセージを表示して終了します。この間のログはファイル「htskelton.log」に次のように記録されています。
*** Begin ****** 21:01:23 61564/442816 KB *** Start ****** 21:01:23 61440/442816 KB Output Underflow 21:01:23 61440/442816 KB Output Underflow 23:07:19 55868/442816 KB Output Underflow 23:07:19 55868/442816 KB *** End ****** 23:50:33 53744/442816 KB *** Final ****** 23:50:33 53744/442816 KB |
再生の音質はまずまずでほとんど遅延もなく、聴力補助ツールとして十分に期待がもてます。しかし、ヘッドフォンアンプのボリューム調整が微妙に難しいです。音量ゼロ位置から少し右に回すと十分な音量になるのですが、わずかでも大きくなりすぎると、新聞をめくる音が強調されたりテーブルに食器を置く音が耳に響きます。
このあたりは、デジタルフィルターによってカバーすべきテーマであり今後を待つことになります。またalsamixerのソフトウェアボリュームの設定とも関連するので、根気よくテストすることにしましょう。
引き続きデジタルフィルターに取り組みたいところですが、次回から少し回り道をしたいと思います。ここまでI2Sデジタル出力マイクを使ってきましたが、ADMP441の販売が終了になるといったハプニングに遭遇しました。そこで、アナログ出力マイクを使う場合にはどうすればよいか、といったことも考えておきたいのです。アナログ信号をデジタル変換するための仕組みについて考え、アナログ/デジタルコンバーターの製作、アナログマイクを使った聴力補助ツールの作成とテストなどをやってみる予定です。では、お楽しみに!