24. イコライザーの実装

 前回作成したイコライザーのコードを実装する段階で、発信を伴ったノイズが発生して対応に四苦八苦。イコライザーを有効にしたとたんにブィーンという発振、ザヮザヮと音が濁ってしまうのです。ハードのチェックとソフトの見直しでも原因がわからずしばらく放置状態でした。ところが、イコライザー信号処理の遅延処理用変数をグローバル変数にするとピタリと止まりました。考えてみれば、遅延処理は継続的に行われる必要があるので、前の値は保持され続けられなければならないわけです。これが毎回初期化されてしまうのが原因でした。グローバル変数は避けたいので、イコライザー信号処理関数内に静的変数として定義することで解決しました。
 今回の実装で、自作ADコンバーターとヘッドフォンアンプを使った試作機はほぼ完成することになります。ただ、なお取り組むべき大きな課題を残していて、それは最後の節でまとめることにします。

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



1.追加する新機能

 まず、今回追加する機能について簡単に取りまとめておきましょう。
①ミュート機能

 テスト中に再生を一時的に中断したり再開するためのミュート機能を追加します。大文字キー[M]を押すことで中断と再開を反転させます。


②音量調整機能

 再生中の音量を、左右同時にアップ/ダウンさせる機能です。大文字キー[U]でアップ、[D]でダウンさせます。


③イコライザーチャンネル選択機能

 イコライザーは左右を独立して設定できるようにします。イコライザーモードへの切り替えは小文字キー[e]で行いますが、切り替えた直後は左チャンネルが選択されます。これを右チャンネルに切り替えるには大文字キー[R]を、もう一度左へ戻すには大文字キー[L]を押します。
 左右どちらかのチャンネルが選択された状態で数値キー[1]~[9]を押すと、そのたびに対応するチャンネルのバンド(周波数帯域)1~9の感度(音量レベル)を1レベルずつアップさせます。[Shift]キーを押さえた状態で数値キーを押すと、対応したバンドの感度を1レベルずつダウンさせます。
 イコライザーチャンネル選択状態で数値キーが押されると感度に影響を与えるので、設定完了後はチャンネル選択完了を指示して数値キーを無効にする必要があります。大文字キー[Q]でこれを指示します。


④イコライザーバンド感度の表示

 数値キーでバンドの感度を調整すると、その都度バンドの感度状態を表示させます。


⑤その他の機能拡張

 これまで小文字キーの[w]と[r]で適用中のフィルター種別やカットオフ周波数を一時ファイルに書き込み/入力させていましたが、これにイコライザーのバンド別周波数情報を追加します。また小文字キー[d]で表示していたカットオフ周波数等の情報に、イコライザーのバンド別ノッチ周波数、フィルター係数、左右の感度を追加します。


⑥リミッターとノイズ抑制処理の追加

 今回のテストをしている最中に原因不明のアンダーフローが連続発生し、猛烈なノイズに襲われました。音量を上げている場合は耳に危険を及ぼすことにもなりかねません。これを防ぐためにリミッターを設けて、出力シグナルがある音量レベルを超えた場合はそのレベルで抑えることにしました。あわせて、入力信号に含まれる微小なノイズを抑制するための閾値とロジックを設定します。


2.音声信号処理モジュールの改版

 前回作成したイコライザー関係のコードや今回の機能追加により、音声信号処理モジュールは大幅に変わります。
①ヘッダーファイル

 従来のfilter.hにわずかな#define文の追加といくつもの関数プロトタイプ定義を記述します。ファイル名をfilter2.hと改称しました。#define文の追加は以下のハイライト部分で、18行目以降の関数プロトタイプ部分は省略します。
/*
	処理モード(キー文字と一致させる)
*/
#define	MODE_LOWPASS	'l'				// ローパス
#define	MODE_HIGHPASS	'h'				// ハイパス
#define	MODE_BANDPASS	'b'				// バンドパス
#define MODE_EQUALIZER	'e'				// イコライザー
#define	MODE_THROUGH	'x'				// フィルターなし

#define	EQ_BANDS	9					// イコライザー・バンド数


②音声信号処理関数モジュール
 コード前半の次のハイライト部分を追加しました。ファイル名がfilter.cからfilter2.cに変わります。

#include <stdio.h>
#include <math.h>
#include "filter2.h"

// 音量調整用
int		mute_sw = 0;			// ミュート・スイッチ
float	mute_cst = 0.0;			// ミュート定数
float	volume = 0.5;			// 音量係数
float	volume_cst = 0.1;		// 音量増減定数
float	volume_min = 0.0;		// 音量最小値
float	volume_max = 1.0;		// 音量最大値
double	limit_val = 0.95;		// リミッター閾値
int		noise_sw  = 0;			// ノイズ除去スイッチ
double	noise_val = 0.005;		// ノイズ除去閾値

