久々の投稿になります。
EmpathというAI会社でアプリケーションサイドの開発をしています。
先日リモトーキー (R:EmoTalkie)というサービスをリリースしました。無料で使えます。
今回はR:EmoTalkieの開発背景 + 技術記事という構成で書かせていただきます。
開発背景
最近の世間の事情により、慣れている人、慣れていない人もリモートワークへ移行せざるを得ない状況になっています。
そんな中、やはり不安やストレス、寂しさ、そして元気がなくなってしまう人が多くなってきているというのをよく聞くようになり、Empathで「何かしら力になりたい」ということでR:EmoTalkieを2020年4月24日にリリースしました。

(実際のR:EmoTalkieの画面)
どんなもの?
R:EmoTalkieは通話アプリケーションの裏で動かして、使用者の音声を解析し、ミーティングへの参加率や活性度を可視化することで、積極的にミーティングへの参加を促すサポートツールとなっています。
※ブラウザアプリケーションとして実装していますが、音声は解析にしか使用しておらず、保存もしていません。安心して使ってください。

(解析結果は数値では表さず、褒めたり励ます仕様に)
無料?
**「この状況を乗り越えるための助けとなりたい」**という思いはあったのですが、リソース不足、昨今の事情への知識不足から、完璧なサービスをリリースするには限界があると思っていました。
そこで、1つの提案という形で無料でリリースすることにより、我々のことを広く知ってもらい、共感を得た方々と一緒に作り上げたいという思いから無料にしています。
なぜ2週間?
今回はお試しのようなサービスでのリリースとはいえ、前述の通り「この状況を乗り越えるために何か助けとなりたい」という思いがあったため、早期に出すことが重要だと考えていました。
とはいえ、スタートアップでリソースが限られている為、作業者自体は1人で行う必要もあり、なんとか形として出せる期間が2週間でした。
開発
今回のアプリケーションではWebRTCのコードを一部書いていますが、通話機能をもたせておらず、あくまでWebRTCの音声取得部分だけを利用しています。
WebRTCをちゃんと実装したい場合は、他の人の記事ですがWebRTCハンズオン 本編を参考にしていただくと分かりやすいと思います。
前提
音響周りの開発、webRTC関連の開発は初めてです。弊社には音響エンジニアがいますが、リソース的に入ることが難しいため、私1人で分からないながらも格闘したものとなります。
間違っているところがありましたらコメント等でご指摘いただければ幸いです。
開発環境
エディター : Visual Studio Code
開発言語 : ReactJS
解析API : EmpathAPI
通信ライブラリ : axios
コード
読みやすいように、実際のコードから少々変更してポイントのみ記載しています。
また、WebRTCの解説等は別の有志の方々の記事の方が参考になるかと思いますので、詳しくはこちらでは書きません。
マイクの使用開始
今回はカメラは使用せず、マイクのみとなるので、下記のように設定します。
   navigator.mediaDevices
      .getUserMedia({
         audio: {
            echoCancellation: true,
            echoCancellationType: 'browser',
            noiseSuppression: true,
            sampleRate: { ideal: 11025 }
         },
         video: false
      })
      .then((stream) => {
         this.handleStartMeeting(stream);
      })
      .catch(this.handleFailed);
echoCancellationやnoiseSuppressionを有効にしていますが、あまり実感できず、、こちら詳しい方がいましたらコメントしていただけると助かります...
Streamの取得
// ①
context = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLERATE }); // サンプリングレートを指定
...
// ②
   processor.onaudioprocess = (audio) => {
      this.micAnim(); //音量によって変わるマイクアニメーション
      var input = audio.inputBuffer.getChannelData(0);
      var bufferData = new Float32Array(BUFFER);
      for (var i = 0; i < BUFFER; i++) {
         bufferData[i] = input[i];
      }
      audioData.push(bufferData);
   }
   this.recordingTimer(); //5秒区切りの音声作成用タイマー
