はじめに
iPhoneのマイクだけでハウリングしやすい帯域を出すアプリを作りました。
小規模なライブハウスで、週1回ほどPAをやらせていただいています。
初心者に近い私が、こういう機能があったらいいなと思ったのが制作のきっかけです。
開発中、ハウリング検出の論文に「1kHz未満の成分は無視してよい」とあるのを見つけ、そのまま実装しました。
これが間違いでした。
この記事は、その前提が自分の用途とは違い、設計を作り直した記録です。
そもそもハウリングをどう見つけるか
録った音をFFTにかけ、同じ周波数のピークが決めた秒数のあいだ残り続けるかで判定します。Accelerateの vDSP をそのまま使いました。
let log2n = vDSP_Length(log2(Float(nf)))
guard let s = vDSP_create_fftsetup(log2n, FFTRadix(kFFTRadix2)) else { return nil }
var window = [Float](repeating: 0, count: nf)
vDSP_hann_window(&window, vDSP_Length(nf), Int32(vDSP_HANN_DENORM))
vDSP_fft_zrip(fftSetup, &split, 1, log2n, FFTDirection(FFT_FORWARD))
問題はこの後です。どの周波数より下を「検出しない」ことにするか。
最初にやったこと:論文どおり1kHzで切る
参考にしたのは Alkaher & Cohen (2022) のハウリング検出の論文です。そこに disregard frequency components below 1 kHz とありました。
実装します。
var howlMinHz = 1000.0 // これより下は見ない
しばらくこれで動かしていました。
その前提が合わない、と言われる
現場の人に使ってもらったところ、「低い帯域のハウリングを拾えていない」と言われました。
論文をもう一度読み直しました。
- 評価しているのは 車の中と書斎のシミュレーション
- 評価範囲は
between 2000 and 6750 Hz - 本文に
musicという語が1回も出てこない(29ページのPDFを検索した)
ライブ会場の話ではありませんでした。
一方、機材メーカーの公開資料には could promote feedback in the low- or low-midrange frequencies とあり、疑う周波数として400Hzと630Hzが挙がっています。
1kHz未満にハウリングが起きない、というのは間違いです。
ハウリングの音は、周波数で変わります。TOAの解説にこうあります。
増幅された音の周波数が高いと「キーン」という高い音のハウリングが、周波数が低いと「ボーン」という低い音のハウリングが起こります。
(TOA株式会社「『ハウリング』の発生原因と抑制方法」)
1kHzで切るということは、 低音のハウリングを検出しないと決めてしまう ことです。
では下限をどこに下げるか
下限を 150 / 200 / 250 / 315 / 400 / 630 / 1000 Hz の7通りにして、手元の実データで回しました。
| 下限 | 声を誤ってハウリングと判定した割合 |
|---|---|
| 150Hz | 63% |
| 400Hz | 6% |
| 1000Hz | 0% |
どこに置いても、声の基音か倍音に印が付きます。
理由ははっきりしています。声が出ている間は、何を見ても声と区別できません。ハウリングも声も、同じように「そこに強いピークが立っている」ので、下限をいくら動かしても分けられません。
誤りが0になるのは1000Hzだけですが、そのときは低い帯域のハウリングも1件も見つかりません。どこで切るか、という問題設定そのものが間違っていました。
変えたこと:周波数ではなく、時間で分ける
声が出ている間と、声を止めた後で、見る範囲を切り替えることにします。
// 声が出ている間は、この下を測らない(低い帯域は声を止めた後に測る)
var howlMinHzVoice = 1000.0
// 声を出さない測定の下限。ハウリングは全帯域で起こる
var howlMinHzSilence = 150.0
// 「結果を見る」を押したあと、何秒分「無音として」録音を続けるか
var postVoiceSilenceSec = 6.0
やっていることは単純です。アプリの「結果を見る」を押した後も、6秒だけ録音を続け、そこを150Hzから調べ直します。
声を止めても、部屋の残響はしばらく残ります。
残響は減衰しますが、減衰しきらずに大きくなっていく音があれば、それがハウリングです。
一周して戻った音が元より大きい(ループ利得が1を超える)と、
音は周回ごとに育ち発振が止まらなくなります。
(ハウリング対策の決定版)
声を出している間は、声とハウリングが混ざっています。声を止めると、回り続けている音だけが残ります。その音は回るたびに大きくなるので、はっきり分かります。
時間とともに大きくなるかを見る指標がある
van Waterschoot の講演資料に、IMSD という指標の定義が出てきます。
howling exhibits an exponential amplitude buildup over time
(Acoustic feedback control in sound reinforcement systems)
ハウリングは、時間とともに大きくなり続ける。声はそうなりません。
同じ資料に、検出基準ごとの誤警報率も載っています(検出率95%に揃えたとき)。
| 基準 | 単独での誤警報率 |
|---|---|
| PTPR(山の鋭さ) | 70% |
| PNPR(周りの雑音との差) | 33% |
| IPMP(同じピークが続いた回数) | 54% |
| IMSD(時間とともに大きくなるか) | 40% |
| PNPR & IMSD | 5% |
| PHPR & PNPR & IMSD | 3% |
単独ではどれも当てになりません。 組み合わせると一気に下がります。 とくに時間とともに大きくなるかを見る IMSD が入った組み合わせが強い。
このアプリは PAPR・PTPR・PNPR・IPMP の4つを組み合わせています。そこに「声を止めた後だけ低い帯域を見る」という時間の切り分けを足した形です。
何秒必要かも実測する
声を止めた後、何秒分録音を続けるかを0.5秒刻みで振りました。
| 声を止めた後に録音を続ける長さ | 200〜800Hzのハウリングを見つけた本数 |
|---|---|
| 2.5秒 | 0 / 15本 |
| 5.0秒 | 0 / 15本 |
| 5.5秒 | 15 / 15本 |
| 6.0秒 | 15 / 15本 |
5.5秒で急に全部見つかります。ハウリングが立ち上がってピークとして認識できるまでに、それだけかかるということです。0.5秒の余裕をとって6.0秒にしました。
どの長さでも、声を誤ってハウリングと判定したのは 0 回でした。声が出ている間にアプリが判定したのは 140 回、そのうち 0 回です。
おわりに
論文に書いてある前提をそのまま実装し、低音のハウリングを丸ごと見落としていました。読み直すと、実験の条件がこちらの用途とまったく違っていました。
論文の数字を使うときは、その論文が何を測ったのかまで読む。 当たり前のことですが、実装を急いでいると飛ばしてしまいます。
この記事で当たったもの
-
Alkaher, Cohen「Temporal Howling Detector for Speech Reinforcement Systems」(Acoustics 2022, 4, 967–995) …
disregard frequency components below 1 kHzの出典。題名のとおり話し声の拡声が対象 - van Waterschoot「Acoustic feedback control in sound reinforcement systems」(KU Leuven / TU Delft) … IMSD の定義と、検出基準ごとの誤警報率
- TOA株式会社「『ハウリング』の発生原因と抑制方法」 … 周波数が高いと「キーン」、低いと「ボーン」
- chenwj1989/python_howling_suppression … 同じ指標をPythonで実装したものが公開されています。IPMPを「5フレーム中3回」で見る形が、この記事の実装と同じでした
数字はすべて手元の実装と実測データに当たって書いています。
チェックワンツー(App Store・無料) … アナライザーは制限なく使えます。