0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】Bluetoothの300ms遅延を数学的に相殺!スマホ1台でカーステの周波数特性と残音を測定・比較できる音響アプリを作った話

0
Posted at

はじめに

カーオーディオやホームオーディオでDSPやイコライザー(EQ)を調整するとき、こんな経験はないでしょうか?

  • 「ジャズを聴くと低音がボワボワしてブーミーになる」
  • 「なんとなく感覚で低域(80Hz〜160Hz)を下げてみたけれど、ボーカルまで痩せてしまった」
  • 「そもそも今の音って、理想のバランスからどれくらいズレているの?」

音響空間の特性(定在波やドア鉄板の共振、反射)は車や部屋ごとに全く異なります。耳の感覚だけで調整を繰り返していると、基準がないためいわゆる「チューニング沼」にハマってしまいます。

プロは測定用マイクやPCソフト(REWなど)を使って周波数特性を可視化しますが、「車の中にノートPCやオーディオインターフェースを持ち込むのは面倒…スマホ1台で手軽に測れないか?」 と考えたのが、今回開発したAndroid音響測定アプリ 「SoundTune」 です。

本記事では、スマートフォンと車をワイヤレス(Bluetooth)接続した際に立ちはだかる「300msの伝送遅延問題」をどのように数学的・信号処理的に克服したか、そして 「自宅ステレオとカーステの周波数カーブを重ね合わせて比較する仕組み」 の実装について紹介します。


直面した最大の壁:Bluetoothの「300ms伝送遅延」

スマホで音響測定(スイープ音やパルス音を鳴らし、マイクで拾って解析する)を行う際、最大の敵となるのが Bluetooth接続特有の巨大な遅延(レイテンシ) です。

車内オーディオにBluetoothで繋いで音を鳴らすと、以下のような経路をたどります。

[Androidアプリ]
      │
      ▼ (Android OS オーディオフレームワーク: 約30ms)
[Bluetooth A2DP エンコーダー (SBC/AAC)]
      │ (圧縮 & パケット化バッファ: 約80ms)
      ▼ (無線電波伝送)
[カーナビ / ディスプレイオーディオ 受信部]
      │ (★音飛び防止の受信ジッターバッファ: 約150ms)
      ▼ (デコード & ナビ内蔵DSP処理: 約40ms)
[アンプ & スピーカー]
      │ (空気中の伝播時間: わずか約3〜5ms)
      ▼
[スマートフォンのマイク]

空気中を音が進む時間は数ミリ秒しかないのに、Bluetoothの送受信バッファだけで合計約300msもの遅延 が発生します。

さらに厄介なことに、Bluetoothのパケット再送やバッファ状態によってこの遅延時間は毎回数十ミリ秒単位で揺らぎます(ジッター)。そのため、単純に「音を出した時間」と「マイクに音が届いた時間」を測る従来のアプローチでは、車内の音響特性ではなく「Bluetoothの気まぐれな遅延」を測ることになってしまい、全く使い物になりませんでした。


解決策1:残音・共振(Decay)を測る「パルス持続時間・長さ比較方式」

カーオーディオで最も多い「低音がブーミーで締まりがない」という問題は、音圧(dB)だけでなく**「音が止まった後も、車内やドア鉄板が共振して音が残り続けている(残音・余韻)」**ことが原因です。

しかし、Bluetooth遅延がある環境で、どうやって「実際の残音時間」だけを抜き出せばよいでしょうか?

発想の転換:「音の長さ(幅)」を測る

ここで採用したのが、「きっかり500msの音を鳴らし、マイクで拾った実際の受音時間を引き算する」 というパルス持続時間比較方式です。

【発振音 (アプリ)】:   |←────── 500 ms ──────→|
                      ┌──────────────────────┐
                      │                      │
  ────────────────────┘                      └─────────────────────────────────────────
                      ▲ 発振開始               ▲ 発振終了 (500ms後)

                             ↓ Bluetooth伝送遅延 (~300ms)

