Mirrativ Tech Blog

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

イヤホン配信を支える音のプログラミング入門

 こんにちわ。shogo4405です。本エントリーは普段UI開発を行なっているクライアントエンジニア向けに、Mirrativのイヤホン配信を支えている音のプログラムの基礎を紹介していきたいといます。

 音のプログラミングの概要を掴んでもらい、より詳しい部分については文献を調べるための参考になれば幸いです。

はじめに

 イヤホン配信の概要になります。Mirrativの配信はゲーム音源をマイクからの集音に頼っています。ユーザーが配信を行う際に、イヤホンを装着するとゲーム音源が配信にのらず視聴側としては物足りない配信になります。

 これを解決する手段として、OS内部で再生中のゲーム音源をキャプチャーした音源。マイクから集音した音源。これらの音源を合成する方法があります。この手段をとることにより、イヤホンしながらでもゲーム音声を視聴者に届けることができるようになります。サービス上では、イヤホン配信と呼称しています。このイヤホン配信を支える音のプログラミングを紹介した記事になります。

用語集

 本エントリーを読み進めていく上で、最低限、抑えておきたい用語を紹介していきます。

PCM

 PCMとは、音声などのアナログ信号をデジタルデータに変換する方式の一つ。無圧縮の音声データ。ライブ配信では、このPCMデータを圧縮して転送し視聴側で伸長し再生を行っています。

サンプリングレート

 アナログ信号からデジタル信号へ変換する処理において、1 秒間に実行する標本化(サンプリング)処理の回数のこと。ミラティブでは、44.1khz を採用しています。

量子化ビット数

 1秒間の音声に与えるデータ容量を表す数値。16bit, 24bit といった表現があります。

ビット数 表現範囲
16bit Int16 -32,768〜32,767
24bit Float -1.0〜0.0〜1.0

インターリーブ

 スレテオ音声のように複数チャンネルがある場合に、不連続にデータを配置すること。

var interleved: [Int16]    = [L, R, L, R, L, R, L, R]  // インターリーブ
var nonInterleved: [Int16] = [L, L, L, L, R, R, R, R]  // ノンインタリーブ

ハイパスフィルター

 ローカットフィルターともいう。特定の周波数より高い周波数はそのまま通し、それより低い周波数は減衰すること。

ローパスフィルター

 ハイカットフィルターともいう。特定の周波数より低い周波数はそのまま通し、それより高い周波数は減衰すること。

システム音声の集音

 冒頭で、OS内部で再生中のゲーム音源をキャプチャーすると言及しました。簡単に紹介すると、iOSでは、ReplayKitを利用する場合には、マイク音源とシステム音源両方の音声が取得できます。

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
    switch sampleBufferType {
    case .video:
        break
    case .audioApp:
        // システム音源を取得できる
        break
    case .audioMic:
         // マイク音源を取得できる
         break
    @unknown default:
        break
    }
}

 Androidは、Android10から提供されているAPIを利用します。マイクでお馴染みのAudioRecordの設定で利用できるようになります。再生キャプチャ  |  Android デベロッパー  |  Android Developers

val audioRecord = AudioRecord.Builder()
    .setAudioPlaybackCaptureConfig(
        AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
            .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
            .addMatchingUsage(AudioAttributes.USAGE_GAME)
            .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
            .build()
         )
         .setAudioFormat(
             AudioFormat.Builder()
                 .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                 .setSampleRate(Consts.SAMPLING_RATE)
                 .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                 .build()
             )
         .setBufferSizeInBytes(sampleCount * 2)
         .build()

音のプログラミング入門

 さて。本エントリーの趣旨の音のプログラミング入門になります。なおPCMデータを扱う場合、1次元の配列として扱います。1024長毎に処理することが多いです。

var soundData1: [Int16] = [0, 10, 20, 30, 40, 50, 60, ...]

音声合成

 マイク音声とアプリ音声の合成につかう基礎的な技術です。音声を合成するには、加算処理を行います。前提条件として、同一のサンプリングレートでなければなりません。なお、符号付き16bitで表現できる範囲は-32,768〜32,767です。オーバーフローが発生しないように気をつけましょう。

