THETA Sの全天球配信を強化する(2) 3Dサウンドを配信しよう


こんにちは、インフォコム技術企画室のがねこです。 前回はOculus Rift CV1を使えるようにするまでをお伝えしました。今回はそのCV1の性能をより活かすための強化です。

CV1はヘッドフォン一体型となりましたので、これを使わない手は有りません。今回の目玉の2つ目は、3Dサウンド(的なもの)への対応です。

VRコンテンツでは映像が注目されがちですが、音があたえる効果も絶大です。見ている映像、方向に合わせた音を聞かせることで、その体験は何倍もリアルになります。今回のAppsJapanで展示を行う全天球配信では、サウンド面も大幅に強化しました。映像の配信と表示もWebの技術(WebRTC, WebGL, WebVR)で行っていますので、サウンド面もWeb技術を使います。3Dサウンドの配信についてはメンバーのtokさん考案のもと、私も協力して実現させてました。

マイクの設置

今回はステレオマイクを2セット用意し、90度回転させた状態で配置しています。(実際にはステレオマイク付Webカメラを流用しています)

stereo_mic_cross.png

ちなみにtokさん開発の実物はこんな形をしています。

theta_stereo_twin-thumb-autox.jpg

ステレオ音声の取得

カメラから音声を取得するのは、WebRTCでもおなじみの getUserMedia です。今回は新しいAPIである、 navigator.mediaDevices.getUserMedia() を使いました。

※Chromeで利用するには、chrome://flagsで「試験運用版のウェブ プラットフォームの機能」を有効にする必要があります。ステレオサウンドの取得、通信は私が実験したところ Firefoxでは出来ませんでした。そのため、今回はChromeを使っています。

また、普通に取得するとマイクがステレオでも、モノラル音声になってしまいます。これはエコーキャンセリング機能を働かせるためです。おそらくステレオだとエコーと元の音の判別が難しくなってしまうのでしょう。今回は敢えてエコーキャンセリング機能を無効にすることで、ステレオ音声の取得を行っています。

function startSelectedAudio(num) {
        var deviceID = 'selected id heare';
        var constraints = {
            video: false,
            audio: { 
                deviceId: {exact: deviceID},
                echoCancellation:false // エコーキャンセリングを無効に
            }
        };
        if (isChrome) {
            var constraints = {
                audio: {
                    mandatory: { echoCancellation : false, googEchoCancellation: false}, // エコーキャンセリングを無効に
                    optional: [{sourceId: deviceID}]
                },
                video: false
            };
        }

        console.log('mediaDevice.getMedia() constraints:', constraints);

        // NOTE:
        //  to use this code, please turn on "#enable-experimental-web-platform-features" of chrome://flags , then restart Chrome. 
        // 注意:
        //  このコードを実行するには、chrome://flagsで「試験運用版のウェブ プラットフォームの機能」を有効にしてください(要再起動)
        //
        navigator.mediaDevices.getUserMedia(
            constraints
        ).then(function(stream) {
            // 音声取得時の処理
            localAudioStream = stream;
        }).catch(function(err){
            // エラー時の処理
            console.error('getUserMedia Err:', err);
        });
    }

これで、マイクがステレオであれば、ステレオで音声が取得できます。今回はステレオマイクが2セットあるので、デバイスIDを変えて2回音声を取得しています。

背面からの音に対応する、左右反転音声の生成

先ほどのマイクを90度向きを変えて交差させた図で、正面を向いているときはステレオマイク1の音を再生すればOKです。ところが視聴者が背後を振り返った場合、ステレオマイク1の音を左右反転させて聞かせる必要があります。同様にどの方向を向いているかで、視聴者に聞かせる音が変わります。

  • 正面を向いている場合 … ステレオマイク1の音声をそのまま
  • 右を向いている場合 … ステレオマイク2の音声をそのまま
  • 後ろを向いている場合(背後を振り返った場合) … ステレオマイク1の音声を左右反転させる
  • 左を向いている場合 … ステレオマイク2の音声を左右反転させる

左右反転した音声を生成するのは配信側でも視聴側でも良いのですが、今回は配信側で生成することにしました。左右反転はWebAudio を用いて行います。

stereo_flip_webaudio.png

この時若干の遅延が発生します。このままでは通常のステレオ音声とズレを感じてしまうため、そのズレを無くすために通常ステレオ音声も同じくWebAudioの処理を行っています。

stereo_normal_webaudio.png

ちなみにコードはこのようになっています。

var context = new window.AudioContext();
var micNode1, splitter1, merger1Normal, merger1Flip, outputStream1Normal, outputStream1Flip;
function splitMerge1(stream) {
	micNode1 = context.createMediaStreamSource(stream);
	splitter1 = context.createChannelSplitter(2);
	micNode1.connect(splitter1);
	merger1Normal = context.createChannelMerger(2);
	merger1Flip = context.createChannelMerger(2);
	splitter1.connect(merger1Normal, 0, 0);
	splitter1.connect(merger1Normal, 1, 1);
	splitter1.connect(merger1Flip, 0, 1);
	splitter1.connect(merger1Flip, 1, 0);
	
	outputStream1Normal = context.createMediaStreamDestination();
	merger1Normal.connect(outputStream1Normal);
	localAudioStream1Normal = outputStream1Normal.stream;
	
	outputStream1Flip = context.createMediaStreamDestination();
	merger1Flip.connect(outputStream1Flip);
	localAudioStream1Flip = outputStream1Flip.stream;
}

マルチストリーム通信

全天球配信のデモでは、離れた場所のカメラの映像、マイクの音声をWebRTCで通信し、視聴者が体験することができます。今回は360度映像×1、ステレオ音声×4 のマルチストリーム通信を行っています。

sending_multistream.png

最大の問題は受け取った側で、順序が保存されないことです(※ここは自信がありません。もしかしたら順序は決まっているのかもしれません)。受け取った音声ストリームが、マイク1かマイク2か、通常か左右反転かが区別できません。別の仕組みと合わせて工夫すれば識別できそうですが、今回は間に合いませんでした。それを補うため、視聴側で人間が判別するという職人技をメンバーのtokさんが編み出してくれました。

ステレオ音声通信

WebRTCでの通信を通すと、通常はせっかく取得したステレオ音声もモノラル音声になってしまいます。ステレオで通信するためには、音声コーデックのOpusにオプションを指定してあげる必要があります。

元のSDPの抜粋
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1

改変後のSDPの抜粋
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1; stereo=1; maxaveragebitrate=131072

ステレオの指定とビットレートの指定が追加されています。このオプション指定は、送信側だけでなく受信側でも追加する必要があります。

※この方法についてはこちらのサイトの情報を参考にさせていただきました。

ここまでやって、ようやく3Dサウンドを送信することができます。あとは受信側での処理が必要です。