ChromeでWebRTCステレオ配信

こんにちは、テリーです。GoToEat使ってますか? 昼も夜もオトクに外食できるのはとてもありがたいですね。飲食店の方々は今年はずっと大変だったと思いますが、今回のキャンペーンを機に、キャッシュレス決済やオンライン予約など、DXを進めて成果を上げていただけるのを期待しています。

さて、今回はテレカンでステレオマイクを使うケースを取り上げたいと思います。テレワークの時代には1人1部屋で独立しているので、ノートパソコンのマイクやヘッドセットのマイクを使って音声収録している人がほとんどだと思いますが、会社に出社している方は、小会議室に2〜3人集まって、テレカンするケースもあるでしょう。もしくはテレカン中に高品質ビデオを共有配信し、臨場感のある音を確認するというケースもあるでしょう。

いまどきマイクはステレオになってるでしょ?と思っている人も多いと思いますが、ステレオになっているのはスピーカーだけです。実はほとんどのパソコンのマイクはモノラルマイクです。1人の人間の声を収録するという目的においては、音源である口が1つであればマイクも1つでよいのです。

それならばと、専用のステレオマイクを購入し、接続して、ステレオ収録したものを相手に送り届ければ、ステレオ再生されるのを期待しますが、なんと3大主要ブラウザの1つ、Chromeはそうなっていません。収録はステレオでも、テレカンで配信しようとした瞬間にモノラルに合成されてしまうのです。これは長らく指摘されている不具合に近い挙動ですが、開発の優先度が高くないようです。そこで、Chromeでステレオ音声の配信・視聴をするための方法を解説します。

ステレオ音源の準備

まず、ステレオマイクの代わりに、動画ファイルを音声入力として使用します。
下記の再生ボタンを押すと、パソコンのスピーカーから左右の区別がはっきりわかる音が再生されます。右のスピーカー・イヤホンからは「これは右です」、左からは「こっちは左」と聞こえるでしょうか? イヤホンの方が確認しやすいです。聞き終えたら右下のRerunボタンを押して停止してください。

See the Pen
1.stereo_source
by terry (@terry_a)
on CodePen.

配信するとモノラルになることの確認

次に、ステレオ配信しようとしたものが、本当にモノラル配信に変わってしまうことを確認してみましょう。
Chromeで本ページを開き、下記の「▶Send▶」ボタンを押すと、WebRTCで接続し、送受信したものを再生します。注意深く聞いてみると、左右の区別がなく、どちらのスピーカー・イヤホンからも「これは右です」「こっちは左」が混ざった音が聞こえるでしょう。スピーカーで聞いている人は中央から音が出ているように聞こえるでしょう。聞き終えたら右下のRerunボタンを押して停止してください。
他のブラウザではどうでしょうか? FireFoxで本ページを開いてみてください。左右の音声がはっきり区別できるでしょう。Safariではどうでしょうか? SafariはChromeと同じようにモノラルにミックスされています。

See the Pen
2.mono_webrtc
by terry (@terry_a)
on CodePen.

ステレオ配信概要

問題点を認識できたところで、本題に入ります。
配信先のブラウザでステレオ再生されること、つまり左のスピーカーから左チャンネルが再生され、右のスピーカーから右チャンネルが再生されることがゴールです。

下記のフローで処理を行います。

  1. ステレオ音声を2つのモノラル音声に分離
  2. WebRTCを2つ接続し、それぞれにモノラル音声を送信
  3. 受信側で2つのモノラル音声をステレオにミックス

図にするとこのようになります。ステレオの分離と結合という知識が重要になります。上記フローの1,3の順で解説し、最後に全体を通した解説をします。

ステレオ音声を2つのモノラル音声に分離

ステレオ音声をチャンネルごとに分離するには、WebAudio APIのcreateChannelSplitterを使用します。
下記のサンプルでは、どちらか一方の音声だけがスピーカーからモノラルで再生されます。「left」ボタンを押すと左チャンネルの音声(「こっちは左」)が再生され、「right」ボタンを押すと右チャンネルの音声(「これは右です」)が再生されることが確認できます。聞き終えたら右下のRerunボタンを押して停止してください。

