Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

iOS 18 x AirPods Pro2 で配信の音声が聞こえづらいことがある問題の調査

こんにちは、エンジニアのちぎら(@_naru_jpn)です。ゲーム配信アプリであるミラティブではイヤホンを使用しながら配信をすることができますが、動作の安定のために 有線のイヤホンの使用を推奨しています

昨年 iOS 18 がリリースされてから、稀に AirPods Pro2 で配信の音声が途切れるといったようなお問い合わせがユーザーさんから届くようになりました。サービスとしては無線のイヤホンは推奨していないものの、技術的にどのような事象が発生しているのかは興味がありました。今回は ReplayKit を介して取得した音声の波形を手軽に記録・可視化できるようにした仕組みの話と、その調査結果を書こうと思います。

前提

iOS では、ReplayKit というフレームワークを介して、端末から映像データと音声データを取得することができます。音声データには、アプリケーションが発する音(audioApp)、マイクからの集音(audioMic)の2種類があります。

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
  switch sampleBufferType {
  case RPSampleBufferType.video: // 映像データ
    break
  case RPSampleBufferType.audioApp: // 音声データ(アプリケーションが発する音)
    break
  case RPSampleBufferType.audioMic: // 音声データ(マイクからの集音)
    break
  @unknown default:
    fatalError("Unknown type of sample buffer")
  }
}

CMSampleBuffer は、様々な形式のデータが格納できる構造になっています。例えば、音声データ(audioApp もしくは audioMic)の場合には CMSampleBuffer から AudioBuffer を取り出し、そこから音声の波形データを表す Int16 の配列を取得するといったことができます。

検証のための仕組み

音声の録音と可視化をするためのライブラリとサンプルアプリケーションを作成しました。

github.com

BroadcastWavCapture は、ReplayKit を介して取得した音声データを指定されたパスに WAV ファイルとして録音するライブラリです。アプリケーションからは録音された音声ファイルが閲覧できるようになっており、音声の再生や波形データのプレビューもできるようにしました。

ライブラリが音声を App Groups 上に録音し、アプリケーションが参照する

アプリケーションに含まれている機能の一部(閲覧、プレビュー)

今回はせっかくなので WAV ファイルのバイナリフォーマットを調べて、バイナリデータをプログラムから直接編集する形でファイルを作成してみました。とてもシンプルな形式です。

extension WAV {
  public struct File {
    public struct RIFF {
      /// 1-4
      public let mark = "RIFF"
      /// 5-8
      public let fileSize: UInt32
      /// 9-12
      public let fileType = "WAVE"
    }
    public struct FMT {
      /// 13-16
      public let mark: String = "fmt "
      /// 17-20
      public let size: UInt32 = 16
      /// 21-22
      public let format: UInt16 = 1
      /// 23-24
      public let numChannels: UInt16
      /// 25-28
      public let sampleRate: UInt32
      /// 29-32
      public let byteRate: UInt32
      /// 33-34
      public let blockAlign: UInt16
      /// 35-36
      public let bitsPerSample: UInt16
    }
    public struct DATA {
      /// 37-40
      public let mark: String = "data"
      /// 41-44
      public let dataSize: UInt32
      /* ここ以降にデータが格納される */
    }

    public let riff: RIFF
    public let fmt: FMT
    public let data: DATA
  }
}

また、音声の波形部分は SwiftUI の ScrollView 上に Path を使用して描画しているので、大量の描画が必要となる長時間の録音データの表示には向いていません(30秒間くらいの音声までならスムーズに表示できます)。

Path { path in
  let pointSpacing = lengthOfSecond / CGFloat(sampleRate)
  let pointStride = Int(0.25 / pointSpacing)

  path.move(to: CGPoint(x: 0, y: middle))
  // pointStride で間引いた音声のサンプルごとに線を引いて描画している
  for index in stride(from: 0, to: samples.count, by: pointStride) {
    let x = CGFloat(index) * pointSpacing
    let amplitude = CGFloat(samples[index]) * (height / 2)
    path.addLine(to: CGPoint(x: x, y: middle - amplitude))
  }
}
.stroke(color, lineWidth: 0.5)