// イコライザー用
double	fc[EQ_BANDS+1] = {0};   // ノッチ周波数
double	Ql[EQ_BANDS+1] = {0};	// 感度・左チャンネル
double	Qr[EQ_BANDS+1] = {0};	// 感度・右チャンネル
double	a[EQ_BANDS+1], r[EQ_BANDS+1];  // フィルター係数


 第20章の『デジタルフィルターの作成』で作成した2つのフィルター適用処理関数、applyPassFilterとapplyThroughFilterは、ミュートと音量調整さらにノイス抑制処理とリミッター機能の追加で次のように変わりました。
 なお、音声処理でアンダーフローやオーバーフローエラーが発生したことを知らせるため、コンソールに'*'と'$'を表示していますが、同様に、音声信号がリミッターの値を超えたら'X'を表示させています。

/*
	applyPassFilter : ローパス、ハイパス、バンドパスフィルター信号処理関数
		[引数]  int order	   : フィルターの次数
				int frames     : バッファのフレーム数
				float  *inptr  : 入力バッファへのポインター
				double *h      : フィルター係数配列へのポインター
				float  *outptr : 出力バッファへのポインター
*/
void applyPassFilter(int order, int frames, float *inptr, double *h, float *outptr)
{
	double sl, sr, yl, yr, zl, zr;
	int over, done;
	
	over = done = 0;

	for (int i=0; i<frames; i++) {
		if (mute_sw == 0) {
			sl = (double)*inptr++;
			sr = (double)*inptr++;
			if (noise_sw !=0) {									// ノイズ除去を処理する
				if (sl >= -noise_val && sl <= noise_val)
					sl = 0;
				if (sr >= -noise_val && sr <= noise_val)
					sr = 0;
			}
			yl = yr = 0;
			for (int j=0; j<=order; j++) {
				yl = yl + sl * h[j];
				yr = yr + sr * h[j];
			}
			zl = (float)(yl * volume);
			if (zl > limit_val) {								// リミッターをかける
				zl = limit_val;		over = 1;
			}
			else if (zl < -limit_val) {
				zl = -limit_val;	over = 1;
			}
			zr = (float)(yr * volume);
			if (zr > limit_val) {
				zr = limit_val;		over = 1;
			}
			else if (zr < -limit_val) {
				zr = -limit_val;	over = 1;
			}
			if (over == 1 && done == 0) {
				printf("X");	done = 1;
			}
			*outptr++ = (float)zl;
			*outptr++ = (float)zr;
		}
		else {
			*outptr++ = mute_cst;
			*outptr++ = mute_cst;
		}
	}
}

/*
	applyThroughFilter : フィルターなし(素通し)信号処理関数
		[引数]  int frames      : バッファのフレーム数
				float* inptr    : 入力バッファへのポインター
				float* outptr   : 出力バッファへのポインター
*/
void applyThroughFilter(int frames, float *inptr, float *outptr)
{
	float sl, sr, zl, zr;
	int over, done;
	
	over = done = 0;

	for (int i=0; i<frames; i++) {
		if (mute_sw == 0) {
			sl = *inptr++;
			sr = *inptr++;
			if (noise_sw !=0) {									// ノイズ除去を処理する
				if (sl >= -noise_val && sl <= noise_val)
					sl = 0;
				if (sr >= -noise_val && sr <= noise_val)
					sr = 0;
			}
			zl = (float)(sl * volume);
			if (zl > limit_val) {								// リミッターをかける
				zl = limit_val;		over = 1;
			}
			else if (zl < -limit_val) {
				zl = -limit_val;	over = 1;
			}
			zr = (float)(sr * volume);
			if (zr > limit_val) {
				zr = limit_val;		over = 1;
			}
			else if (zr < -limit_val) {
				zr = -limit_val;	over = 1;
			}
			if (over == 1 && done == 0) {
				printf("X");	done = 1;
			}
			*outptr++ = (float)zl;
			*outptr++ = (float)zr;
		}
		else {
			*outptr++ = mute_cst;
			*outptr++ = mute_cst;
		}
	}
}


 209行目以降に以下のコードを追加しています。今回の機能追加のうち、機能選択やイコライザーのバンド別感度調整などキー入力を伴う部分以外の大半がここに記述されています。

/*
	changeMuteMode : ミュートをON/OFFする
*/
void changeMuteMode()
{
	mute_sw==0 ? (mute_sw=1) : (mute_sw=0);
}

/*
	tellMuteMode : ミュートの状態を通知する
		[返却]	int mute_sw
*/
int tellMuteMode()
{
	return mute_sw;
}

/*
	raiseVolume : 音量係数をアップする
*/
void raiseVolume()
{
	if (volume < volume_max && mute_sw == 0)
		volume += volume_cst;
}