See the Pen
3.channel_split
by terry (@terry_a)
on CodePen.

JavaScriptの主要部分を解説します。

const splitter = audioCtx.createChannelSplitter(2); マルチチャンネルオーディオをモノラルに分離するNodeを新規作成。チャンネル数は最大2つ
source.disconnect(); ステレオ音声の過去のNodeを切断
source.connect(splitter); ステレオ音声をChannelSplitter Nodeに接続
splitter.connect(audioCtx.destination, channel); ChannelSplitterの出力を標準スピーカーに接続。出力は第2引数(0または1)のチャンネルのみ。他のチャンネルは破棄

2つのモノラル音声をステレオにミックス

2つのモノラル音声データを、左右のステレオ音声に結合するには、WebAudio APIのcreateChannelMergerを使用します。
下記のサンプルでは、単純に2つの音を再生した場合と、ステレオ音声としてマージしてから再生した場合の比較ができます。
▶再生ボタンを押すと左右両方のスピーカーから同じ音が再生されます。「stereo」ボタンを押すと、1つ目の音が左のスピーカーから、2つ目の音が右から再生されます。
聞き終えたら右下のRerunボタンを押して停止してください。

See the Pen
4.channel_merge
by terry (@terry_a)
on CodePen.

JavaScriptの主要部分を解説します。

const merger = audioCtx.createChannelMerger(2); 複数のモノラル入力を、マルチチャンネルオーディオに結合するNodeを新規作成。チャンネル数は2
sourceL.disconnect();
sourceR.disconnect();
過去のNodeを切断
sourceL.connect(merger, 0, 0); 1つ目のモノラル音声をChannelMerger Nodeに接続し、0番目(左)のチャンネルとして出力
sourceR.connect(merger, 0, 1); 2つ目のモノラル音声をChannelMerger Nodeに接続し、1番目(右)のチャンネルとして出力
merger.connect(audioCtx.destination); ChannelMergerのステレオ出力を標準スピーカーに接続

WebRTCを2つ接続

最後にこれまでの知識を1つにまとめた、上記構成図通りのプログラムがこちらです。
元の音源を2つのモノラル音声に分離、モノラル音声ごとにWebRTCを接続し送信、受信側で2つのモノラル音声をステレオにミックスして再生します。
下記の「▶Send▶」ボタンを押すと、WebRTCで接続し、送受信したものを再生します。注意深く聞いてみると、左のスピーカーから左チャンネルが再生され、右のスピーカーから右チャンネルが再生されていることがわかります。聞き終えたら右下のRerunボタンを押して停止してください。

See the Pen
5.stereo_multistream
by terry (@terry_a)
on CodePen.

JavaScriptの主要部分を解説します。

