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行目以降の関数プロトタイプ部分は省略します。
8
9
10
11
12
13
14
15
16
17
/*
    処理モード(キー文字と一致させる)
*/
#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に変わります。

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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'を表示させています。

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/*
    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行目以降に以下のコードを追加しています。今回の機能追加のうち、機能選択やイコライザーのバンド別感度調整などキー入力を伴う部分以外の大半がここに記述されています。

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
/*
    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()関数で同期をとっています。
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#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'/' ')


②主処理:初期化処理部

 ここでイコライザーの初期化処理と感度の設定をしています。
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/* == 準備処理 == */
/* (ログファイルの準備) */
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);


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

 イコライザーモードの時はここでイコライザー信号処理関数を実行させています。
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/* (フィルター信号処理) */
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;
}


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

 イコライザーモードを指示されると左右の感度を表示して、処理モードをイコライザーに設定します。
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/* (キー判定とフィルター係数取得処理等) */
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行目以降のコードを追加しています。追加機能のキー判定と関数呼び出しはすべてここで行っています。処理内容はコメントのとおりです。
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
        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;
    }
}


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

・カットオフ周波数等をファイルから設定する。
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
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;
}
・カットオフ周波数等をファイルに書き込む。
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
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を取得する。
654
655
656
657
658
659
660
661
662
663
664
665
666
667
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;
}
・フィルター種別からフィルター名を取得する。
674
675
676
677
678
679
680
681
682
683
684
685
686
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;
}
・帯域別音量を表示する。
708
709
710
711
712
713
714
715
716
717
718
719
720
721
/*
    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");
}
・ログ情報をファイルに書き出す。
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
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-2025 Marchan, All rights reserved.