🧊

iOSでWebRTCが使えないからWebSocketとWebAudioで擬似ストリーミングしてみた

というわけで、またも会社の合宿で作ったものを紹介します。
そして前回に引き続き、なんかまた優勝しました(∩´∀`)∩ワーイ

github.com

ざっくりでよければ社内勉強会で発表したスライドがあるのでこっちを。

WebSocketでAudioStreamingしてみた

何を作ったのか

具体的な利用シーンはコレです。

  • イカしよーよ!
  • じゃあイカデンワたてるわー
  • あ、あたしiPhoneしか持ってないから無理だ・・
  • (せめて聞くだけでもできればな・・

とか、

  • イカやろうぜ!
  • Skypeよろー
  • 俺のPC、Skypeと相性悪いから通話なしで!
  • (せめて携帯で聞くだけでもできたらな・・

とか。

そんなときのソリューションになればなーと思って作ったのがコレ。

やってること

Pub: 配信する側

  • 端末マイクからAudioStreamを拾う
  • Web Audio APIで適当にノイズ処理
  • Web Audio APIでサンプルデータを取得
  • それをWorkerに立てたSocket.IOのクライアントからサーバーへ流す

Sub: 聞く側

  • Workerに立てたSocket.IOのクライアントから音データをもらう
  • WebAudio APIで再生する

これでiPhoneしかもってない人でも、聴けるだけならチャットに参加できるように!

イデア自体は今にはじまったものではなく、WebSocketでバイナリが飛ばせるようになった時代からあったものです。
そんな先人のネタをかき集め、現代風に書きなおした感じです。

以下、個別のメモとしてインターネットに放流したいものを書いていきます。

細かい話

コードは参考のために雑に抜粋してます。

マイクの音をjsで加工するには

WebRTC界隈ではおなじみの、Media Streaming API a.k.a. `getUserMedia()`を使います。
で、そのストリームをWebAudioでAudioNodeにして、それで触ります。

var ctx = new AudioContext();

// マイク取る
navigator.getUserMedia(
  { audio: true },
  function(stream) {
    // AudioNodeに
    var source = ctx.createMediaStreamSource(stream);

    // オーディオのサンプルに触るなら
    var processor = ctx.createScriptProcessor(BUFFER_SIZE, 1, 1);
    processor.onaudioprocess = function(ev) {
      var inputBuffer  = ev.inputBuffer;
      var outputBuffer = ev.outputBuffer;

      // Float32なArrayBuffer
      var inputData  = inputBuffer.getChannelData(0);
      var outputData = outputBuffer.getChannelData(0);
      
      // 何もしなくてもoutputは返さないと音が出ない
      outputData.set(inputData);
    };
    
    source.connect(processor);
    processor.connect(ctx.destination);
  },
  function(err) {
    console.error(err);
  }
);

ポイントは、

  • `destination`までちゃんと`connect()`しないと音が流れてこない
  • `onaudioprocess`ではoutputを返さないといけない

なんちゃってバンドパスフィルタ

普段なにげに使ってる電話ですが、あいつはすごいんです。

日常にはノイズがあふれていて、パソコンのマイクで音を拾う場合には、肝心の声以外にも雑音がたくさん乗ります。
そこで、必要な周波数帯域(バンド)だけを通過(パス)させる必要があり・・。

// なんちゃってバンドパスフィルタ
var filter = ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = (100 + 7000) / 2;
filter.Q.value = 0.25;

WebAudio APIにはそういうフィルタを作るAudioNodeがあるので、それで設定します。
アナログ電話は300Hz ~ 3.4kHz / ひかり電話は100Hz ~ 7kHzあたりが有効な帯域らしいので、それを模したい・・が、その範囲をうまいことカバーするのは大変なので固定で決め打ち。

ほんとは音声成分を解析して、選択域を随時変えたりするアクティブなフィルタにすべきなんでしょうが・・・まぁ無いよりマシです。
普通に通話してる声を拾うだけなら十分だと思います。

裏タブに回ってもWebWorkerは仕事する

WebAudio APIで音を鳴らすだけであれば、裏タブに回ろうがブラウザ自体が非アクティブになろうが問題ないんですけど・・。
WebSocketで通信するあたりの処理は間引かれてしまうっぽい?です。

そこで、それらの処理はWebWorkerにやらせることで、安定した動作が期待できました。
Browserifyしてる中で使いたい場合は、webworkifyってのを使う。

GitHub - substack/webworkify: launch a web worker that can require() in the browser with browserify

呼び出し側のコード。

// これを
var worker = new Worker('./worker.js');

// こう
var webworkify = require('webworkify');
var worker = webworkify(require('./worker.js'));

// 後は一緒
worker.postMessage();
worker.addEventListener('message', (ev) => {});

呼ばれる側のコード。

// これを
importScripts('./path/to/socket.io-client.js');

// こう
var io = require('socket.io-client');

module.exports = function(self) {
  // ココはいつもと一緒
});

書き味はほぼ一緒だと思います。
はじめてWorkerを正しい使い方で使った気がする!

WebSocketで音データを飛ばして流す

肝心のところですね。

まずは飛ばす側。

// evはonaudioprocessより
var outputBuffer = ev.outputBuffer;
var outputData = outputBuffer.getChannelData(0);

socket.emit('audio', outputData.buffer);

受ける側。

var ctx = new AudioContext();
var startTime = 0;

// evはonaudioprocessより
socket.on('audio', function(buffer) {
  // バッファはFloat32
  var f32Audio = new Float32Array(buffer);

  var source = ctx.createBufferSource();
  var audioBuffer = ctx.createBuffer(1, BUFFER_SIZE, ctx.sampleRate);
  audioBuffer.getChannelData(0).set(f32Audio);
  source.buffer = audioBuffer;
  source.connect(ctx.destination);

  // 再生タイミングに注意
  var currentTime = ctx.currentTime;
  if (currentTime < startTime) {
    source.start(startTime);
    startTime += audioBuffer.duration;
  } else {
    source.start(startTime);
    startTime = currentTime + audioBuffer.duration;
  }
});

大事なポイントは2つ。

  • AudioBufferのPCMデータはFloat32Arrayなので、受け側もあわせる
  • ソケットの到着順に随時再生だと、自然に聞こえないのでこちらでスケジューリングが必要

調べてもぜんぜん例がなくて苦労したけど、できてみたらあっさりしたコードで拍子抜けした(˘ω˘

性能について

結論から書くと、

  • ハードウェア性能は問わなそう
  • ネットワークは狭い・速いにこしたことない
    • ただLANでもWANでも品質にそこまで差はない
  • 別に遅くはない
    • 遅延は1秒あるかないかとか
    • が、長いことやってるとズルズル遅れていくような?
    • まぁ定期的に繋ぎ直せば良い
  • 音質は通話レベルなら問題ない
    • 音楽を聞くとなると音質が気になる

というわけで、割と良い気がしてます。
パフォーマンスって意味では2−3人しかつなげた状態までしか試せてないので、誰か試してくれないかなーと思ってるところ。

おわりに

とりあえずWeb屋として出来る限りのことをやってみた感があって、これ以上のチューニングを諦めてる感があります。
音質をもうちょい調整できれば、家ネットワークでBluetoothに頼らずともワイヤレスで聞けたりするのになー。
そういう感じのラジオシステムみたいな使い方もできちゃうのになー。

というわけで、WebAudioガチ勢の人とか、TypedArrayマニアの人とか、音声通話のフィルタ作ったことあるよって人とか、何かとフィードバックあれば嬉しいです!

そのままは使えんけど、コードを適当にコピーしてつなぎこめばイカ○ンワのプラグイン的にも!