【受音音 (マイク)】:                            |←─────── 580 ms ────────→|
                                             ┌───────────────────────────┐
                                             │                           │\ (残音の尾引き)
  ───────────────────────────────────────────┘                           └──\─────────
                                             ▲ マイク受音開始 (ON)        ▲ マイク受音終了 (OFF)
                                             (t = 300ms)                  (t = 880ms)

Bluetooth遅延が300msあっても、音の「始まり」も「終わり」も等しく300ms遅れて届きます。
したがって、受音開始点(ON)から受音終了点(OFF)までの実測時間 $T_{\text{received}}$ を測れば、Bluetooth遅延は完全に引き算で相殺されます!

$$\text{残音・尾引き時間 (Tail)} = T_{\text{received}} - 500\text{ ms}$$

ボリュームによるバラつきを抑える「-6dB対称エッジ判定」

もう一つの課題は「再生ボリュームによる判定誤差」でした。「特定の音量以下になったら終了」という絶対値の閾値判定を行うと、大音量のときは長く、小音量のときは短く判定されてしまいます。

そこで、受音波形全体のピーク(定常状態の音圧)を自動検出し、立ち上がり(ON)も立ち下がり(OFF)も、全く同一の相対閾値「ピーク比 -6dB(振幅50%点)」で検出 するアルゴリズムを実装しました。

// AcousticUtils.kt より抜粋
// ピーク音圧の50%(-6dB)を基準エッジとする
val halfPowerThreshold = peakLevel * 0.50

// 1. 立ち上がり点(ON)の検出
var onIndex = -1
for (i in 0 until rmsLevels.size) {
    if (rmsLevels[i] >= halfPowerThreshold) {
        onIndex = i
        break
    }
}

// 2. 立ち下がり点(OFF)の検出(ピーク以降)
var offIndex = -1
for (i in maxIndex until rmsLevels.size) {
    if (rmsLevels[i] < halfPowerThreshold) {
        offIndex = i
        break
    }
}

val onTimeMs = timePoints[onIndex]
val offTimeMs = timePoints[offIndex]
val receivedDurationMs = max(0.0, offTimeMs - onTimeMs)
// Bluetooth遅延が綺麗に消え、車内の残音・共振(尾引き)のみが算出される!
val tailMs = max(0.0, receivedDurationMs - emittedDurationMs)

これにより、ボリュームの大小やBluetoothの遅延に左右されず、「500msの音を出したとき、車内で何ms音が伸びて響いているか(+XX ms)」 をミリ秒単位で安定して評価できるようになりました。


解決策2:スイープ周波数特性(f特)測定とリードイン・チャープ同期

もう一つのコア機能が、20Hz〜20kHzの全帯域を一気に測定する 「対数スイープ周波数特性(Log-Sine Sweep)」 です。

ここでも「Bluetoothの300ms遅延」が問題になります。マイク側で「今鳴っている音が何Hzなのか」を時間軸と正確に一致させないと、周波数と音圧の対応関係がズレてしまいます。

1.5kHz リードイン・チャープによる自動タイムアライン

スイープ音(20Hz〜20kHz、4秒間)を発振する直前に、「1.5kHzの短い同期チャープ信号(80ms)」 を発振させます。

[再生信号]: [1.5kHzチャープ (80ms)] ── (無音ギャップ 150ms) ──> [20Hz〜20kHz ログスイープ (4秒)]

マイク側は常にバッファリングを行い、Goertzelアルゴリズムを使って1.5kHzのチャープ信号の立ち上がりエッジを瞬時に検知します。
チャープが見つかれば、「チャープ終了から150ms後がスイープの20Hz開始点」 と特定できるため、Bluetoothの遅延が何百ミリ秒あっても、ミリ秒単位でピタリとスイープ波形に時間同期させることができます。

小さな音量でも確実に検知する「動的ノイズフロアトラッキング」

車内や自宅で大音量を出すのは近所迷惑になります。静かな控えめな音量でもしっかりスイープを検知できるよう、以下の工夫を施しました。

  1. 環境ノイズフロアの動的測定: 音が届く前の最初の150msで周囲のノイズ音圧を計測。
  2. 相対SN比判定: 「固定の大きな音圧」ではなく、「周囲のノイズフロアに対して1.8倍以上のエネルギーがあれば検知」 する相対判定を採用。
  3. Goertzelアルゴリズム: 特定周波数(1.5kHz)の成分のみをピンポイントで高感度抽出。