/*
	reduceVolume : 音量係数をダウンするする
*/
void reduceVolume()
{
	if (volume > (volume_min+0.01) && mute_sw == 0)
		volume -= volume_cst;
}

/*
	setVolume : 音量係数を直接設定する(Web連携用)
*/
void setVolume(float val)
{
	volume = val;
}

/*
	音量係数を通知する
		[返却]	float volume ミュート中ならマイナス値
*/
float tellVolumeRate()
{
	return volume;
}

/*
	initEqualizer : イコライザーの初期化処理
		[引数]  double Fs   :  サンプリング周波数
				int level   : 感度(0~9)
*/
void initEqualizer(double Fs)
{
	double Ke;				// 帯域幅
	double w;

	fc[0] = 16.0;
    for (int i=1; i<=EQ_BANDS; i++) {
         Ke   = fc[i-1];                              			// 帯域幅は低域側ノッチ周波数を使用
         w    = 2.0*M_PI * Ke / Fs;
         r[i] = (1 + cos(w) - sin(w)) / (1 + cos(w) + sin(w));	// 帯域幅を設定
         fc[i]= fc[i-1] * 2;
         a[i] = -(1 + r[i]) * cos(2.0*M_PI*fc[i]/Fs);           // フィルタ係数
    }
}

/*
	adjustQl : 左側感度を設定する
		[引数] int band : 対象バンド
				int q    : 感度(0~9)
*/
void adjustQl(int band, int q)
{
	if (band >= 0 && band <= EQ_BANDS)
		Ql[band] = (double)q;
}

/*
	adjustQr : 右側感度を設定する
		[引数] int band : 対象バンド
				int q    : 感度(0~9)
*/
void adjustQr(int band, int q)
{
	if (band >= 0 && band <= EQ_BANDS)
		Qr[band] = (double)q;
}

/*
	tellQl : 左側感度を通知する
		[引数] int band  : 対象バンド
		[返却]	int Ql[・] : 感度(0~9)
*/
int tellQl(int band)
{
	if (band >= 0 && band <= EQ_BANDS)
		return (int)Ql[band];
	else
		return 0;
}

/*
	tellQr : 左側感度を通知する
		[引数] int band  : 対象バンド
		[返却]	int Qr[・] : 感度(0~9)
*/
int tellQr(int band)
{
	if (band >= 0 && band <= EQ_BANDS)
		return (int)Qr[band];
	else
		return 0;
}

/*
	applyEqualizer : イコライザー信号処理関数
		[引数]  int frames     : バッファのフレーム数
				float  *inptr  : 入力バッファへのポインター
				float  *outptr : 出力バッファへのポインター
*/
void applyEqualizer(int frames, float *inptr, float *outptr)
{
	static	double	u0l[EQ_BANDS+1] = {0};	// u0~u2は静的変数でないとノイズが発生する
	static	double	u1l[EQ_BANDS+1] = {0};
	static	double	u2l[EQ_BANDS+1] = {0};
	static	double	u0r[EQ_BANDS+1] = {0};
	static	double	u1r[EQ_BANDS+1] = {0};
	static	double	u2r[EQ_BANDS+1] = {0};
	double	z = 0.3;	//(感度の調整値)
	double	xl, xr, sl, sr, yl, yr, zl, zr, ql, qr;
	int		over, done;
	
	over = done = 0;

	for (int i=0; i<frames; i++) {
		sl = (double)*inptr++;
		sr = (double)*inptr++;
		if (noise_sw !=0) {										// ノイズ除去を処理する
			if (sl >= -noise_val && sl <= noise_val)
				sl = 0;
			if (sr >= -noise_val && sr <= noise_val)
				sr = 0;
		}
		yl = yr = 0;
		for(int j=1; j<=EQ_BANDS; j++) {
			u0l[j] = sl - a[j] * u1l[j] - r[j] * u2l[j];		// 逆ノッチフィルタの内部信号計算
			u0r[j] = sr - a[j] * u1r[j] - r[j] * u2r[j];
			xl     = r[j] * u0l[j] + a[j] * u1l[j] + u2l[j];	// オールパスフィルタ出力計算
			xr     = r[j] * u0r[j] + a[j] * u1r[j] + u2r[j];
			yl   = yl + (Ql[j]*z) * (sl - xl)/2.0;				// 逆ノッチ出力に感度をつけて加算
			yr   = yr + (Qr[j]*z) * (sr - xr)/2.0;
			u2l[j] = u1l[j];  u1l[j] = u0l[j];					// 信号遅延
			u2r[j] = u1r[j];  u1r[j] = u0r[j];
		}
		if (mute_sw == 0) {
			ql = zl = yl * volume;
			if (zl > limit_val) {								// リミッターをかける
				zl = limit_val;		over = 1;
			}
			else if (zl < -limit_val) {
				zl = -limit_val;	over = 1;
			}
			qr = zr = yr * volume;
			if (zr > limit_val) {
				zr = limit_val;		over = 1;
			}
			else if (zr < -limit_val) {
				zr = -limit_val;	over = 1;
			}
			if (over == 1 && done == 0) {
				printf("X");	done = 1;
			}
			*outptr++ = (float)atan(zl / (M_PI/2.0));           // クリップ防止で出力バッファ転送
			*outptr++ = (float)atan(zr / (M_PI/2.0));
		}
		else {
			*outptr++ = mute_cst;
			*outptr++ = mute_cst;
		}
	}
}