①の部分は重要です。流れてくる音声のサンプリングレートを11kで指定しています。
最後の行で5秒区切りの音声作成用Functionを呼び出します。
onaudioprocessについては、Web Sounderで以下のように説明されています。
onaudioprocessイベントの発生は以下のように実装されているようです.
1. AudioDestinationNodeへの接続とイベントハンドラの設定が完了したときに発生
2. 1. のあとは, バッファサイズのデータを処理するごとに発生
5秒区切りの音声作成
EmpathAPIを使用する上で、重要になってくるのは、5秒区切りの音声であるという部分になるかと思います。
そこを下記のタイマー処理で、5秒区切りの音声ArrayをAPIにて送るということをしています。
   let Interval = setInterval(() => {
      var count = this.state.count - 1;
      if (count > 1) {
         this.setState({
            count: count,
         });
      } else {
         clearInterval(Interval);
         this.resetCount();
         if (this.state.speachCount > MIN_SPEECH_REC_TIME) {
            this.stopAndSend();
         }
         ....
      }
   }, 1000);
初期値5のcountを1秒起きのタイマーでカウントダウンをし、5秒経ったらstopAndSend()で一旦マイクを停止し、EmpathAPIへ音声データを投げる処理に入ります。
マイクの再スタートは作成するサービスによって変わってくるので、適宜再スタートさせてあげてください。
常時録音でもよいですが、EmpathAPIのコール数には上限があるため、1時間のミーティングで使用するとえらい数になり、すぐ上限を超えてしまうかと思います...
if (this.state.speachCount > MIN_SPEECH_REC_TIME)こちらの行では、最低発話時間を見ています。ある一定の音量以上で発話した最低時間を設定しないと、全く発話していないのにAPIを叩いてしまうことになります。また、物を落としたときなどの雑音も解析してしまうことにもなります。
今回は約1秒以上、音声が入ってきたものを発話としています。
WAV変換
EmpathAPIに音声データを送るにはこれでは不十分なので、EmpathAPIで受け取れるWAVへ変換していきます。
   let encodeWAV = function (samples, sampleRate) {
      ...
      let view = new DataView(buffer);
      ...
      view.setUint32(24, sampleRate, true); // サンプリングレート
      view.setUint16(34, 16, true); // サンプルあたりのビット
      ...
      return view;
   }
   ...
   let dataview = encodeWAV(mergeBuffers(audioData), 11025);
   let audioBlob = new Blob([dataview], { type: 'audio/wav' });
こちらの書き方はいくつも扱っている記事があるので、詳しくは記載していきません。
重要なのはWAVとして扱うことと、サンプリングレートを11025とすることです。
EmpathAPI送信
こちらは前の記事でも取り扱ったのと同じく、axiosを使用していきます。
   const post = (wavData): Promise<*> => {
      let formData = new FormData();
      formData.append('apikey', key);
      formData.append('wav', wavData);
      return axios.post(url, formData, {
         headers: {
            'content-type': 'multipart/form-data',
         },
      }).then((res) => {
         return Promise.resolve(res);
      }).catch((response) => {
         return Promise.reject(response);
      })
   }
ポイントはapikeyとwavDataとなります。
apiKeyはEmpathAPIのAPI Key設定にあります。(※ログインが必要です。)
wavDataは前項のaudioDataを使用します。
また、忘れてはいけないのはEmpathAPIはmultipart/form-dataで受けつけているので、こちらも設定をします。
戻り値は{ "error": 0, "calm": 16, "anger": 9, "joy": 6, "sorrow": 17, "energy": 0 }のような値が返ってきます。
まとめ
ざっくりとしたポイント説明だったので、これだけでは作成は難しいかもしれません。が、大体こんな雰囲気で作られているということが伝わればと思います。
こうしたほうがいいよ、ここ間違っているよ、などがあればご連絡していただければと思います。
おわりに
今回リリースさせていただいたアプリはあくまで「Empath技術を使うとこんなことができるよ」という提示をしたに過ぎません。ここからはもっとリモートワーカー達がより過ごしやすくなるサービスとするには、たくさんのアイディアや技術者が必要となってきます。
弊社と共になにか創生したい!という方々はぜひこちらからご連絡をしていただければと思います。