というわけで、またも会社の合宿で作ったものを紹介します。
そして前回に引き続き、なんかまた優勝しました(∩´∀`)∩ワーイ
ざっくりでよければ社内勉強会で発表したスライドがあるのでこっちを。
何を作ったのか
具体的な利用シーンはコレです。
- イカしよーよ!
- じゃあイカデンワたてるわー
- あ、あたしiPhoneしか持ってないから無理だ・・
- (せめて聞くだけでもできればな・・
とか、
とか。
そんなときのソリューションになればなーと思って作ったのがコレ。
やってること
Pub: 配信する側
細かい話
コードは参考のために雑に抜粋してます。
マイクの音を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人しかつなげた状態までしか試せてないので、誰か試してくれないかなーと思ってるところ。