WebRTCで音声データを取得し音声のボリュームを表示する
ブラウザでマイクから取得した音声のボリュームを画面上で表示する必要があったので調べてみた
今回もNuxt.js
で書いてみる
前提
- yarn: v1.22.5
- Nuxt.js: v.2.14.6
ソースコード
<project-root>/pages/sample.vue
を作成
<template> <section class="section"> <div class="columns"> <div class="column"> <meter :value="volume" min="0" max="1" high="0.25"></meter> <span>{{ volume }}</span> </div> </div> <div class="buttons"> <button @click="getUserMedia">音声を取得する</button> </div> </section> </template> <script lang="ts"> /* eslint-disable no-console */ export default { data() { return { localstream: undefined, streamConst: { audio: true, // 録音する場合はaudio: trueとする video: false, }, audioContext: undefined, audioScript: undefined, mic: undefined, volume: 0, } }, mounted() {}, methods: { getLocalMediaStream(mediaStream: any) { this.localstream = mediaStream // 音声取得 // 音声コンテキストを作成 let audioContext = window.AudioContext this.audioContext = new audioContext() // 音声データを扱うためのオブジェクトを生成 // bufferSizeはOSごとに最適なサイズをあてる: https://weblike-curtaincall.ssl-lolipop.jp/portfolio-web-sounder/webaudioapi-basic/custom this.audioScript = this.audioContext.createScriptProcessor(2048, 1, 1) // audioContextで音声ストリームを扱えるようにする this.mic = this.audioContext.createMediaStreamSource(mediaStream) // それぞれをconnectで接続するしストリームの音声データを扱えるようにする this.mic.connect(this.audioScript) this.audioScript.connect(this.audioContext.destination) // イベントハンドラー this.audioScript.onaudioprocess = this.handleLocalOnAudioProcess }, handleLocalOnAudioProcess(event: any) { // via. https://tech.drecom.co.jp/web_audio_api_audiobuffer/ // サンプリング周波数と量子化ビット数を-1〜1の間で正規化した値を Float32Array で保存 [asin:B07X6F1C2P:detail] [asin:B01LYO6C1N:detail] const input = event.inputBuffer.getChannelData(0) // 入力ソースの音声データを取得 let sum = 0.0 for(let i = 0; i < input.length; ++i) { // inputBufferの値は-1〜1の間で正規化された数値であるため2乗することで正の数値に変更する sum += input[i] * input[i] } // sum / input.length: inputBufferから取得した音量の平均値を取得 // 平方根をを取ることで input[i] * input[i] の値をもとに戻す this.instant = Math.sqrt(sum / input.length) // 少数第3位を四捨五入し表示 this.instant = this.instant.toFixed(2) }, handleLocalMediaStreamError(error: string) { console.error(`navigator.getUserMedia error: ${error}`) }, getUserMedia() { console.log('getUserMedia') navigator.mediaDevices .getUserMedia(this.streamConst) .then(this.getLocalMediaStream) .catch(this.handleLocalMediaStreamError) }, }, } </script>
getUserMedia
でストリームの取得許可がとれたら音声データをゴニョゴニョするための準備を行う
流れとしては
- 音声コンテキストの作成:
new audioContext()
- 作成した音声コンテキストでストリームを扱えるように設定:
audioContext.createMediaStreamSource()
- 音声データを扱うためのオブジェクトを生成:
audioContext.createScriptProcessor()
- イベントハンドラの設定:
audioScript.onaudioprocess
getLocalMediaStream(mediaStream: any) { this.localstream = mediaStream let audioContext = window.AudioContext this.audioContext = new audioContext() this.audioScript = this.audioContext.createScriptProcessor(2048, 1, 1) this.mic = this.audioContext.createMediaStreamSource(mediaStream) this.mic.connect(this.audioScript) this.audioScript.connect(this.audioContext.destination) this.audioScript.onaudioprocess = this.handleLocalOnAudioProcess },
音量の可視化はhandleLocalOnAudioProcess()
で処理を行う
inputBuffer
には各入力チャンネルの音声データがFloat32Array
で格納されている。getChannelData(N)
で入力チャンネルを指定し音声データを取得する。今回は入力が1箇所しかないのでgetChannelData(0)
で取得。
取得した音声データの中身はサンプリング周波数
と量子化ビット数
を-1〜1
の間で正規化した値が入っている。
これらを正の数値に変更し平均値を作成し画面にボリュームとして表示する(for(let〜) - Math.sqrt()
の部分)
handleLocalOnAudioProcess(event: any) { const input = event.inputBuffer.getChannelData(0) // 入力ソースの音声データを取得 let sum = 0.0 for(let i = 0; i < input.length; ++i) { sum += input[i] * input[i] } this.instant = Math.sqrt(sum / input.length) this.instant = this.instant.toFixed(2) },
取得したボリュームはmeter要素
で可視化する
<meter :value="volume" min="0" max="1" high="0.25"></meter> <span>{{ volume }}</span>
最後にyarn dev
しhttp://localhost:3000
にアクセスし動作確認を行う
参考サイト
Dockerコンテナでyarnをインストールする
Django REST Framework
用に作成しているpythonのDockerコンテナに
nuxt.js
を利用するためにyarn
をインストールしてみた
前提
- docker:
19.03.8
- dockerコンテナのベースイメージ:
python:3.7-stretch
- yarn:
1.22.5
手順
1. Dockerfile
に以下を追記
RUN curl https://deb.nodesource.com/setup_12.x | bash RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update && apt-get install -y yarn
2. コンテナをビルドする
# docker-compose利用前提 $ docker-compose up -d # 既にイメージを作っていて再作成する場合はこっち $ docker-compose up -d --build
参考サイト
- dockerイメージ内にyarnをインストールする - Qiita
- Cannot install Yarn inside Docker image · Issue #7329 · yarnpkg/yarn
プログラマのためのDocker教科書 第2版 インフラの基礎知識&コードによる環境構築の自動化
- 作者:WINGSプロジェクト阿佐 志保
- 発売日: 2018/04/11
- メディア: Kindle版
Amazon RekognitionをLaravelから使う
案件でWebカメラから取得した画像を元に表情を分析してどうのこうのする必要があり調べてみたのでメモ。
Amazon Rekognition
を選定した理由は料金がGCPのサービスに比べて安かったから…!
Nuxt.js
から直接APIに対してPOSTすることもできるんだけど、CORSを回避する手段も用意しておきたいと思ってLaravel
も利用している
環境
- MacOS: 10.15.6
- node.js: v14.12.0
- yarn: v1.22.5
- Nuxt.js: v.2.14.6
- Laravel: 5.3.8
手順
1-1. IAMでユーザを作る
1-2. AWS SDKを利用して実装
まずはSDKをインストール
$ composer require aws/aws-sdk-php
1-3. 表情分析用のコントローラを作成する
$ php artisan make:controller FaceController
Amazon Rekognition
自体はAws\Rekognition\RekognitionClient
を通して行う
<?php namespace App\Http\Controllers; use Aws\Rekognition\RekognitionClient; use Illuminate\Http\Request; class FaceController extends Controller { /* フロントからのPOSTデータを受け取る */ public function post(Request $request) { $photo = $request->photo; $detectedData = $this->detectFaces($photo); $ret = [ 'detectedData' => $detectedData, ]; return $ret; } /* Amazon Rekognitionで表情分析を行う */ private static function detectFaces($file) { $options = [ 'region' => 'ap-northeast-1', 'version' => 'latest', 'credentials' => [ 'key' => 'XXXX', // ここを編集してね 'secret' => 'XXXX' // ここを編集してね ] ]; $result = null; try { $client = new RekognitionClient($options); // 顔検出 $result = $client->detectFaces([ 'Image' => [ 'Bytes' => file_get_contents($file), ], // 'ALL': 全ての結果が返ってくる // 'DEFAULT': 一部結果(顔の角度, 明るさetc)のみ返る 'Attributes' => ['ALL'] ]); } catch (\Exception $e) { echo $e->getMessage() . PHP_EOL; } return isset($result['FaceDetails']) ? $result['FaceDetails'] : null; } }
api.php
にルーティングを追加する
... Route::group(['prefix' => 'face'], function ($route) { $route->post('/post', 'FaceController@post'); });
1-4. フロント部分を作成する
フロントは先日ブログに書いたNuxt.jsでWebカメラで映像を撮影 & 画像の取得をしてみる - @Satoh_D no blogのコードに以下を追記する
<template> <section class="section"> <div class="columns"> <div class="column"><video id="localvideo" ref="localVideo" autoplay :srcObject.prop="localstream" width="640" height="350" ></video></div> </div> <div class="columns"> <div class="column"> <canvas id="localPic" ref="localPicCanvas" width="640" height="350" ></canvas> </div> </div> <div class="buttons"> <b-button @click="getUserMedia">映像を取得する</b-button> <b-button @click="getUserPic">写真を撮る</b-button> <b-button @click="getEmotion">表情判定</b-button> </div> </section> </template> export default { data() { return { localstream: undefined, canvas: undefined, video: undefined, // movie -> videoに変更 photo: undefined, // 追加 res: undefined, // 追加 streamConst: { video: { width: 640, height: 350, }, }, } }, mounted() { this.canvas = this.$refs.localPicCanvas this.video = this.$refs.localVideo }, computed: {}, methods: { getLocalMediaStream(mediaStream: any) { this.localstream = mediaStream }, handleLocalMediaStreamError(error: string) { console.error(`navigator.getUserMedia error: ${error}`) }, getUserMedia() { navigator.mediaDevices .getUserMedia(this.streamConst) .then(this.getLocalMediaStream) .catch(this.handleLocalMediaStreamError) }, getUserPic() { const ctx = this.canvas.getContext('2d') ctx.drawImage(this.video, 0, 0, 640, 350) this.photo = this.canvas.toDataURL() }, getEmotion() { // base64 -> blobに変換 const dataurl = this.photo const bin = atob(dataurl.split(',')[1]) const buffer = new Uint8Array(bin.length) for (let i = 0; i < bin.length; i++) { buffer[i] = bin.charCodeAt(i) } const blob = new Blob([buffer.buffer], { type: 'image/png' }) const formdata = new FormData() formdata.append('photo', blob, 'image.png') formdata.append('hogehoge', 'fugafuga') this.res = this.$axios .$post('/face/post', formdata, { headers: { 'content-type': 'multipart/form-data' }, }) .then((res) => { console.log('resnpose data', res) }) .catch((error) => { console.log('response error', error) }) }, }, }
必要なものはgetEmotion()
の部分
canvas
要素に書き出した画像を画像ファイルとして実体化し、axios
でLaravel側にPOSTする
canvas
の画像をblob
に変換する方法としてHTMLCanvasElement.toBlob()
という関数が用意されているようだが、現時点でSafariがサポートされていないのでこのような実装になっている
const dataurl = this.photo const bin = atob(dataurl.split(',')[1]) const buffer = new Uint8Array(bin.length) for (let i = 0; i < bin.length; i++) { buffer[i] = bin.charCodeAt(i) } const blob = new Blob([buffer.buffer], { type: 'image/png' })
POST部分で気をつけることは、画像などファイルを含む場合はheaders: { ‘content-type’: ‘multipart/form-data’ }
でheaderをつけてあげる
今は戻り値をconsole.log()
で出力するだけ(必要に応じて処理を追加する)
nodenv経由でインストールしたnodeでyarnをインストールしたけど動かなかった件
nodeenv経由のnpm
でyarn
をインストールしたけどcommand not found
とエラーが出てしまい利用できなかった
この解決方法をメモしておく
環境
- MacOS: 10.15.6
- nodenv: 1.4.0+3.631d0b6
- node.js: v14.12.0
- yarn: 1.22.5
解決方法
nodenv rehash
するだけ
yarnをインストールした直後はshimsが更新されておらずPATHが通っていないので、このコマンドを打つことでshimsを更新しPATHが通った状態になるとのこと。
$ npm installl -g yarn $ yarn # 何故かエラーとなる bash: yarn: command not found # 解決 $ nodenv rehash $ yarn -v 1.22.5
参考サイト
Nuxt.jsでWebカメラで映像を撮影 & 画像の取得をしてみる
案件でブラウザからWebカメラにアクセスし、画像を取得する実装を刷ることになったのでサンプルを作ってみた。
Nuxt.js
初めて使ったけど学習コスト低くていいですね。もっと勉強したい。
環境
- MacOS: 10.15.6
- node.js: v14.12.0
- yarn: v1.22.5
- Nuxt.js: v.2.14.6
手順
1. Nuxt.jsでプロジェクトを作成する
$ yarn create nuxt-app webrtc-sample yarn create v1.22.5 [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies... [4/4] 🔨 Building fresh packages... success Installed "create-nuxt-app@3.3.0" with binaries: - create-nuxt-app create-nuxt-app v3.3.0 ✨ Generating Nuxt.js project in webrtc-sample ? Project name: webrtc-sample ? Programming language: JavaScript ? Package manager: Yarn ? UI framework: Buefy ? Nuxt.js modules: Axios ? Linting tools: ? Testing framework: None ? Rendering mode: Single Page App ? Deployment target: Server (Node.js hosting) ? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Version control system: Git 🎉 Successfully created project webrtc-sample
プロジェクトができたら yarn dev
で起動
これで http://localhost:3000
でブラウザで表示ができるようになる
この時点ではデフォルトのページが表示される
$ yarn dev
ソースコード
新規でページを作成し、そこにソースコードを記述する(index.vueを編集しても可)
今回は /pages/webrtc.vue
を作成
<template> <section class="section"> <div class="columns"> <div class="column"><video id="localvideo" ref="localVideo" autoplay v-bind:srcObject.prop="localstream" width="500" height="500"></video></div> </div> <div class="columns"> <div class="column"><canvas id="localPic" ref="localPicCanvas" width="640" height="350"></canvas></div> </div> <div class="buttons"> <b-button @click="getUserMedia">映像を取得する</b-button> <b-button @click="getUserPic">写真を撮る</b-button> </div> </section> </template> <script> export default { data: function() { return { localstream: undefined, canvas: undefined, movie: undefined, streamConst: { video: { width: 1280, height: 700 } } } }, mounted: function() { this.canvas = this.$refs.localPicCanvas; this.movie = this.$refs.localVideo; }, methods: { getLocalMediaStream: function(mediaStream) { this.localstream = mediaStream; }, handleLocalMediaStreamError: function(error) { console.error(`navigator.getUserMedia error: ${error}`); }, getUserMedia: function() { navigator.mediaDevices .getUserMedia(this.streamConst) .then(this.getLocalMediaStream) .catch(this.handleLocalMediaStreamError); }, getUserPic: function() { const ctx = this.canvas.getContext('2d'); ctx.drawImage(this.movie, 0, 0, 640, 350); } } } </script>
Webカメラで撮影する部分と写真を撮る部分は以下の通り
Webカメラで撮影する部分
<template> ... <div class="column"><video id="localvideo" ref="localVideo" autoplay v-bind:srcObject.prop="localstream" width="500" height="500"></video></div> ... <b-button @click="getUserMedia">映像を取得する</b-button> ... </template> <script> export default { data: function() { return { localstream: undefined, streamConst: { video: { width: 1280, height: 700 } } } }, ... methods: { getLocalMediaStream: function(mediaStream) { this.localstream = mediaStream; }, handleLocalMediaStreamError: function(error) { console.error(`navigator.getUserMedia error: ${error}`); }, getUserMedia: function() { navigator.mediaDevices .getUserMedia(this.streamConst) .then(this.getLocalMediaStream) .catch(this.handleLocalMediaStreamError); }, ... } } </script>
ブラウザからWebカメラにアクセスするには navigator.mediaDevices.getUserMedia()
を利用する
navigator.mediaDevices.getUserMedia()
はpromise
が利用できるのでthen(), catch()
を利用することができる
ストリームが取得できた場合、video要素
にバインドすると映像が表示できるようになる
このとき、ストリームをv-bind:src
で指定してもよいが(ストリームをURL.createObjectURL
で変換する必要あり)、URL.createObjectURL
が廃止予定のメソッドであるためv-bind:srcObject.prop
で指定するほうが良いらしい
写真を撮る部分
<template> ... <div class="column"><canvas id="localPic" ref="localPicCanvas" width="640" height="350"></canvas></div> ... <b-button @click="getUserPic">写真を撮る</b-button> ... </template> <script> export default { data: function() { return { canvas: undefined, movie: undefined, } }, mounted: function() { this.canvas = this.$refs.localPicCanvas; this.movie = this.$refs.localVideo; }, methods: { ... getUserPic: function() { const ctx = this.canvas.getContext('2d'); ctx.drawImage(this.movie, 0, 0, 640, 350); } } } </script>
撮影する部分はcanvas要素
を利用して描画するのみ
setInterval
を利用すれば指定ミリ秒後に写真を取り続けるといったことも可能になる
参考サイト
【JavaScript】IE11では関数の引数に初期値を設定すると「’)’がありません」エラーと出る
今やってる案件(IE11も対応範囲)で、謎のエラーに悩まされてたのが解決出来たのでメモ。
(今もIE11対応が必要なのかというのはあるけれども。。)
エラーの現象
一部のページをIEで確認するとDevtoolのコンソールタブに
’)’がありません
というメッセージが表示される。
何度も該当箇所の括弧の数を確認しても問題ない…。
解消方法
少し調べてみたところ、IE固有のバグ とのこと。
IEでは関数の引数に次のように初期値を指定すると今回のようなエラーが出るそうで。。
// 今回エラーが起きていた該当箇所のソース(一部改変) function foo ( bar = true ) { }
エラーが出ないよう関数内で処理をするよう変更したことで解消できた。
本当にIEってやつは…
function foo ( bar ) { if (bar == undefined ) { bar = true; } }
参考にしたサイト
改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで
- 作者:山田 祥寛
- 発売日: 2016/09/30
- メディア: 大型本
Anker PowerPort Atom III Slimを買ってみた
Amazonの新生活セールで「Anker PowerPort Atom III Slim」が500円引きの2,000円になっていたのでポチってみた。
最近出張にMacbook Proを持っていくことが多く、ACアダプタを軽いものにすることで少しでも荷物を軽くするのが狙い。
ACアダプタって地味に重いんだよなぁ。。
届いてびっくりしたのがその小ささ。
名刺よりも一回り小さいくらいで、薄さもそれなり。
重さも56g(実測 / カタログでは59g)とめちゃくちゃ軽い。
Macbook ProのACアダプタが190gくらいだったので140gくらいは軽量化に成功したことになる。
これで次の出張が少し楽になる...かも?