/*
	changeNoiseSwitch : ノイズスイッチをON/OFFする
*/
void changeNoiseSwitch(void)
{
	noise_sw==0 ? (noise_sw=1) : (noise_sw=0);
}

/*
	setNoiseSwitch : ノイズスイッチを設定する
		[引数] int stat  : 状態(0/1)
*/
void setNoiseSwitch(int stat)
{
	noise_sw = stat;
}

/*
	tellNoiseSwitch : ノイズスイッチの状態を通知する
		[返却]	int noise_switch
*/
int tellNoiseSwitch(void)
{
	return noise_sw;
}

/*
	displayEqualizerTable : イコライザー情報を表示する
*/
void displayEqualizerTable()
{
    printf("\n[Equalizer]\n");
    printf(" #     r[・],     fc[・],     a[・],   Q[L=R]\n");
	for (int i=0; i<=EQ_BANDS; i++) {
		printf("%2d : %6f, %7.0f, %9.6f, %4.1f=%.1f\n", i, r[i], fc[i], a[i], Ql[i], Qr[i]);
	}
	printf("\n");
}


3.聴覚補助ツールHAT21の改版

 従来のhat21a.cに対して大幅な修正や追加が必要なことから、新たにhat21b.cファイルにコードを記述します。以下ではhat21a.cから変わった部分をハイライト表示しています。
①関数プロトタイプ宣言、データ定義部

 イコライザー信号処理用のバンド別感度は音声信号処理関数モジュール内に定義していますが、それを表示するためにHAT21側でもQL[EQ_BANDS+1],QR[EQ_BANDS+1]として領域を確保していて、感度の変更が指定されるたびにadjustQl(),adjustQr()関数で同期をとっています。
#define	ID	"HAT21 Version 0.20"

#define SAMPLE_RATE			(96000)		// サンプリングレート
#define FRAMES_PER_BUFFER	(512)		// フレームバッファ・サイズ
#define	FILTER_ORDER		64			// フィルターの次数
#define	LOGSIZE				1000		// ログファイルの最大レコード数

#define PA_SAMPLE_TYPE		paFloat32
#define SAMPLE_SIZE			(sizeof(float))
#define SAMPLE_SILENCE		(0.0f)

// Function prototype
int		main(void);
void	raiseFrequency(void);
void	reduceFrequency(void);
void	raiseBandFrequency(void);
void	reduceBandFrequency(void);
void	initBasicInfo(void);
int		getBasicInfo(char*);
void	putBasicInfo(char*);
void	displayBasicInfo(void);
char	getFilterLogid(char);
char*	getFilterName(char);
int		getSoftwareVolumeDevice(void);
void	displayQuantity(char);
int		kbhit(void);
int		openLog(LOGCTRL*);
void	closeLog(LOGCTRL*);
int		writeLog(LOGCTRL*, char);

// Monitoring switch & control
int 	test_mode = 1;
int		logging = 1;

LOGCTRL	logctrl;
LOGREC	logrec;

// 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;						// ステップ毎の調整値

// Volume control
float	volumeCtrl = 0.5;				// 音量コントロール

// Range of frequency
const double cst_fe_upper = 15000.0;	// 上限周波数
const double cst_fe_lower = 100.0;		// 下限周波数

// For signal processing
char 	process_mode;					// 処理モード(処理中のフィルタリング種別)
double	h[FILTER_ORDER+1] = {0};		// フィルター係数
float	buffer[FRAMES_PER_BUFFER][2];	// Playback buffer

// For Equalizer
int		QL[EQ_BANDS+1] = {0};			// 感度・左チャンネル
int		QR[EQ_BANDS+1] = {0};			// 感度・右チャンネル
char	channel = ' ';					// イコライザー設定チャンネル('L'/'R'/' ')


②主処理:初期化処理部

 ここでイコライザーの初期化処理と感度の設定をしています。
	/* == 準備処理 == */
	/* (ログファイルの準備) */
	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);


③主処理:音声入力再生処理部

 イコライザーモードの時はここでイコライザー信号処理関数を実行させています。
		/* (フィルター信号処理) */
		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;
			case MODE_EQUALIZER:
				applyEqualizer(FRAMES_PER_BUFFER, (float*)sampleBlock, (float*)buffer);
				break;
		}


