こんにちは、エンジニアのちぎら(@_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
の配列を取得するといったことができます。
検証のための仕組み
音声の録音と可視化をするためのライブラリとサンプルアプリケーションを作成しました。
BroadcastWavCapture
は、ReplayKit を介して取得した音声データを指定されたパスに WAV ファイルとして録音するライブラリです。アプリケーションからは録音された音声ファイルが閲覧できるようになっており、音声の再生や波形データのプレビューもできるようにしました。
今回はせっかくなので 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)
この録音機能と波形のプレビュー機能を用いて、以下のパターンで音声の挙動を詳しく調べてみました。
- iOS 17 でイヤホンを使用しない場合
- iOS 18 でイヤホンを使用しない場合
- iOS 17 で AirPods Pro2 を使用した場合
- iOS 18 で AirPods Pro2 を使用した場合
検証結果
それぞれの検証は、適度にガヤガヤしているオフィスで実施しています。また、録音中は音声を発するアプリケーション(YouTube)が音声を再生しています。つまり、audioMic
と audioApp
の両方ともが集音されることを期待しています。
1. iOS 17 でイヤホンを使用しない場合
audioMic
(iOS 17 イヤホンなし)audioApp
(iOS 17 イヤホンなし)
audioMic
と audioApp
の両方ともで音声が録音されていることがわかります。audioMic
の冒頭にある比較的大きな振幅は、端末が録音を開始した際に発する「ポン」という音です。冒頭以外では振幅が小さくなっていますが、振幅が小さい範囲では単に小さな雑音を集音しています。
2. iOS 18 でイヤホンを使用しない場合
audioMic
(iOS18 イヤホンなし)audioApp
(iOS18 イヤホンなし)
こちらも audioMic
と audioApp
の両方ともで音声が録音されていることがわかります。
3. iOS 17 で AirPods Pro2 を使用した場合
audioMic
(iOS 17 + AirPods Pro2)audioApp
(iOS 17 + AirPods Pro2)
こちらも audioMic
と audioApp
の両方ともで音声が録音されていることがわかります。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)にて随時発信していますので、ぜひフォローいただけると嬉しいです。
インターンも募集中です!