21. デジタルフィルターの実装

 「フィルターなし」を含む4種類のデジタルフィルターを使う準備が整いました。今回は、デジタルフィルターの実装と合わせて、フィルターの選択やカットオフ周波数の調整などの機能を組み込みます。リアルタイムサウンド処理は今までの方式と変わりませんが、アプリケーションの構造はかなり複雑で大きくなります。
 そこで、まずアプリケーションに必要な機能を取りまとめて全体の処理フローとの関連を整理し、それぞれの機能単位で開発を進めることにしましょう。

 ※ソースコードと関連ファイルは右側の[Download]ボタンでダウンロードしてください。   



1.所要機能と処理フロー

 まずアプリケーションに必要な機能を取りまとめます。
(1)キーボードによるフィルター種別の選択

 キーボードからフィルター種別を選択できるようにします。当面は次の4種類に限定しますが、プログラム上では必要に応じて追加できる構造にしておきます。なお、[]の入力は不要です(以下同様)。
   [l] : ローパスフィルター
   [h] : ハイパスフィルター
   [b] : バンドパスフィルター
   [x] : フィルター無し
 初回の起動時は「フィルター無し」で始まるようにします。2回目以降の起動では、前回終了時に使用していたフィルターを引き継ぎます。


(2)フィルターの適用条件の設定

 それぞれのフィルターについて、カットオフ基準周波数と可変周波数幅を設定し、キーボード操作でカットオフ周波数を増減できるようにします。
   [+] : カットオフ周波数を可変周波数幅だけ増加する
   [-] : カットオフ周波数を可変周波数幅だけ減少する
 バンドパスフィルターでは、上記のキーは高域側カットオフ周波数に対して作用させます。低域側については次のキーを割り当てます。
   [>] : 低域側カットオフ周波数を可変周波数幅だけ増加する
   [<] : 低域側カットオフ周波数を可変周波数幅だけ減少する
 選択中のフィルター種別とカットオフ周波数の状態はメモリー上に保持させます。また可変範囲(上下限値)を設定して、その範囲内で増減できるようにします。


(3)処理終了時のフィルター状態のファイル出力

 終了指示はスペースキーを押下することで行います。
 終了指示による正常終了、またはエラーによる異常終了のいずれの場合も、処理中のフィルター種別やカットオフ周波数などのカレント情報をファイルに出力します。
 出力情報は、次の起動時に読み込んで以前の状態を引き継いで動作できるようにします。


(4)ログ情報の出力

 処理の開始終了、エラーの発生、フィルターの選択指示などの詳細な動作をログファイルに記録します。
 追加書き出しによるファイル領域の不足を回避するために、これらのログ情報は古いものから順次上書きする「循環ファイル」に出力します。最大レコード数を超えたログは先頭位置から順次上書きされる仕組みです。このために、現在どの位置まで書き込んでいるかを示すレコードポインター(出力位置)を保持しなければなりません。これをファイルで保持すると処理時間が増加してタイミングエラー発生の原因になるため、メモリー上に保持します。この情報は終了時にファイルに記録し、起動時に読み込ませることにします。


(5)その他の機能

・処理中のカットオフ周波数を随時初期値で再設定する機能。
・処理中のフィルター種別とカットオフ周波数を、一時ファイルに出力する機能。
・一時ファイルに出力した内容から、フィルター種別とカットオフ周波数を再現する機能。


 次に全体の大まかなフローを把握しておきましょう。処理の中核は、PortAudioのブロッキングI/O関数であるPa_ReadStream()とPa_WriteStream()で構成する無限ループの間に実装します。通常はキー入力が無いので、Pa_ReadStream()で入力した音声データに対して、その時点で指定されているフィルターの適用処理を行って出力します。つまり、音声信号処理はここに設定すればよいことになります。
 キー入力を検知すると、終了指示のスペースキーであれば終了処理を行います。そうでなければ、キー判定により新たなフィルターが選択されていれば、そのフィルターのフィルター係数取得関数を実行します。これに該当しなければ、カットオフ周波数の増減指示なら周波数増減関数を実行、あるいはその他のオプション機能の指示なら該当する関数を実行します。
 このように基本的な処理フローはきわめてシンプルですが、キー判定と該当処理をすべてそのまま記述すると主処理(main関数)部が肥大化してわかりにくくなるので、適宜、まとまった機能を実行する関数としてまとめることになります。またログファイルへの記録は、処理の各所で行うことから専用の関数を作成して、必要に応じて呼び出します。