④主処理:フィルター係数取得処理部

 イコライザーモードを指示されると左右の感度を表示して、処理モードをイコライザーに設定します。
		/* (キー判定とフィルター係数取得処理等) */
		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;
			case MODE_EQUALIZER:
				printf("\n");
				displayQuantity('L');  printf("\n");
				displayQuantity('R');  printf("\n");
				if (process_mode != MODE_EQUALIZER) {
					process_mode = MODE_EQUALIZER;
					printf("\nApplied : [EQU]");
					if (logging == 1)	writeLog(&logctrl, LOGID_EQUALIZER);
					channel = 'L';
					printf("\nEqualizer: Left channel selected.\n");	// 左チャンネルを選択させる
				}
				break;


⑤主処理:キー判定処理部

 270行目のキー判定の続きに、342行目以降のコードを追加しています。追加機能のキー判定と関数呼び出しはすべてここで行っています。処理内容はコメントのとおりです。
			case 'w':	// カットオフ周波数等を一時ファイルに書き出す
				putBasicInfo("temp.ctrl");
				if (logging == 1)	writeLog(&logctrl, LOGID_WRITE);
				printf(" (WRITE) ");	break;
			case 'M':	// ミュートをON/OFFする
				changeMuteMode();
				if (tellMuteMode() == 0)
					printf("\n Mute:OFF\n");
				else
					printf("\n Mute:ON\n");
				break;
			case 'N':	// ノイズ除去スイッチをON/OFFする
				changeNoiseSwitch();
				if (tellNoiseSwitch() == 0)
					printf("\n Noise eraser: OFF\n");
				else
					printf("\n Noise eraser: ON\n");
				break;
			case 'U':	// 音量アップ
				raiseVolume();
				volumeCtrl = tellVolumeRate();
				if (volumeCtrl >= 0.0)
					printf("(%3.2f)\n", volumeCtrl);
				break;
			case 'D':	// 音量ダウン
				reduceVolume();
				volumeCtrl = tellVolumeRate();
				if (volumeCtrl >= 0.0)
					printf("(%3.2f)\n", volumeCtrl);
				break;
			case 'L':	// イコライザー左チャンネル設定指示
				process_mode = MODE_EQUALIZER;
				channel = 'L';
				printf("\nEqualizer: Left channel selected.\n");
				break;
			case 'R':	// イコライザー右チャンネル設定指示
				process_mode = MODE_EQUALIZER;
				channel = 'R';
				printf("\nEqualizer: Right channel selected.\n");
				break;
			case 'Q':	// イコライザー設定終了
				channel = ' ';
				printf("\nEqualizer: Channel selector is disabled.\n");
			default:
				if (process_mode == MODE_EQUALIZER) {
					if (channel == ' ')							// チャンネル未指定ならスキップ
						break;

		            if (key >= 49 && key <= 49+EQ_BANDS-1) {	  // 数字キー押下時
						int pos = key-48;
						if (channel == 'L') {
			                QL[pos] = QL[pos] + 1;				// クオリティファクタを1増加
			                if(QL[pos] > 9)					   // 9を超えると9に変更
								QL[pos] = 9;
							adjustQl(pos, QL[pos]);
							displayQuantity(channel);
						}
						else {
			                QR[pos] = QR[pos] + 1;				// クオリティファクタを1増加
			                if(QR[pos] > 9)					   // 9を超えると9に変更
								QR[pos] = 9;
							adjustQr(pos, QR[pos]);
							displayQuantity(channel);
						}
		            }
		            else if (key >= 33 && key <= 33+EQ_BANDS-1) {	// [SHIFT] + 数字キー押下時
						int pos = key-32;
						if (channel == 'L') {
			                QL[pos] = QL[pos] - 1;				// クオリティファクタを1減少
			                if(QL[pos] < 0)
								QL[pos] = 0;					// 0を割り込むと0に変更
							adjustQl(pos, QL[pos]);
							displayQuantity(channel);
						}
						else {
			                QR[pos] = QR[pos] - 1;				// クオリティファクタを1減少
			                if(QR[pos] < 0)
								QR[pos] = 0;					// 0を割り込むと0に変更
							adjustQr(pos, QR[pos]);
							displayQuantity(channel);
						}
		            }
		            key = 0;
				}
				else printf("\r");
				break;
		}
	}


⑥いくつかの関数の追加と修正

