1.起動・終了処理の仕組み
①自動起動とマニュアル起動
すでに第24章『イコライザーの実装』で自動起動の設定をしました。電源を入れ直したりリブートしたりすると、自動起動するのがわかります。ふだんはこれで十分なのですが、フィルターのカットオフ周波数などを変更したい場合は、パソコンなどからSSH接続して設定する必要があります。
めったに行わない操作なので、自動起動されているプロセスの終了は以下のように「ps aux | grep hat21」で検索して、killコマンドで停止させることにします。
HAT21に自動起動かマニュアル起動かを識別させるために、コマンドライン引数を利用します。HAT21ソースコードのmain関数を次のように変更して、引数に文字列「weblink」と指定されていれば自動起動と判断して、Web連携モード変数weblinkを1に設定します。
int main(int argc, char *argv[]) { // コマンドライン引数が「weblink」ならWeb連携モードを設定する if (argc > 1 && strncmp(argv[1], "weblink", 6) == 0) weblink = 1; : : |
そして、自動起動時には「weblink」の文字列を引数として起動するよう、/usr/local/bin/autostart.shの内容を次のように修正します。
#!/bin/sh cd /home/share/testAudio/hat wait /home/share/testAudio/hat/hat21 weblink |
こうしておけば、自動起動では引数に"weblink"が渡されて、コマンドライン引数を使わないマニュアル起動と判別することができるわけです。オーディオ処理開始の前に次のコードを追加して、Web連携モード変数weblinkがゼロ、つまり自動起動でない場合はWeb連携要否の指示を受けるようにします。
/* == Web連携の要否設定 == */ if (weblink != 1) // Web連携モードでなければ(自動起動でなければ)以下を確認する { while (1) { printf(" Web連携チューニングを有効にしますか(y/n)? : "); char ans = getchar(); if (ans == 'y') { weblink = 1; break; } if (ans == 'n') { weblink = 0; break; } } } |
②終了処理での電源オフ操作
リモートコンソールに設置した[Power Off]ボタンから、HAT21が動作中のPi Zeroの電源をOFFにするためには簡単なシェルスクリプトを使用します。
[Power Off]ボタンのクリックはHAT21の変更監視ロジックでキャッチされ、変数「int doPoweroff = 0」を1に変更して終了処理に分岐します。分岐先ではdoPoweroffが1に設定されていると、シェルスクリプトpoweroff.shを起動して電源をオフにします。
FinalProc: /* 終了処理 */ printf("\nHAT21: Hearing Assist Tool Closed!\n\n"); fflush(stdout); : : if (doPoweroff == 1) system("poweroff.sh"); return 0; |
/usr/local/bin/にpoweroffのシェルスクリプトを作成して、続く3行を書き込みます。
$ sudo nano /usr/local/bin/poweroff.sh |
#!/bin/sh sudo poweroff exit 0 |
$ sudo chmod +x poweroff.sh |
2.基本的な追加・変更
HAT21にWeb連携の仕組みを組み込むにあたり、準備作業として以下の各事項を行っておきます。
○ヘッダーファイルhat.hへの追加
次のログ情報種別を追加します。後で述べるようにログ情報の出力自体を無効化するために、当面この種別が使用されることはありませんが、全体の整合性をとるために追加しています。
#define LOGID_WEBSYNC 'q' // Web共用データ入力エラー |
続いて本体部分のプログラム(ダウンロードファイル内のhat21c.c)に以下の追加と変更を行っています。
①インクルードファイルの追加
Web連携共用データファイル作成時に、ファイルのアクセス権限を設定するために次のファイルをインクルードしておきます。
#include <sys/stat.h> |
②バージョン番号について
今回のプログラムソースファイルhat21c.cは、コンデンサーマイクを使った(つまりマイクアンプとADコンバーターを使った)試作機としては完成版です。本来なら正式バージョンとすべきですが、さらにデジタルマイクの見直しを考えているので、Version 0.30に留めることにします。
#define ID "HAT21 Version 0.30" |
③共用データフィールドの位置定義
共用データファイルの文字型データ配列の操作位置を次のとおり定義します。
// Web共用データフィールド位置 #define COMPOS_PROC 0 #define COMPOS_KEY 1 #define COMPOS_VALUE 2 #define COMPOS_MODE 3 #define COMPOS_NOISECUT 4 #define COMPOS_LVLL 5 #define COMPOS_LVLR 15 #define COMPOS_VOLM 24 #define COMPOS_MARK 25 #define COMREC_SIZE 26 |
④Web共用データの定義
ドキュメントルートの位置と、Web連携処理用のデータを定義します。
#define COMMON_DATA "/var/www/html/comdata" // Web linkage control int weblink = 0; // 1ならWebアプリと連携 struct timespec start_time; // タイマー制御用 char comdata[100]; // Web連携共用データ展開領域 FILE *fpcom; // Web連携共用データファイル int doPoweroff = 0; // Power off 制御用 |
⑤関数の新設
以下の3つの関数を追加しています。
○int型→char型
/* intをcharに変換する */ char cvt_itoc(int dt) { return '0' + dt; } |
○char型→int型
/* charをintに変換する */ int cvt_ctoi(char dt) { return dt - '0'; } |
○Web連携共用データファイルの作成
その時点のHAT21の設定情報をもとに、ドキュメントルートへ共用データを書き出します。
/* Web連携共用データファイルを作成する */ int createWebCommomFile() { comdata[COMPOS_PROC] = '_'; comdata[COMPOS_KEY] = '_'; comdata[COMPOS_VALUE] = cvt_itoc(0); comdata[COMPOS_MODE] = process_mode; for (int i=1; i<=EQ_BANDS; i++) { comdata[COMPOS_LVLL+i-1] = cvt_itoc(QL[i]); comdata[COMPOS_LVLR+i-1] = cvt_itoc(QR[i]); } comdata[COMPOS_NOISECUT]= cvt_itoc(tellNoiseSwitch()); comdata[COMPOS_VOLM] = cvt_itoc((int)(tellVolumeRate()*10)); comdata[COMPOS_MARK] = ';'; comdata[14] = '*'; comdata[COMREC_SIZE] = '\0'; if (test_mode == 1) printf("comdata : %s\n",comdata); FILE *fp = fopen(COMMON_DATA, "w"); if (fp == NULL) { printf("Can't open COMMON_DATA.\n"); return -1; } fputs(comdata, fp); fclose(fp); chmod(COMMON_DATA, S_IRWXG | S_IRWXO | S_IRWXU); // 全ユーザーに全権限を許可 return 0; } |
⑥ログ情報出力の無効化
ログ情報の書き込みを有効にしている int loggingにゼロを設定して、無効化します。これは、ログ情報の書き込みによって、PortAudioライブラリーのアンダーフローエラーを助長している疑いがあるための一時的な措置です。
これに関連して、従来のログ情報関連関数openLog()、closeLog()、writeLog()は、変数loggingが1でなければ動作しないよう変更しました。
3.共用データファイル制御
(1)Web連携と情報交換の方法
リモートコンソールで何かアクションがあれば、ドキュメントルートに設置した共用データファイルに変更内容が書き込まれます。HAT21側はこの内容を読み込んで、新たなアクション要求があったかどうかを判定して動作します。
新たな操作が発生したとの判断は、読み込んだ共用データの先頭1バイトが文字'p'になっていることで認識します。そして、対応した動作を行う前に、直ちに先頭の'p'を'_'に置換して書き戻します。HAT21はこのように変更を監視していて、操作が発生しなければ読み込んだ先頭が'_'なので何もしません。
(2)監視のタイミング
ここで問題になるのは、どんな間隔で共用データを読み込むかということです。無条件に読み込みを繰り返せば、ファイルリードによるビジー状態が発生します。もっとも簡単な秒タイマーを使って秒単位でアクセスすると、リモートコンソールの操作が反映するまでにわずかな遅れが発生してギクシャクした感じになってしまいます。
これを解消するために、次節に示すミリ秒タイマーを作成しました。試行の結果、250ミリ秒ごとに共用データをアクセスさせています。250ミリ秒に未達なら何もしません。コンソールからのアクション要求がない場合を含め、ここで「何もしない」とは、制御をPortAudioのストリーム入出力処理に委ねるということで、その間も基本的な音声処理は継続されます。
(3)ミリ秒タイマーの作成
きわめて簡単なロジックを記述し、単独のオブジェクトとしてコンパイルしています。ヘッダーファイルmsec_timer.hをインクルードして、HAT21コンパイル時に msec_timer.oをリンクすれば利用できます。
以下がソースコードです。
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "msec_timer.h"
struct timespec start_time, cur_time, pivot_time;
long sec, nsec;
double interval;
/*
ミリ秒タイマーを開始する
[引数] long interval: インターバルをミリ秒で指定
*/
void start_msec_timer(long itv)
{
interval = (double)itv;
clock_gettime(CLOCK_REALTIME, &start_time);
memcpy(&pivot_time, &start_time, sizeof(start_time));
}
/*
インターバルだけ経過したかを通知する
[返却] 1: 経過した、 0: 未経過
*/
int confirm_msec_timer()
{
clock_gettime(CLOCK_REALTIME, &cur_time);
sec = cur_time.tv_sec - start_time.tv_sec;
nsec = cur_time.tv_nsec - start_time.tv_nsec;
if (nsec < 0)
nsec += 1000000000L;
double passed_nsec = nsec / 1000000.0;
if (passed_nsec >= interval) {
memcpy(&start_time, &cur_time, sizeof(start_time));
return 1;
}
return 0;
}
/*
タイマー開始からの経過時間を通知する
[返却] 経過したミリ秒数
*/
long tell_passed_msec()
{
char buf[20];
double w;
clock_gettime(CLOCK_REALTIME, &cur_time);
sec = cur_time.tv_sec - pivot_time.tv_sec;
nsec = cur_time.tv_nsec - pivot_time.tv_nsec;
if (nsec < 0) {
sec--;
nsec += 1000000000L;
}
sprintf(buf, "%ld.%09ld", sec, nsec);
sscanf(buf, "%lf", &w);
return (long)(w * 1000.0);
}
HAT21のコード内では、マニュアル起動でWeb連携の要否を確認する処理に続いて、次のようにしてタイマーを開始しています。
if (weblink == 1) { printf(" ** Web連携で動作中 **\n"); // 250ミリ秒タイマーを開始する start_msec_timer(250); } |
4.Web連携処理の詳細
以下では、リモートコンソールとのWeb連携のために追加した部分だけをピックアップしています。全体は、ダウンロードファイルhat21c.cに記述されています。
(1)主処理の開始部分
追加変更部はハイライト表示しています。
/*
主処理
*/
int main(int argc, char *argv[])
{
// コマンドライン引数が「weblink」ならWeb連携モードを設定する
if (argc > 1 && strncmp(argv[1], "weblink", 6) == 0)
weblink = 1;
PaStreamParameters inputParameters, outputParameters;
PaStream *stream = NULL;
PaError err;
const PaDeviceInfo* inputInfo;
const PaDeviceInfo* outputInfo;
char *sampleBlock = NULL;
int numBytes;
int numChannels;
/* == 準備処理 == */
/* (ログファイルの準備) */
logctrl.max_rec = LOGSIZE;
if (openLog(&logctrl) != 0) {
printf("*ERR : ログファイルの準備に失敗しました\n");
exit(-1);
}
/* イコライザー初期化処理 */
initEqualizer(SAMPLE_RATE);
/* (遮断周波数等の準備) */
if (getBasicInfo("base.ctrl") != 0) {
printf("*ERR : 基本制御情報の準備に失敗しました\n");
exit(-2);
}
/* (イコライザー音量係数の設定) */
for (int i=0; i<=EQ_BANDS; i++) {
adjustQl(i, QL[i]);
adjustQr(i, QR[i]);
}
if (logging == 1) writeLog(&logctrl, LOGID_BEGIN);
/* (ライブラリー初期化処理) */
err = Pa_Initialize();
if(err != paNoError) goto error2;
/* (入力デバイス情報の取得) */
inputParameters.device = getSoftwareVolumeDevice();
if (inputParameters.device < 0) {
printf("Error: Not found software volume device.\n");
return -1;
}
inputInfo = Pa_GetDeviceInfo(inputParameters.device);
/* (出力デバイス情報の取得) */
outputParameters.device = Pa_GetDefaultOutputDevice();
outputInfo = Pa_GetDeviceInfo(outputParameters.device);
/* (チャンネル数の設定) */
numChannels = inputInfo->maxInputChannels < outputInfo->maxOutputChannels
? inputInfo->maxInputChannels : outputInfo->maxOutputChannels;
/* (入出力ストリームの設定) */
inputParameters.channelCount = numChannels;
inputParameters.sampleFormat = PA_SAMPLE_TYPE;
inputParameters.suggestedLatency = inputInfo->defaultHighInputLatency ;
// inputParameters.suggestedLatency = 0.02;
inputParameters.hostApiSpecificStreamInfo = NULL;
outputParameters.channelCount = numChannels;
outputParameters.sampleFormat = PA_SAMPLE_TYPE;
// outputParameters.suggestedLatency = outputInfo->defaultLowOutputLatency;
outputParameters.suggestedLatency = 0.04;
printf("defaultLowOutputLatency = %f, suggestedLatency = %f\n",
outputInfo->defaultLowOutputLatency,outputParameters.suggestedLatency);
outputParameters.hostApiSpecificStreamInfo = NULL;
/* (バッファ領域の確保) */
numBytes = FRAMES_PER_BUFFER * numChannels * SAMPLE_SIZE;
sampleBlock = (char *) malloc(numBytes);
if(sampleBlock == NULL)
{ printf("Could not allocate record array.\n");
goto error1;
}
/* (動作基本情報の表示) */
if (test_mode == 1) {
printf("Input device # %d.\n", inputParameters.device);
printf(" Name: %s\n", inputInfo->name);
printf(" LL: %g s\n", inputInfo->defaultLowInputLatency);
printf(" HL: %g s\n", inputInfo->defaultHighInputLatency);
printf("Output device # %d.\n", outputParameters.device);
printf(" Name: %s\n", outputInfo->name);
printf(" LL: %g s\n", outputInfo->defaultLowOutputLatency);
printf(" HL: %g s\n", outputInfo->defaultHighOutputLatency);
printf("Num channels = %d\n", numChannels);
printf("Frames/Buffer = %d\n", FRAMES_PER_BUFFER);
printf("Sample size = %d\n", SAMPLE_SIZE);
printf("Buffer size = %d\n", numBytes);
displayEqualizerTable();
}
if (logging == 1) writeLog(&logctrl, LOGID_STRAT);
if (logging == 1) writeLog(&logctrl, getFilterLogid(process_mode));
printf("\nHearing Assist Tool %s Start!\n", ID); fflush(stdout);
/* == Web連携の要否設定 == */
if (weblink != 1) // Web連携モードでなければ(自動起動でなければ)以下を確認する
{
while (1) {
printf(" Web連携チューニングを有効にしますか(y/n)? : ");
char ans = getchar();
if (ans == 'y') {
weblink = 1; break;
}
if (ans == 'n') {
weblink = 0; break;
}
}
}
if (weblink == 1) {
printf(" ** Web連携で動作中 **\n");
// 250ミリ秒タイマーを開始する
start_msec_timer(250);
}
/* == Web連携共用データファイルの確認・準備 == */
FILE *fp = fopen(COMMON_DATA, "r");
if (fp == NULL) {
if (createWebCommomFile() != 0) {
printf("*共用データファイルを作成できません!\n");
return -1;
}
printf("*共用データファイルを作成しました!\n");
}
else
fclose(fp);
/* オーディオ処理開始 */
memset(sampleBlock, SAMPLE_SILENCE, numBytes);
memset(buffer, SAMPLE_SILENCE, sizeof(buffer));
printf("\nApplied : [%s]", getFilterName(process_mode));
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;
(2)Web連携アクション要求の監視と処理
リモートコンソールからのアクション要求を監視し、要求があった操作を実行する部分です。ハイライト部分はコード末尾の〔ポイント〕に説明があります。
/* (キータッチ判定) */
char key;
if (weblink == 1) {
// Web連携なら経過時刻が250ミリ秒未満なら確認処理をスキップする
if (confirm_msec_timer() != 1)
continue;
// 共用データをcomdataに読み込んで
fpcom = fopen(COMMON_DATA, "r+");
if (fpcom == NULL) {
printf("Error: Can't open common data!\n");
if (logging == 1) writeLog(&logctrl, LOGID_WEBSYNC);
continue;
}
fgets(comdata, COMREC_SIZE, fpcom);
// 実行が指示されてなければスキップする
if (comdata[COMPOS_PROC] == '_') {
fclose(fpcom);
continue;
}
// 先に実行指示をリセットしておく
comdata[COMPOS_PROC] = '_';
comdata[COMREC_SIZE] = '\0';
rewind(fpcom);
fputs(comdata, fpcom);
fclose(fpcom);
int bnd, sns;
float vol;
if (test_mode == 1)
printf("[[ %c ]]\n", comdata[COMPOS_KEY]);
switch (comdata[COMPOS_KEY]) {
case 'm': // モード変更
key = comdata[COMPOS_MODE];
if (test_mode == 1)
printf("#Mode : %c\n", key);
break;
case 'L': // 左感度変更
channel = 'L';
bnd = cvt_ctoi(comdata[COMPOS_VALUE]);
sns = cvt_ctoi(comdata[COMPOS_LVLL+bnd-1]);
QL[bnd] = sns;
adjustQl(bnd, QL[bnd]);
if (test_mode == 1)
printf("#Left slider changed! band: %d, sens: %d\n", bnd, sns);
displayQuantity(channel);
continue;
case 'R': // 右感度変更
channel = 'R';
bnd = cvt_ctoi(comdata[COMPOS_VALUE]);
sns = cvt_ctoi(comdata[COMPOS_LVLR+bnd-1]);
QR[bnd] = sns;
adjustQr(bnd, QR[bnd]);
if (test_mode == 1)
printf("#Right slider changed! band: %d, sens: %d\n", bnd, sns);
displayQuantity(channel);
continue;
case 'n': // ノイズカット
changeNoiseSwitch();
if (test_mode == 1) {
if (tellNoiseSwitch() == 0)
printf("\n Noise suppress: OFF\n");
else
printf("\n Noise suppress: ON\n");
}
continue;
case 'v': // 音量調整
vol = (float)(cvt_ctoi(comdata[COMPOS_VOLM]) / 10.0);
volumeCtrl = vol;
setVolume(vol);
if (test_mode == 1)
printf("#Volume slider changed! volume: %f\n", vol);
continue;
case 'w': // 設定のファイル出力
if (test_mode == 1)
printf("#Write basicinfo to 'base.ctrl'.\n");
putBasicInfo("base.ctrl");
continue;
case 'z': // 終了処理をして電源を切る
if (test_mode == 1)
printf("#Do final process and Power OFF.\n");
doPoweroff = 1;
goto FinalProc;
case 'd': // 設定の表示
displayBasicInfo();
continue;
default:
continue;
}
}
〔ポイント〕
319行目: Web連携モードの場合にアクション要求を監視し、要求があった操作を実行します。
321~322行:250ミリ秒経過してなければ何もしません。
325行目: 共用データファイルを読み書きモードでオープンします。
331行目: 文字配列comdataに共用データを読み込みます。
334行目: 実行指示位置に実行指示がなければ('_'なら)何もしない。
340行目: アクション要求であることがわかったので、実行指示位置に'_'をセットする。
342~343行:ファイルポインターを先頭に戻して共用データを書き戻す。
351行目: 処理種別を示すキー情報を取り出して、その内容によって以降のケース文を実行する。
401~402行:電源オフが指示されていたら、パワーオフスイッチdoPoweroffを1にして終結処理へ分岐する。
(3)入力キーの判定と処理
コードは省略しますが、420~575行で、マニュアル起動モード時の入力キー判定と該当処理が行われます。この部分は従来通りで追加変更はありません。
(4)終了処理
FinalProc:
/* 終了処理 */
printf("\nHAT21: Hearing Assist Tool Closed!\n\n"); fflush(stdout);
createWebCommomFile();
err = Pa_StopStream(stream);
if(err != paNoError) goto error1;
free(sampleBlock);
Pa_Terminate();
if (logging == 1) writeLog(&logctrl, LOGID_FINAL);
putBasicInfo("base.ctrl");
closeLog(&logctrl);
if (doPoweroff == 1)
system("poweroff.sh");
return 0;
〔ポイント〕
583行目: 終了時の設定情報を元に共有データファイルを更新します。
591~592行:doPoweroffが1なら、シェルスクリプトpoweroff.shを起動して電源をオフにします。
5.動作検証
Raspberry Pi Zeroの電源を入れてしばらく待ってイヤフォンを耳にあてると、HAT21が自動起動されているのでマイクを通した音声が聞こえています。
この状態でPCのブラウザーを立ち上げて、アドレスバーからhat21.htmlにアクセスします。手元の環境では次のように入力します。
http://192.168.0.38/hat21.html
リモートコンソールが表示されるので、イヤフォンを装着したままでフィルター種別を切り替えたり、ボリュームのスライドバーを動かしてみます。フィルター種別では、Equalizerを選択すると左右9つの感度調整スライドバーが有効になり、それ以外を選ぶと無効化されることがわかります。またボリュームを調整すると、イヤフォンの音量が変化します。操作のたびにタイトルの下に表示されている共用データの値が変化して、HAT21との更新状態をモニターすることができます。
ブラウザーを閉じて、今度はスマートフォンからコンタクトします。Android上のブラウザーChrome(version 91.0.4472)を立ち上げて、PCと同じようにhat21.htmlにアクセスして操作してみましょう。Equalizerモードでの感度調整も簡単で、以前の数値キーとシフトキーでやっていたのとは比較になりません。いろいろ試した後に[Power Off]ボタンをクリックします。これに反応して、Pi Zeroの電源が切れました。
もう一度Pi Zeroの電源を入れます。今度はSSH経由で立ち上げたいので、PCからSSHでPi Zeroに接続します。すでにHAT21が作動しているので、まずこれを止める必要があります。
$ ps aux | grep hat21 |
プロセスユーザーに続いて表示されるPID(プロセス番号)をKillコマンドで停止させます。複数行表示された場合は、行末が起動コマンドになっているもの、手元の環境では「/home/share/testAudio/hat/hat21」になっている行のプロセス番号を指定、例えば「311」であれば次のようにして停止させます。
$ kill 311 |
そして、HAT21があるディレクトリーに移動してハンドで起動します。すでに動作しているHAT21を止めずに起動すると、「An error occured while using the portaudio stream」エラーになるので注意してください。
$ cd /home/share/testAudio/hat $ ./hat21 : : (省略) : : Hearing Assist Tool HAT21 Version 1.0 Start! Web連携チューニングを有効にしますか(y/n)? : |
ここで'y'を入力すれば、自動起動と同じように動作してリモートコンソールが有効になります。この状態では、キーボードからの制御は一切できなくなります。フィルター別のカットオフ周波数の調整が必要な場合などでは、'n'を入力すればリモートコンソールを導入する前の状態で動作します。キーボードから必要な調整を行った後はスペースキーで処理を終了させることができます。
リモートコンソールのデザインはさらに改善の余地がありますが、所期の目的は達成できたようです。やはり専用のコンソールができると断然使いやすくなりますね。次回は、先延ばししていた電源について検討し、最終的な姿を決める予定です。お楽しみに!