2.デジタルフィルターの実装

(1)前提事項

 4種類のフィルター種別と、対応する係数取得関数および信号処理関数はオブジェクトファイル filter.oとして提供されます。これらを使用するには、ヘッダーファイル filter.hをインクルードし、コンパイル時にfilter.oをリンクする必要があります。filter.hには次のような内容が定義されています。
◎フィルター処理種別 #define MODE_LOWPASS 'l' // ローパス #define MODE_HIGHPASS 'h' // ハイパス #define MODE_BANDPASS 'b' // バンドパス #define MODE_THROUGH 'x' // フィルターなし ◎フィルター係数取得関数 void getCoefficientLPF(double fe, double fs, int N, double* h); void getCoefficientHPF(double fe, double fs, int N, double* h); void getCoefficientBPF(double fe1, double fe2, double fs, int N, double* h); ◎信号処理(フィルター適用処理)関数 void applyPassFilter(int order, int frames, float* inptr, double* h, float* outptr); void applyThroughFilter(int frames, float* inptr, float* outptr);


(2)音声信号処理の実装

 処理中のフィルター種別を保持するために、グローバル変数 process_modeを定義します。
char process_mode; // 処理モード(処理中のフィルタリング種別)

 process_modeには、filter.hで定義している MODE_LOWPASS/ MODE_HIGHPASS/ MODE_BANDPASS/ MODE_THROUGHのいずれかが設定されます。
 フィルター適用処理の実行では、このprocess_modeの内容を調べてローパス/ハイパス/バンドパスであれば、すべて関数applyPassFilter()を実行します。フィルター無しの場合はapplyThroughFilter()関数を実行させます。このために、以下のコードを音声入力再生処理の無限ループ内に配置します。
/* (フィルター信号処理) */ switch (process_mode) { case MODE_THROUGH: applyThroughFilter(FRAMES_PER_BUFFER, (float*)sampleBlock, (float*)buffer); break; case MODE_LOWPASS: case MODE_HIGHPASS: case MODE_BANDPASS: applyPassFilter(FILTER_ORDER, FRAMES_PER_BUFFER, (float*)sampleBlock, h, (float*)buffer); break; }


(3)フィルター係数取得関数の実装

 適用するフィルターの種別はキー入力で指示しますが、押下されたキーが有効かどうか(処理中のフィルター種別を重複指示された場合は無効とする)を判定して、有効であれば指定された種別でprocess_modeを更新します。指示された種別に切り替えたことをコンソール表示で通知し、必要ならログに記録します。
 続いて対応するフィルター係数取得関数を、カットオフ周波数やサンプルレート、フィルター次数などの条件を引数として呼び出します。これによって係数配列h[]にフィルター係数が設定されるので、次のフィルター適用処理に反映されることになります。音声入力再生処理の無限ループ内に次のコードを配置します。
/* (キー判定とフィルター係数取得処理等) */ switch (key) { case MODE_LOWPASS: if (process_mode != MODE_LOWPASS) { process_mode = MODE_LOWPASS; printf("\nApplied : [LPF]"); if (logging == 1) writeLog(&logctrl, LOGID_MODELPF); getCoefficientLPF(fe_low_pass, SAMPLE_RATE, FILTER_ORDER, h); } break; case MODE_HIGHPASS: if (process_mode != MODE_HIGHPASS) { process_mode = MODE_HIGHPASS; printf("\nApplied : [HPF]"); if (logging == 1) writeLog(&logctrl, LOGID_MODEHPF); getCoefficientHPF(fe_high_pass, SAMPLE_RATE, FILTER_ORDER, h); } break; case MODE_BANDPASS: if (process_mode != MODE_BANDPASS) { process_mode = MODE_BANDPASS; printf("\nApplied : [BPF]"); if (logging == 1) writeLog(&logctrl, LOGID_MODEBPF); getCoefficientBPF(fe_lo_cutoff, fe_hi_cutoff, SAMPLE_RATE, FILTER_ORDER, h); } break; case MODE_THROUGH: if (process_mode != MODE_THROUGH) { process_mode = MODE_THROUGH; printf("\nApplied : [NON]"); if (logging == 1) writeLog(&logctrl, LOGID_MODENON); } break; }


