@Satoh_D no blog

大分にUターンしたので記念に。調べたこととか作ったこととか食べたこととか

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 devhttp://localhost:3000にアクセスし動作確認を行う

参考サイト

Vue.js&Nuxt.js超入門

Vue.js&Nuxt.js超入門

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

参考サイト

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()で出力するだけ(必要に応じて処理を追加する)

Vue.js&Nuxt.js超入門

Vue.js&Nuxt.js超入門

PHPフレームワーク Laravel入門 第2版

PHPフレームワーク Laravel入門 第2版

nodenv経由でインストールしたnodeでyarnをインストールしたけど動かなかった件

nodeenv経由のnpmyarnをインストールしたけど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を利用すれば指定ミリ秒後に写真を取り続けるといったことも可能になる

参考サイト

Vue.js&Nuxt.js超入門

Vue.js&Nuxt.js超入門

【JavaScript】IE11では関数の引数に初期値を設定すると「’)’がありません」エラーと出る

今やってる案件(IE11も対応範囲)で、謎のエラーに悩まされてたのが解決出来たのでメモ。
(今もIE11対応が必要なのかというのはあるけれども。。)

エラーの現象

一部のページをIEで確認するとDevtoolのコンソールタブに

’)’がありません

というメッセージが表示される。

何度も該当箇所の括弧の数を確認しても問題ない…。

解消方法

少し調べてみたところ、IE固有のバグ とのこと。
IEでは関数の引数に次のように初期値を指定すると今回のようなエラーが出るそうで。。

// 今回エラーが起きていた該当箇所のソース(一部改変)
function foo ( bar = true ) {
}

エラーが出ないよう関数内で処理をするよう変更したことで解消できた。
本当にIEってやつは…

function foo ( bar ) {
    if (bar == undefined ) {
        bar = true;
    }
}

参考にしたサイト

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くらいは軽量化に成功したことになる。

これで次の出張が少し楽になる...かも?