これにより、会話程度の控えめな音量(-35 dBFS前後)でも確実にスイープ測定を完遂できるようになりました。


自宅ステレオとカーステを重ねて比較できるグラフ描画

測定した周波数特性は、オーディオ規格に準拠した 対数グラフ(20Hz〜20kHz、ISO 1/3オクターブ・29バンド) として描画されます。

さらに、このアプリの一番の推し機能が 「名前をつけて保存 & 複数グラフの重ね合わせ表示(Overlay View)」 です。

【画面イメージ】
┌──────────────────────────────────────────────┐
│ 周波数特性 (20Hz 〜 20kHz, dBFS)               │
│  0 dB ─────────────────────────────────────  │
│       \     /\           /\ (車: 63Hz共振) │
│-20 dB ──\──/───\────────/───\───────────  │
│           \/     \_____/      \ (車: 3kHz谷)│
│-40 dB ─────────────────────────────────────  │
│       20  50  100  200  500  1k  2k  5k  10k │
└──────────────────────────────────────────────┘
 [✔] 🔵 自宅ステレオ (リファレンス)
 [✔] 🟠 カーステ_EQ前
 [ ] 🟢 カーステ_EQ後

チューニングの実践フロー

  1. 自宅のリファレンス環境を測定:
    • 自分が最も気に入っている自宅のオーディオ前でスイープを鳴らし、「自宅ステレオ」 として保存。
  2. 車内で測定:
    • 運転席の耳の高さにスマホを置き、カーステでスイープを鳴らして 「車_調整前」 として保存。
  3. 2つのグラフを重ね合わせる:
    • チェックボックスをONにして重ね合わせると、一目で違いが浮き彫りになります。
    • 「自宅に比べて、車の63Hz〜80Hzが+8dBも盛り上がっている(これがブーミーの原因か!)」
    • 「3kHz付近のボーカル帯域がガラスの反射で-6dB凹んでいる」
  4. DSP/EQで補正して再測定:
    • 盛り上がっている帯域をカーDSPのパラメトリックEQでピンポイントにカット。
    • 再測定して 「車_調整後」 として重ねると、自宅のフラットなカーブにどれだけ近づいたかが視覚的に一発で分かります。

技術スタック & アーキテクチャ

  • 言語: Kotlin
  • 最小 / ターゲットSDK: Android 8.0 (minSdk 23 / targetSdk 37)
  • ビルドシステム: Gradle 9.6 + AGP 9.4
  • UI: View Binding + ViewPager2 + TabLayout + Material Design 3 + Custom View
  • オーディオ入出力:
    • 再生: AudioTrack (PCM 16-bit stereo 44.1kHz, 低レイテンシ静的/ストリーミング)
    • 録音: AudioRecord (MediaRecorder.AudioSource.UNPROCESSED または VOICE_RECOGNITION によるOSエフェクトバイパス生PCM取得)
  • 信号処理:
    • Goertzel Algorithm(特定周波数の高感度単一ビンDFT)
    • スライディングRMSウィンドウ & 動的ノイズフロア推定
    • ログ・スイープ信号合成 & リードイン・チャープ同期
    • 1/3オクターブ(29中心周波数)平滑化バンドパス

まとめ

音響測定を専用機材なしで「スマホ1台+Bluetooth接続」でやろうとすると、OSや無線伝送の遅延・ジッターという大きな壁にぶつかります。

しかし、

  • 「音の長さを測ることで遅延を相殺する(残音測定)」
  • 「リードイン・チャープで音響タイムラインを同期する(スイープ測定)」

という工夫を施すことで、日常使っているスマホをそのまま高精度な音響アナライザーとして活用できるようになりました。

「自宅の良い音を車でも再現したい」「DSPの調整に明確な基準が欲しい」という方は、ぜひ試してみてください!


ソースコード (GitHub)

アプリの全ソースコードはGitHubで公開しています。スターやフィードバックをいただけると励みになります!

👉 GitHub: amekusa03/AndroidSoundTune

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?