3.カットオフ周波数の制御

(1)基準周波数と制御範囲の設定

 カットオフ周波数の初期値を下記のコード「// Basic cutoff frequency」のように設定しています。実際のカットオフ周波数は「// Current cutoff frequency」のフィールドを使用するので、初回の起動時のみ「// Basic cutoff frequency」から値をコピーします。
 fe_differは一度に増減させる周波数で、キーからの指示によって処理中のカットオフ周波数をその値だけ増減します。「// Range of frequency」でカットオフ周波数の最大値と最小値を決めていて、この範囲内で変化させることができます。
// Basic cutoff frequency const double cst_hi_cutoff = 8000.0; // バンドパス:高域側遮断周波数 [Hz] const double cst_lo_cutoff = 400.0; // バンドパス:低域側遮断周波数 [Hz] const double cst_low_pass = 8000.0; // ローパス:カットオフ周波数 [Hz] const double cst_high_pass = 800.0; // ハイパス:カットオフ周波数 [Hz] const double cst_step_diff = 100.0; // ステップ毎の調整値[Hz] // Current cutoff frequency double fe_hi_cutoff; // バンドパス:高域側遮断周波数 double fe_lo_cutoff; // バンドパス:低域側遮断周波数 double fe_low_pass; // ローパス:カットオフ周波数 double fe_high_pass; // ハイパス:カットオフ周波数 double fe_differ; // ステップ毎の調整値 // Range of frequency const double cst_fe_upper = 15000.0; // 上限周波数 const double cst_fe_lower = 100.0; // 下限周波数


(2)周波数の増減制御

 前述のように[+][-]でローカットとハイカットおよびバンドパスの高域側カットオフ周波数を増減します。また[>][<]で、バンドパスの低域側カットオフ周波数の増減を行います。先の「フィルター係数取得関数の実装」のコードに続けて、以下の記述を配置します。