・カットオフ周波数等をファイルから設定する。
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 {
		// 遮断周波数等をファイルから読み込んで設定する
		int noise_stat;
		if (fread(&process_mode, sizeof(char), 1, fp) < 1)		return -1;
		if (fread(&volumeCtrl, sizeof(float), 1, fp) < 1)		return -1;
		if (fread(&noise_stat, sizeof(int), 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;
		for (int i=0; i<=EQ_BANDS; i++) {
			if (fread(&QL[i], sizeof(int), 1, fp) < 1)	return -1;
			if (fread(&QR[i], sizeof(int), 1, fp) < 1)	return -1;
		}
		setVolume(volumeCtrl);	setNoiseSwitch(noise_stat);
		displayBasicInfo();
		fclose(fp);
	}
	return 0;
}
・カットオフ周波数等をファイルに書き込む。
void putBasicInfo(char *fname)
{
	int	noise_stat = tellNoiseSwitch();

	FILE *fp = fopen(fname, "wb");
	fwrite(&process_mode, sizeof(char), 1, fp);
	fwrite(&volumeCtrl, sizeof(float), 1, fp);
	fwrite(&noise_stat, sizeof(int), 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);
	for (int i=0; i<=EQ_BANDS; i++) {
		fwrite(&QL[i], sizeof(int), 1, fp);
		fwrite(&QR[i], sizeof(int), 1, fp);
	}
	fclose(fp);
}
・フィルター種別からログIDを取得する。
char getFilterLogid(char mode)
{
	char logid;

	switch (mode) {
		case MODE_LOWPASS:		logid = LOGID_MODELPF;		break;
		case MODE_HIGHPASS:		logid = LOGID_MODEHPF;		break;
		case MODE_BANDPASS:		logid = LOGID_MODEBPF;		break;
		case MODE_THROUGH:		logid = LOGID_MODENON;		break;
		case MODE_EQUALIZER:	logid = LOGID_EQUALIZER;	break;
		default:				logid = '?';				break;
	}
	return logid;
}
・フィルター種別からフィルター名を取得する。
char* getFilterName(char mode)
{
	static char filter_name[10];
	switch (mode) {
		case MODE_LOWPASS:		strcpy(filter_name, "LPF");	break;
		case MODE_HIGHPASS:		strcpy(filter_name, "HPF");	break;
		case MODE_BANDPASS:		strcpy(filter_name, "BPF");	break;
		case MODE_THROUGH:		strcpy(filter_name, "NON");	break;
		case MODE_EQUALIZER:	strcpy(filter_name, "EQU");	break;
		default:				strcpy(filter_name, "");	break;
	}
	return filter_name;
}
・帯域別音量を表示する。
/*
	displayQuantity : 帯域別音量を表示する
		[引数] char ch : チャンネル('L'または'R')
*/
void displayQuantity(char ch) {
	printf("  Lv(%c) ", ch);
	for(int i=1; i<=EQ_BANDS; i++){
		if (ch == 'L')
			printf(" %d,",(int)(QL[i]));
		else
			printf(" %d,",(int)(QR[i]));
	}
	printf("        \r");
}
・ログ情報をファイルに書き出す。
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;
		case LOGID_EQUALIZER:	strcpy(kind, ">> Applied : EQU  ");	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;
}


4.HAT21のチューニング

 これでソフトウェアも準備が整い、コンパイルして実行させるばかりの状態になりました。イコライザーを有効にすると周波数帯域別の感度を細かく調整できるので、とても聴きやすくなると思います。

<注意!>
 チューニングでは最初にヘッドフォンアンプの音量を絞って、耳を傷めないよう注意してください!

①コンパイルと実行

 出来上がった聴覚補助ツールHAT21(Hearing Assist Tool 2021)Version 0.20は、次のファイルで構成されています。
  ・hat.h(聴覚補助ツールHAT21用ヘッダーファイル)
  ・hat21b.c(聴覚補助ツールHAT21 Version 0.20ソースコード)
  ・filter2.o(フィルター係数計算・音声信号処理オブジェクトモジュール)
 ダウンロード解凍後のフォルダーには、実行形式のプログラムを含むすべてのファイルが入っているのですぐに実行できますが、あらためてコンパイルする場合は次のようにします。
$ gcc hat21b.c -o hat21 filter2.o -lportaudio -lm
 コンパイルが完了すると、同じフォルダーに実行ファイルhat21が作成されます。
 このプログラムは次のようにして実行します。
$ ./hat21
 なお、コンパイル時に指定したオブジェクトモジュールfilter2.oは、次の2つのファイルから構成されているので、必要に応じてリコンパイルしてください。
  ・filter2.h(フィルター係数計算・音声信号処理モジュール用ヘッダーファイル)
  ・filter2.c(フィルター係数計算・音声信号処理モジュール ソースコード)
$ gcc -c filter2.c


②基本的なチューニング

 HAT21には次のような音量の可変要素があります。
  ・マイクアンプの左右各2段の半固定抵抗器
  ・ADコンバーターの左右の半固定抵抗器
  ・ヘッドフォンアンプの左右の半固定抵抗器
  ・ソフトウェアボリューム
  ・HAT21の音量調整機能
  ・HAT21のイコライザーのバンド別感度調整機能
 これらを適当に調整すると混乱してしまいます。そこで、一例として以下のような進め方を提案します。なお、ここではマイクアンプにコンデンサーマイクを接続した場合を述べていますが、従前の高感度アンプ付きコンデンサーマイクをADコンバーターに接続する場合は、適宜手順を省略してください。