この録音機能と波形のプレビュー機能を用いて、以下のパターンで音声の挙動を詳しく調べてみました。

  1. iOS 17 でイヤホンを使用しない場合
  2. iOS 18 でイヤホンを使用しない場合
  3. iOS 17 で AirPods Pro2 を使用した場合
  4. iOS 18 で AirPods Pro2 を使用した場合

検証結果

それぞれの検証は、適度にガヤガヤしているオフィスで実施しています。また、録音中は音声を発するアプリケーション(YouTube)が音声を再生しています。つまり、audioMicaudioApp の両方ともが集音されることを期待しています。

1. iOS 17 でイヤホンを使用しない場合

audioMic(iOS 17 イヤホンなし)
audioApp(iOS 17 イヤホンなし)

audioMicaudioApp の両方ともで音声が録音されていることがわかります。audioMic の冒頭にある比較的大きな振幅は、端末が録音を開始した際に発する「ポン」という音です。冒頭以外では振幅が小さくなっていますが、振幅が小さい範囲では単に小さな雑音を集音しています。

2. iOS 18 でイヤホンを使用しない場合

audioMic(iOS18 イヤホンなし)
audioApp(iOS18 イヤホンなし)

こちらも audioMicaudioApp の両方ともで音声が録音されていることがわかります。

3. iOS 17 で AirPods Pro2 を使用した場合

audioMic(iOS 17 + AirPods Pro2)
audioApp(iOS 17 + AirPods Pro2)

こちらも audioMicaudioApp の両方ともで音声が録音されていることがわかります。audioApp では冒頭に音声がありませんが、イヤホンを使用して録音を開始した場合、audioApp の音声が届きはじめるまでにタイムラグが生じるようです。

4. iOS 18 で AirPods Pro2 を使用した場合

audioMic(iOS 18 + AirPods Pro2)
audioApp(iOS 18 + AirPods Pro2)

audioApp は「iOS 17 で AirPods Pro2 を使用した場合」と同様の形状をしていますが、audioMic は明らかに今までの録音結果と異なります。

audioMic の平面になっているように見える範囲では、実際に音声データの振幅がゼロ(無音)になっています。音声の内容を確認すると、おそらく ノイズキャンセリングが適用されているような状態になっているのだと思いますが、雑音がキャンセルされて無音になっています。また、一時的に振幅がゼロでない区間がありますが、ここでは人間の声が集音されていました。ただし、声が大きくなかったからか、声が潰れたような感じで録音されていました。

この音声データは ReplayKit に流れてくるデータを加工せずにそのまま録音しています。録音する素材がそのような状態になっているため、この声の録音のしづらさはプログラム側で改善することができません。リスニングモードの ON/OFF など試してみましたが、取得できるデータの傾向に変化は見られませんでした。

まとめ

iOS 18 と AirPods Pro2 の組み合わせで音声が聞こえづらい状況があることはなんとなく把握はしていましたが、音声の波形を確認することでより詳しく状況がわかりました。

他の機器の組み合わせで同様の事象が再現するのかどうか、検証はできていません。また、目についた設定は変更して試してみましたが挙動は変わらず、端末の設定によってこの事象が改善するかどうかもわかっていません。

謎が多い ReplayKit の挙動のひとつですが、設定で状況が変わるなどお気付きの点をお持ちの方は、どこかでつぶやくなどしていただけますと幸いです!

We are hiring!

ミラティブでは一緒に開発してくれるエンジニアを領域問わず募集しています! 少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、お気軽にご連絡ください。 また、ミラティブの技術関連の情報は公式Xアカウント(@mirrativ_tech)にて随時発信していますので、ぜひフォローいただけると嬉しいです。

hrmos.co

mirrativ.notion.site

インターンも募集中です!

hrmos.co