/* (キー判定とフィルター係数取得処理等) */ switch (key) { : (省 略) : case '+': // カットオフ周波数を上げる raiseFrequency(); break; case '>': raiseBandFrequency(); break; case '-': // カットオフ周波数を下げる reduceFrequency(); break; case '<': reduceBandFrequency(); break; : (省 略) : }


(3)周波数増減関数

 実際にカットオフ周波数を増減する4つの関数は次の通りです。それぞれのcase文では、許容範囲内で周波数の増減を行い、該当するフィルター係数取得関数を呼び出しています。同時に、コンソールに対して上下の矢印により増減を示し、増減後の周波数を表示しています。
/* カットオフ周波数を上げる */ void raiseFrequency() { switch (process_mode) { case MODE_LOWPASS: if (fe_low_pass < cst_fe_upper) { fe_low_pass += fe_differ; getCoefficientLPF(fe_low_pass, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↑ : %f, ", fe_low_pass); } break; case MODE_HIGHPASS: if (fe_high_pass < cst_fe_upper) { fe_high_pass += fe_differ; getCoefficientHPF(fe_high_pass, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↑ : %f, ", fe_high_pass); } break; case MODE_BANDPASS: if (fe_hi_cutoff < cst_fe_upper) { fe_hi_cutoff += fe_differ; getCoefficientBPF(fe_lo_cutoff, fe_hi_cutoff, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↑ : %f, ", fe_hi_cutoff); } break; } } /* カットオフ周波数を下げる */ void reduceFrequency() { switch (process_mode) { case MODE_LOWPASS: if (fe_low_pass > cst_fe_lower) { fe_low_pass -= fe_differ; if (fe_low_pass < cst_fe_lower) fe_low_pass = cst_fe_lower; getCoefficientLPF(fe_low_pass, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↓ : %f, ", fe_low_pass); } break; case MODE_HIGHPASS: if (fe_high_pass > cst_fe_lower) { fe_high_pass -= fe_differ; if (fe_high_pass < cst_fe_lower) fe_high_pass = cst_fe_lower; getCoefficientHPF(fe_high_pass, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↓ : %f, ", fe_high_pass); } break; case MODE_BANDPASS: if (fe_hi_cutoff > (fe_lo_cutoff + cst_fe_lower)) { fe_hi_cutoff -= fe_differ; if (fe_hi_cutoff < (fe_lo_cutoff + cst_fe_lower)) fe_hi_cutoff = (fe_lo_cutoff + cst_fe_lower); getCoefficientBPF(fe_lo_cutoff, fe_hi_cutoff, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↓ : %f, ", fe_hi_cutoff); } break; } } /* バンドフィルターの下限カットオフ周波数を上げる */ void raiseBandFrequency() { if (process_mode == MODE_BANDPASS) { if (fe_lo_cutoff < (fe_hi_cutoff - fe_differ)) { fe_lo_cutoff += fe_differ; getCoefficientBPF(fe_lo_cutoff, fe_hi_cutoff, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↑ : %f, ", fe_lo_cutoff); } } } /* バンドフィルターの下限カットオフ周波数を下げる */ void reduceBandFrequency() { if (process_mode == MODE_BANDPASS) { if (fe_lo_cutoff > cst_fe_lower) { fe_lo_cutoff -= fe_differ; if (fe_lo_cutoff < cst_fe_lower) fe_lo_cutoff = cst_fe_lower; getCoefficientBPF(fe_lo_cutoff, fe_hi_cutoff, SAMPLE_RATE, FILTER_ORDER, h); printf(" ↓ : %f, ", fe_lo_cutoff); } } }


4.ログ情報の記録

(1)ログ情報の種別と構造体

 ヘッダーファイルhat.hにログ情報の種別と構造体を定義しています。構造体LOGCTRLは、循環記録ファイルを制御するためのものです。最大レコード数max_recを設定しておくと、ログ出力のたびにレコード数とファイルポインターが更新されます。
 構造体LOGRECは、ログ情報を構成するフィールドです。daytimeとmessageには、記録日時と指定されたログ情報種別に対応したメッセージが格納され、noteにはメモリーの状態が設定されます。
/* ログ情報種別 */ #define LOGID_BEGIN 'b' // 準備処理 #define LOGID_STRAT 's' // 処理開始 #define LOGID_END 'e' // 処理終了 #define LOGID_FINAL 'f' // 終結処理 #define LOGID_ERROVF 'o' // オーバーフローエラー #define LOGID_ERRUDF 'u' // アンダーフローエラー #define LOGID_ABEND 'x' // 異常終了 #define LOGID_CLEAR 'c' // 遮断周波数を初期値に戻す #define LOGID_READ 'r' // 遮断周波数の一時ファイル入力 #define LOGID_WRITE 'w' // 遮断周波数の一時ファイル出力 #define LOGID_MODENON '0' // フィルター無し指示 #define LOGID_MODELPF '1' // ローパスフィルター指示 #define LOGID_MODEHPF '2' // ハイパスフィルター指示 #define LOGID_MODEBPF '3' // バンドパスフィルター指示 typedef unsigned long ulong; /* ログ出力制御情報 */ typedef struct { char daytime[20]; // 日時 char message[20]; // ログメッセージ char note[40]; // ノート } LOGREC; typedef struct { ulong rec_cnt; // レコード数 ulong max_rec; // 最大レコード数 FILE* log_ptr; // ログファイルのポインター } LOGCTRL;


(2)ログファイルのオープン/クローズ

 ログファイルのオープン/クローズの関数は以下の通りです。引数LOGCTRLの最大レコード数を設定してオープンすれば、初回のログ領域確保からファイルポインターの設定まですべて行います。もし運用中に最大レコード数を変更する必要があれば、アプリケーションを停止してログ制御ファイルlog.ctrlを削除し、再起動すれば今までのログ領域を破棄して再確保します。オープン時に、カレントのレコード件数を最大レコード数で除算して除余を求め、これにレコード長を乗算することで出力位置を決定して循環ファイルを実現しています。
 ログ制御情報は、運用中はメモリー上で保持されていて、クローズ処理によって制御ファイルに書き込まれます。不測の事態で処理が中断された場合はこれが行われないため、処理中のレコード出力位置が失われるので注意が必要です。ログ情報自体は最新のものまで正しく書き込まれているので、先にバックアップした上で処理を再開します。
/* ログ制御ファイルがあれば参照、なければ制御情報を初期化してログファイル領域を確保する。 ログファイルをバイナリー入出力モードで開き、制御情報に基づいて出力位置を設定する。 [引数] ctrl : ログ制御情報構造体 LOGCTRLへのポインター [返却] 0 : OK、 0以外 : NG [注意] 初回の関数呼び出しでは、引数の最大レコード数を設定すること。 実行後には、引数のファイルポインターに適切な出力位置を示すポインターが設定される。 */ int openLog(LOGCTRL* ctrl) { // 制御ファイルを開いて FILE *fp = fopen("log.ctrl", "rb"); if (fp == NULL) { printf("Create log file.\n"); // 存在しない場合はログファイルを作成して FILE *fplog = fopen("hat.log", "wb"); memset(&logrec, 0x00, sizeof(logrec)); for (int i=0; imax_rec; i++) fwrite(&logrec, sizeof(logrec), 1, fplog); fclose(fplog); // レコード数を初期化する ctrl->rec_cnt = 0; } else { // 存在すれば制御情報を読み込む if (fread(ctrl, sizeof(LOGCTRL), 1, fp) < 1) return -1; fclose(fp); } // ログファイルを開いて出力位置を設定する ctrl->log_ptr = fopen("hat.log", "rb+"); ulong recpos= (ctrl->rec_cnt % ctrl->max_rec) * sizeof(LOGREC); fseek(ctrl->log_ptr, recpos, SEEK_SET); return 0; } /* ログファイルをクローズし、ログ制御ファイルの情報を更新する [引数] ctrl : ログ制御情報構造体 LOGCTRLへのポインター */ void closeLog(LOGCTRL* ctrl) { // ログファイルをクローズして fclose(ctrl->log_ptr); // 制御ファイルを更新する FILE *fp = fopen("log.ctrl", "wb"); fwrite(ctrl, sizeof(LOGCTRL), 1, fp); fclose(fp); }


(3)ログファイルへの書き込み

 ログ情報の記録は、カレント情報が記録されているLOGCTRL情報のポインターとログ情報種別を引数として呼び出します。ログ情報種別からメッセージを作成するという単純な処理です。あわせてカレント時刻とメモリーの状態を編集しています。ログ情報を記録する前に1レコード分を読み込み、ファイルポインターをレコードサイズ分だけ戻して上書きする方法を採っています。
/* writeLog : ログ情報をファイルに書き出す [引数] ctrl : ログ制御情報構造体 LOGCTRLへのポインター、type : ログ種別 [返却] 0 : OK、 0以外 : NG [注意] 外部変数を参照 (logging == 1)の場合のみログ出力する。 書き出しによってログ・カウンターがカウントアップされる。 */ int writeLog(LOGCTRL* ctrl, char type) { char kind[21] = ""; struct sysinfo info; ulong recpos; LOGREC rec; // ログ出力が指定されていなければ即復帰する if (logging == 0) return 0; // レコード数が最大なら出力位置をファイル先頭にセットする if (ctrl->rec_cnt >= ctrl->max_rec) { ctrl->rec_cnt = 0; fseek(ctrl->log_ptr, 0L, SEEK_SET); } // ファイルのカレント位置から1レコード分読み込んで if (fread(&rec, sizeof(LOGREC), 1, ctrl->log_ptr) < 1) return -1; // 書き出し位置を調整する fseek(ctrl->log_ptr, -sizeof(LOGREC), SEEK_CUR); memset(&rec, 0x00, sizeof(LOGREC)); // メモリー使用状態を取得する sysinfo(&info); unsigned int allmem = info.totalram/1024; unsigned int freemem = info.freeram/1024; // メッセージ種別からメッセージを編集する switch(type) { case LOGID_ERROVF: strcpy(kind, "/Input Overflow "); break; case LOGID_ERRUDF: strcpy(kind, "/Output Underflow "); break; case LOGID_ABEND: strcpy(kind, "/Abend PORTAUDIO "); break; case LOGID_BEGIN: strcpy(kind, "==== Begin ====== "); break; case LOGID_STRAT: strcpy(kind, "==== Start ====== "); break; case LOGID_END: strcpy(kind, "==== End ====== "); break; case LOGID_FINAL: strcpy(kind, "==== Final ====== "); break; case LOGID_CLEAR: strcpy(kind, "* Clear CutoffFe "); break; case LOGID_READ: strcpy(kind, "* Read temp.ctrl "); break; case LOGID_WRITE: strcpy(kind, "* Write temp.ctrl "); break; case LOGID_MODENON: strcpy(kind, ">> Applied : NON "); break; case LOGID_MODELPF: strcpy(kind, ">> Applied : LPF "); break; case LOGID_MODEHPF: strcpy(kind, ">> Applied : HPF "); break; case LOGID_MODEBPF: strcpy(kind, ">> Applied : BPF "); break; default: strcpy(kind, "????????????????? "); break; } // メッセージと日時・メモリー状態をカレント・ログファイルへ書き込んで time_t timer = time(NULL); struct tm *date = localtime(&timer); sprintf(rec.daytime, "%04d/%02d/%02d %02d:%02d:%02d", date->tm_year+1900, date->tm_mon+1, date->tm_mday, date->tm_hour, date->tm_min, date->tm_sec); memcpy(rec.message, kind, sizeof(rec.message)); sprintf(rec.note, "%d/%d KB", freemem, allmem); rec.note[39] = 0x0d; fwrite(&rec, sizeof(LOGREC), 1, ctrl->log_ptr); // ログのレコードカウンターをインクリメントする ctrl->rec_cnt = ctrl->rec_cnt + 1; return 0; }


5.オプション機能とテスト

(1)オプション機能の追加
 テストを始めると、さらにいくつかの機能が欲しくなります。そこで次の機能を追加しました。

①動作中のフィルター種別やカットオフ周波数を表示する機能
  [d]キーを押すと、BasicInfoに続いて次のように表示させます。
②動作中のフィルター種別やカットオフ周波数を初期値に戻す機能
  [c]キーを押すと初期化を行って、画面に"(CLEAR)"と表示させます。
③動作中のフィルター種別やカットオフ周波数を一時退避する機能
  [w]キーを押すとファイル'temp.ctrl'に退避して、画面に"(WRITE)"と表示させます。
④動作中のフィルター種別やカットオフ周波数を一時退避ファイルから復元する機能
  [r]キーを押すとファイル'temp.ctrl'から復元して、画面に"(READ)"と表示させます。

 これらのコードを以下に掲げます。
 主処理の「フィルター係数取得関数の実装」のコードに続けて、以下の記述を配置します。
/* (キー判定とフィルター係数取得処理等) */ switch (key) { : (省 略) : case 'd': // カットオフ周波数等を表示する displayBasicInfo(); break; case 'c': // カットオフ周波数を初期値に戻す initBasicInfo(); if (logging == 1) writeLog(&logctrl, LOGID_CLEAR); Pa_StopStream(stream); Pa_CloseStream(stream); printf(" (CLEAR) "); goto RESTART; case 'r': // カットオフ周波数等を一時ファイルから読み込む getBasicInfo("temp.ctrl"); if (logging == 1) writeLog(&logctrl, LOGID_READ); Pa_StopStream(stream); Pa_CloseStream(stream); printf(" (READ) "); goto RESTART; case 'w': // カットオフ周波数等を一時ファイルに書き出す putBasicInfo("temp.ctrl"); if (logging == 1) writeLog(&logctrl, LOGID_WRITE); printf(" (WRITE) "); break; : (省 略) : }

 それぞれのオプション機能の実行は次のコードが担います。
/* 遮断周波数等の初期値を設定する */ void initBasicInfo() { fe_hi_cutoff = cst_hi_cutoff; fe_lo_cutoff = cst_lo_cutoff; fe_low_pass = cst_low_pass; fe_high_pass = cst_high_pass; fe_differ = cst_step_diff; } /* カットオフ周波数等をファイルから設定する。 ファイル名が"base.ctrl"の時、ファイルが存在しなければ既定値を設定する。 [引数] 制御ファイル名 [返却] 0 : OK、 0以外 : NG */ int getBasicInfo(char *fname) { FILE *fp = fopen(fname, "rb"); if (fp == NULL ) { // ファイルが存在しなければ、 if (strcmp(fname, "base.ctrl") == 0) { // base.ctrなら遮断周波数等の初期値を設定する process_mode = MODE_THROUGH; initBasicInfo(); displayBasicInfo(); } else { process_mode = MODE_THROUGH; initBasicInfo(); putBasicInfo("temp.ctrl"); } } else { // 遮断周波数等をファイルから読み込んで設定する if (fread(&process_mode, sizeof(char), 1, fp) < 1) return -1; if (fread(&fe_hi_cutoff, sizeof(double), 1, fp) < 1) return -1; if (fread(&fe_lo_cutoff, sizeof(double), 1, fp) < 1) return -1; if (fread(&fe_low_pass, sizeof(double), 1, fp) < 1) return -1; if (fread(&fe_high_pass, sizeof(double), 1, fp) < 1) return -1; if (fread(&fe_differ, sizeof(double), 1, fp) < 1) return -1; displayBasicInfo(); fclose(fp); } return 0; } /* 処理中のカットオフ周波数等を表示する */ void displayBasicInfo() { printf("\nBasicInfo Filter [%c]\n\tLPF: %lf\n\tHPF: %lf\n\tBPF: %lf~%lf\n\tdif: %lf\n", process_mode, fe_low_pass, fe_high_pass, fe_hi_cutoff, fe_lo_cutoff, fe_differ); } /* カットオフ周波数等をファイルに書き込む。 [引数] 制御ファイル名 */ void putBasicInfo(char *fname) { FILE *fp = fopen(fname, "wb"); fwrite(&process_mode, sizeof(char), 1, fp); fwrite(&fe_hi_cutoff, sizeof(double), 1, fp); fwrite(&fe_lo_cutoff, sizeof(double), 1, fp); fwrite(&fe_low_pass, sizeof(double), 1, fp); fwrite(&fe_high_pass, sizeof(double), 1, fp); fwrite(&fe_differ, sizeof(double), 1, fp); fclose(fp); }


(2)テストの実施

 アプリケーションのソースファイル名はhat21a.cです。ファイル名の末尾の'a'はバージョン識別のために付けています。このソースコードをコンパイルするには、同じディレクトリーに2つのヘッダーファイル
  ・filter.h
  ・hat.h
が必要です。またフィルター処理関数オブジェクト
  ・filter.o
も同じフォルダーに配置してください。コンパイルは次のように行います。
$ gcc hat21a.c -o hat21 filter.o -lportaudio -lm
 コンパイルが完了すると、同じフォルダーに実行ファイルhat21が作成されます。
 これを次のようにして実行します。
$ ./hat21

 テストでは、コードで定義したキーを使ってフィルターの種別を選択したり、フィルターのカットオフ周波数を変更しながら、最も聞こえやすい条件を見つけることになります。同時に、それぞれのテスト実施者の聴覚にとって最適な音量バランスを調整することも必要です。
 音量バランスは、ADコンバーターとヘッドフォンアンプの可変抵抗器で調整しますが、場合によってはリモートデスクトップ経由でalsamixerを起動して、録音レベルや再生レベルを調整した方が良いケースが起きるかも知れません。
 テストをしやすいように、使用できるキーと機能をまとめておきます。
キー    機   能
lローパスフィルター(LPF)を選ぶ
hハイパスフィルター(HPF)を選ぶ
bバンドパスフィルター(BPF)を選ぶ
xフィルターなしを選ぶ
+使用中のフィルター(LPF/HPF/BPFの上限)のカットオフ周波数を上げる
-使用中のフィルター(LPF/HPF/BPFの上限)のカットオフ周波数を下げる
>BPFの下限カットオフ周波数を上げる
<BPFの下限カットオフ周波数を下げる
d現在のフィルター種別とカットオフ周波数を表示する
c現在のカットオフ周波数を初期値に戻す
r現在のフィルター種別とカットオフ周波数を復元する
w現在のフィルター種別とカットオフ周波数を退避する
[space]処理終了


(3)ログ情報の参照

 ログ情報は、実行ファイルがあるフォルダーにファイル名hat.logで作成されます。テキスト形式で記録しているので、テキストエディターで開くことができます。ただし循環ファイル方式で記録しているので、最大レコード数を超えたログは先頭位置から順次上書きされます。ログ情報は先頭に日付時刻が記録されているので、常に記録順に表示させるには次のように別ファイルへソートするとよいでしょう。
$ sort hat.log > work.log


(4)暫定的なコードの記述

 テスト実施中にアンダーフローなどのエラー発生を調整するために、何カ所かに対策のためのコードを記述しています。エラー発生時のリスタートなど明確な根拠をもったものと、念のためあるいは試行で記述したものがあります。これらは、今後コードを削除したりさらに変更するかも知れません。以下にその部分を列挙しておきます。
○リスタート対応
RESTART:  <== この部分! err = Pa_OpenStream(&stream, &inputParameters, &outputParameters, SAMPLE_RATE, FRAMES_PER_BUFFER, paClipOff, NULL, NULL); if(err != paNoError) goto error1; err = Pa_StartStream(stream); if(err != paNoError) goto error1; /* 音声入力再生処理 */ while (1) {

○ストリームのリード/ライト時のアクティブテスト
// Streamがアクティブでなければ待つ while (Pa_IsStreamActive(stream) != 1) { ; }

○ストリームへのライトでエラーが発生した場合のリスタート
// エラーが発生したらリスタートさせる err = Pa_WriteStream(stream, buffer, FRAMES_PER_BUFFER); if( err ) { printf("*"); fflush(stdout); if (logging == 1) writeLog(&logctrl, LOGID_ERRUDF); Pa_StopStream(stream); Pa_CloseStream(stream); goto RESTART; }

○suggested latencyの変更
  次のように、出力ストリームのsuggestedLatencyをdefaultLowOutputLatencyの0.001610から0.04に変更。
// outputParameters.suggestedLatency = outputInfo->defaultLowOutputLatency; outputParameters.suggestedLatency = 0.04;

 今まで開発したフィルターを使ってテストできるアプリケーションが完成しました。しばらくは、フィルターを選択して特徴を確認したり、カットオフ周波数や音量を変更しながら、聞こえやすい条件を試すことになります。それぞれの聴覚に最適な条件を絞り込んで見ましょう。


 聞こえやすさを個々の聴力にあわせて最適化するには、さらに周波数帯域単位での音量調整が必要になります。引き続きフィルターの機能を拡張して、次回はこれを可能にするイコライザーについて検討し、ロジックを設計する予定です。
 お楽しみに!


 
Copyright (C) 2011-2024 Marchan, All rights reserved.