Step-1: システムの既定値を設定する
 ・HAT21の音量調整機能は0.0から0.1刻みで1.0まで調整でき、既定値は0.5です。
  これは当面は変更しないことにします。
 ・次にソフトウェアボリュームの再生レベルを85、録音レベルを80に設定します。
  第16章『ADCで試すアナログマイク①』の「5.ソフトウェアボリュームのテスト」を参考にしてください。

Step-2: マイクアンプのチューニング
 ・マイクアンプにマイクを繋ぎ、出力側をヘッドフォンアンプに直結します。
 ・アナログマイク側の半固定抵抗器は4つとも右いっぱいまで回します。
 ・ヘッドフォンアンプの半固定抵抗器は2つとも左いっぱいに回して音量を完全に絞ります。
 ・ヘッドフォンアンプにイヤフォンを繋いで、両アンプの電源を入れます。
 ・イヤフォンから十分な音量が再生できるまで、ヘッドフォンアンプの左右の半固定抵抗器を右に回します。
 ・この状態でマイクアンプの出力側の半固定抵抗器を少し左に回転させながら、音質の良好な位置で止めます。

Step-3: システム全体のバランス調整
 ・すべての電源をOFFにします。
 ・マイクアンプの出力を、Raspberry Piに接続されたADコンバーターの入力端子に繋ぎます。
 ・ヘッドフォンアンプを以前の方法でRaspberry Piに接続します。
  第16章『ADCで試すアナログマイク①』の「2.テスト環境の配線」に従ってください。
  なお、ADコンバーターの半固定抵抗器は以前のままとします。
 ・HAT21を起動して「フィルターなしモード」(コンソールにApplied : [NON]と表示)にします。
  つまり、フィルターを素通しの状態にします。
 ・ヘッドフォンアンプの半固定抵抗器は2つとも左いっぱいに回して音量を完全に絞ります。
 ・すべての電源を入れます。
 ・ヘッドフォンアンプの半固定抵抗器を少しずつ回して、高音質で再生されることを確認します。
  もし音質に問題があるようなら、ADコンバーターの半固定抵抗器も調整してみましょう。
以上で調整した音量と音質がHAT21のベースになります。


③イコライザーの調整

 小文字のeキーを押すと、イコライザーモードに切り替わって次の内容が表示されます。左右のバンド別感度レベルLv(L)とLv(R)はすべてゼロであることがわかります。イヤフォンを耳につけると何も聞こえません。
 この状態で1~9までの数値キーを押すと、そのキーに対応したバンドの感度の値が1ずつ増加して、左イヤフォンから音声が再生されてきます。大文字キーRを押すと右チャンネルが選択されて、同様にバンド別感度を設定することができます。
 このように9つの周波数帯域別感度を調整して、利用者がもっとも聴き取りやすい状態を探るのがイコライザーの調整です。感度が強すぎる場合は、[Shift]キーを押さえたままで該当する帯域の数字キーを押すと低減できます。
 調整が終われば大文字キーQを押して、不用意な数値キー操作で設定値が変わらないようにします。
 実際に調整をやってみると、なかなか微妙であることが実感できます。イコライザーの設定状態は、小文字キーdを押せば、他のフィルター係数の状態と合わせていつでも確認することができます。またその状態は[w]や[r]キーでファイル'temp.ctrl'に書いたり読み込んだりすることもできます。また、HAT21を終了するとその時点の設定情報がファイルに退避されます。


④制御キーと機能

 使用できるキーの種類が多くなったので、あらためてキーと機能の対応をまとめておきます。
キー    機   能
lローパスフィルター(LPF)を選ぶ
hハイパスフィルター(HPF)を選ぶ
bバンドパスフィルター(BPF)を選ぶ
xフィルターなしを選ぶ
+使用中のフィルター(LPF/HPF/BPFの上限)のカットオフ周波数を上げる
-使用中のフィルター(LPF/HPF/BPFの上限)のカットオフ周波数を下げる
>BPFの下限カットオフ周波数を上げる
<BPFの下限カットオフ周波数を下げる
d現在のフィルター種別とカットオフ周波数等を表示する
c現在のカットオフ周波数を初期値に戻す
r現在のフィルター種別とカットオフ周波数等を退避する
w現在のフィルター種別とカットオフ周波数等を復元する
eイコライザーモードを選ぶ
MミュートをON/OFFする
Nノイズ抑制をON/OFFする
U音量をアップする
D音量をダウンする
Lイコライザーの左チャンネルを選択する
Rイコライザーの右チャンネルを選択する
Qイコライザーのチャンネル選択を無効にする
1~9対応するバンドの感度を1レベルアップする
[Shift]
+ 1~9
対応するバンドの感度を1レベルダウンする
[space]処理終了