行数 コード 処理内容
1-3 async function sendrecv(){
   const streams = await getSplitStream();
   for(let stream of streams) {
sendrecv関数
Sendボタンがクリックされたときに呼ばれる
ステレオ音源を複数のモノラル音声に分割
分割した音声ごとに次の処理を行う
4-7 const pc = new RTCPeerConnection();
stream.getTracks().forEach(track => pc.addTrack(track, stream))
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
WebRTCで接続
モノラル音声をWebRTCで配信
8 await recv(pc); サンプルのため、簡易的なWebRTC受信処理呼び出し
行数 コード 処理内容
11 async function getSplitStream(){ getSplitStream関数
12 const element = document.getElementById("sendaudio"); audioタグ要素を取得
13 const audioCtx = new(window.AudioContext || window.webkitAudioContext); AudioContextのインスタンスを生成
14 const source = audioCtx.createMediaElementSource(element); audioタグ要素を入力とするソースNodeを生成
15 const destinationL = audioCtx.createMediaStreamDestination(); MediaStream出力Nodeを生成(左音声用)
16 const destinationR = audioCtx.createMediaStreamDestination(); MediaStream出力Nodeを生成(右音声用)
17-22 const splitter = audioCtx.createChannelSplitter(2);
source.connect(splitter);
splitter.connect(destinationL, 0);
splitter.connect(destinationR, 1);
await element.play();
return [destinationL.stream, destinationR.stream];
ステレオ音声をChannelSplitter Nodeに接続
2つのモノラル音声に分割
MediaStream出力Nodeに接続
audioタグ要素(元のステレオ音源再生開始)
スピーカーに接続されていないためこの音は聞こえない
行数 コード 処理内容
24-25 async function recv(sender){
   const pc = new RTCPeerConnection();
recv関数
WebRTC受信処理
26-34    pc.ontrack = (e)=> {
     if(e.track.kind==='audio'){
       if(!window.audioTracks) window.audioTracks=[];
       audioTracks.push(e.track);
       if(audioTracks.length>=2){
         document.getElementById("recvaudio").srcObject = getMergeStream(audioTracks);
       }
     }
   };
オーディオトラックを2つ受信した場合
getMergeStream関数を呼びステレオ音声に結合
audioタグに関連づけて再生
35-42    pc.onicecandidate = (e) => {
     if(e.candidate){sender.addIceCandidate(e.candidate);}
   };
   await pc.setRemoteDescription(sender.localDescription);
   const answer = await pc.createAnswer();
   await pc.setLocalDescription(answer);
   await sender.setRemoteDescription(answer);
}
簡易的なWebRTC受信処理
行数 コード 処理内容
43 function getMergeStream(tracks){ getMergeStream関数
引数で受け取ったオーディオトラック配列をマルチチャンネルオーディオに変換する関数
44 const audioCtx = new(window.AudioContext || window.webkitAudioContext); AudioContextのインスタンスを生成
45 const dest = audioCtx.createMediaStreamDestination(); MediaStream出力Nodeを生成
46 const merger = audioCtx.createChannelMerger(tracks.length); 複数のモノラル入力を、マルチチャンネルオーディオに結合するNodeを新規作成
チャンネル数はtracks配列の長さ(2)
47-55 tracks.forEach((track, index) => {
   const stream = new MediaStream([track]);
   const mutedAudio = new Audio();
   mutedAudio.muted = true;
   mutedAudio.srcObject = stream;
   mutedAudio.play();
   const source = audioCtx.createMediaStreamSource(stream);
   source.connect(merger, 0, index);
})
tracks配列の長さ(2)だけループ処理
各オーディオトラックを無音再生開始
オーディオトラックを入力とするソースNodeを生成
ChannelMergerに接続し、出力チャンネル番号を0から順に指定する
56-57 merger.connect(dest);
return dest.stream;
ChannelMergerのステレオ出力をMediaStream出力Nodeに接続

WebRTC接続のコードについて

上記、一連のサンプルではWebRTC接続・配信・受信部分の詳細コード説明はあえて省略しています。というのも、WebRTCはブラウザ依存度が高く、抽象化したライブラリを使用するケースが非常に多いからです。本記事では、ステレオ配信ができないのはChromeブラウザ単体の問題であることが細部まで確認できるように、ライブラリを使わないサンプルを作成しました。

まとめ

ChromeでWebRTCを使ってステレオ配信再生する方法をご紹介しました。WebRTC高レベルライブラリを使った場合でも、まったく同じ考え方で実装可能です。また、ステレオ音源としてmp3ファイルを指定していますが、BlackHole等のバーチャルオーディオを使ったマルチチャンネル入力もまったく同じコードで対応可能です。ぜひ試してみてください。

有料サービスのご紹介

WebRTCを利用した会員制ライブ配信サービスを短期間で導入したい方、大規模配信のためのサーバ構築・インフラ運用を専門家に任せたい方は「ImageFlux Live Streaming」をご検討下さい。本記事にあるP2Pではなく、WebRTC SFUという技術を用いて、配信映像をサーバに中継させる仕組みです。ステレオ配信に対応しています。1対多の会員制映像配信に特に強みがあり、初期の利用コストが低価格に抑えられます。