var soundData1: [Int16] = [0, 10, 20, 30, 40, 50, 60, ...]
var soundData2: [Int16] = [0, 5, 10, 15, 20, 30, ...]

var result: [Int16] = [0, 0, 0, 0, 0, 0, 0, ...]
for (i in 0..<soundData1.count) {
    var value: Int = Int(soundData[i]) + Int(soundData2[i])
    if (Int16.max < value) {
        value = Int16.max
    } else if (value < Int16.min) {
        value = Int16.min
    }
    result[i] = Int16(value)
}

音量調整

 音量を大きくするには1より大きい数の乗算処理。小さくする場合は1より小さい数の乗算処理を行います。

var soundData1: [Int16] = [0, 10, 20, 30, 40, 50, 60, ...]
var result: [Int16] = [Int16](repeating: 0, count: soundData1.count)
let volume: Float = 0.5
for (i in 0..<soundData1.count) {
    result[i] = soundData1[i] * volume
}

ミュート処理

 音声をミュート状態(無音)にする処理です。一律、0埋めを行います。

var soundData: [Int16] = [0, 10, 20, 30, 40, 50, 60, ...]
for (i in 0..<soundData1.count) {
    soundData[i] = 0
}

リサンプリング

 サンプリングレートの変換につかう処理です。元サンプルレートから大きい値へ変換することをアップサンプルといい、小さい値へ変換することをダウンサンプルといいます。

アップサンプル

 22.05khzでサンプリングされたデータを倍の44.1khzにリサンプリングすることを考えます。後処理として、データを補完する過程で出現する実際には存在しない音域を省きます。元データと同じ22.05khzをローパスするフィルターをかけます。

補間処理のイメージです。各々の配列の1つ1つに中間データを挿入していきます。

f:id:shogo4405:20211202172725p:plain

ダウンサンプル

 44.1khzでサンプリングされたデータを半分の1/2にリサンプリングすることを考えます。前処理として、22.05khzのサンプリングレートで表現できない音域を省きます。変換後の22.05khzをローパスするフィルターをかけます。

間引き処理のイメージです。データを1つ1つ間引いていきます。

f:id:shogo4405:20211202172740p:plain

 簡単のために、リサンプリングは、44.1khz → 22.05khz。 22.05khz → 44.1khzと倍になるようにして説明しました。実際には、48khz → 44.1khzのリサンプリング。16khz → 44.1khzのリサンプリングとなり端数が生じます。

フィルタ処理

 リサンプリングの過程で利用するローパスフィルタの実装です。音声データに対して、特定の周波数を除去するためにあらかじめ設計したデータ(Kernel)を用いて畳み込み演算を行います。

var soundData: [Int16] = [0, 1, 2, 3, 4, 5, 6, ...]
var result: [Int16] = [Int16](repeating: 0, count: soundData1.count)
let kernel: [Int16] = [0, 1, 2, 3, 2, 1, 0] // カーネルは奇数である必要があります
for (n in 0..<soundData.count) {
    for (m in 0..<kernel.count) {
        result[n + m] += soundData[n] * kernel[m]
    }
}

本来なら処理すべき実装ですが、iOSの実装で検証したところ開発者の耳では違いを体験できなかったため省略しています。

まとめ

音のプログラミング入門のまとめです。

  • ある音源とある音源を一緒にしたい
    • PCMデータとPCMデータを加算する
  • 音量を大きくしたい
    • PCMデータに対して1より大きい数を乗算する
  • 音量を小さくしたい
    • PCMデータに対して1より小さい数を乗算する
  • サンプリングレートの変更
    • PCMデータを線形補完する
  • フィルタ処理
    • PCMデータに対してFilterしたいデータを畳み込み演算する

むすびに

 イヤホン配信機能は、このページで紹介した基礎知識で実装できるようになります。しかしながら、ライブ配信は、リアルタイム性が求められるため処理コストがかからないように実際のコード上では多くの工夫をしています。

We are hiring!

 ミラティブでは、一緒にアプリを作ってくれるエンジニアを募集中です!気軽にご連絡ください!

www.mirrativ.co.jp