5.いくつかの課題と今後の対応

 ここまで聴覚補助ツールに必要なハードを検討・製作し、デジタルフィルターの作成やテストを行いながら、ついにイコライザーも組み込むことができました。これで聴覚補助ツールとしての道具立ては揃ったわけですが、これを完成させるためにはなお大きな課題があります。プロジェクトも終盤に差しかかったので、以降に備えて残す課題を整理しておきす。

①リモートアプリケーションへの移行

 現在のHAT21はパソコンのSSHコンソールに繋がれた状態で動作しています。開発途中のテストやチューニングにはきわめて好都合なのですが、完成した聴覚補助ツールは単独で動かす必要があります。現時点でもっとも簡単な方法は、Raspberry Pi起動時に自動起動させる方法でしょう。
 まず/usr/local/bin/にシェルスクリプトファイルautostart.shを作成して、次の内容を書き込みます。
$ sudo nano /usr/local/bin/autostart.sh

 手元の環境では、実行ファイルhat21が/home/share/testAudio/hatにあるので。まずカレントディレクトリーをその位置に変更して(これをしないとHAT21内での制御情報等のファイル操作が失敗します)、少しだけ待ってHAT21を起動しています。
#!/bin/sh cd /home/share/testAudio/hat wait /home/share/testAudio/hat/hat21

 次に、スクリプトファイルに実行権を与えます。
$ sudo chmod +x autostart.sh

そして、例えばcrontabを使って自動起動を指示します。
$ crontab -e

 下のような画面が開くので(カラー表示が見にくいので背景色と文字色を変更しています)、最終行に、先に作成したシェルスクリプトをフルパスで指定します。
 これで再起動すれば、hat21はそれまでにチューニングされた条件に従って動作を開始します。
$ sudo reboot

 しかし、これは糸が切れた凧状態で音量調整もチューニングも何もできません。再チューニングをするには、以下のように実行中のhat21プロセスを検索して、当該プロセスを強制終了させなければなりません。停止後に、コンソールから手動でhat21を起動してチューニングを行い再起動することになります。

 これではとても簡単に使用することはできません。いちど設定すればほとんど変更がないローパスフィルターやバンドパスフィルターなどは良しとしても、中核になるはずのイコライザーの設定や音量調整などはもっと簡単にできなければなりません。
 これを可能にするには、スマートフォンなどからHAT21に接続して調整できるような仕組みを考える必要があります。たとえば、HAT21に外部通信機能を追加してWebアプリケーションと連係動作させるのがいいかも知れません。いずれにしても、なお新たな仕組みの追加や開発が必要になりそうです。


②適切な電源の検討

 これは当初から想定されているテーマですが、ノイズが少なくて安定した電圧が確保できる電源をどうするかが大きな課題です。現時点では、Raspberry Piにはモバイルバッテリーの利用を考えています。アンプ類など他のユニットには同じバッテリーからDC-DCコンバーターを経由して給電する予定ですが、ノイズの問題が気がかりです。


③小型化とマイクの再検討

 現在の自作マイクアンプとエレクトレットコンデンサーマイクの組み合わせは、ノイズがほとんど無く素直な音質なので満足していますが、ボードの枚数が増えて大きくなってしまいました。もっと小型化とシンプル化をはかるために、MEMSマイクロフォンを再検討したいと考えています。十分なゲインが得られるようであれば、マイクアンプもADコンバーターも不要になるので、大幅に小型化・シンプル化することができます。また、ヘッドフォンアンプも低電圧動作の小型なものがほしいところです。


④その他

 PortAudioのブロックI/O方式を使うことに本質的な問題が無いかどうか、もうしばらく観察しなければなりません。音声データのストリーム出力関数Pa_WriteStream()実行時に、不規則に発生するアンダーフローを完全に防ぐことができません。また、出力ストリームのレイテンシーoutputParameters.suggestedLatencyの設定も、推奨されているdefaultLowOutputLatencyを指定すると、アンダーフローが多発して使いものになりません。現時点では、さまざまな値を試してその結果0.04という値を設定しています。リアルタイムサウンド処理の適用事例が見当たらず、この関係の情報が少ないのは困りものです。
 ただ一方で、Raspberry Pi Zeroがヘビーな音声信号処理にどこまで対応できるかを心配していたのですが、この点はまったく問題ないようです。


 これで実験機はほぼ完成といったところですが、実用化までにはまだ課題が多いことを再認識している次第です。これによって後続のテーマに追加・変更が発生します。さて次回はどうしようかな。Web連携制御には少し予備的な実験が必要になりそうだし、先に電源の検討に着手しようかな・・・・。


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