ml5でお前のソウルがウルトラソウルかニセモノソウルか判定する

#それ本当にウルトラソウル？
「__そして輝く？__」と言われたら皆さんはなんと答えますか？
そうですね、「__ｳﾙﾄﾗｿｳｳｩｯｯｯｯｯ　ﾊｱｲｨｯｯ__」ですよね。

でもそのソウル、ちゃんと魂を込めた__ウルトラソウル__ですか？
魂のこもっていない__ニセモノソウル__になっていませんか？
今回はTeachable Machineとml5を使って「__ウルトラソウルかニセモノソウルか__」を判定するウェブアプリを作ってみました。

デプロイしているので是非皆さんも__ｳﾙﾄﾗｿｳｳｩｯｯｯｯｯﾊｱｲｨｯｯ__してみてくださいね。
コードも是非ご覧になってハンズオンしてみてください！

Appは[こちらから](https://competent-northcutt-5b8ae1.netlify.app/ultra_soul.html)
コードは[こちらから](https://gist.github.com/canonno/6a34aaf21dccbc37a07a5ddeeafee62e)

#完成デモ
##ソウル判定
「夢じゃないあれもこれも」をクリックすると、サビが始まります。
「そして～か～がや～く」に続いて全力で「__ｳﾙﾄﾗｿｳｳｩｯｯｯｯｯ　ﾊｱｲｨｯｯ__」と叫んでください。
「__ｳﾙﾄﾗｿｳｳｩｯｯｯｯｯ　ﾊｱｲｨｯｯ__」より前の部分は判定に関わらないので、__しっかり精神統一してください__。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">お前のｿｳﾙが本当にｳﾙﾄﾗｿｳﾙかを判定するウェブアプリ作りました。「夢じゃないあれもこれも」をクリックするとサビが始まるので、サビ最後で元気に「ｳﾙﾄﾗｿｳｳｩｯﾊｱｱｲｨｯ」と叫んでください。<br>え？前奏が長すぎる？？<br>そんなこと言う時点でﾆｾﾓﾉｿｳﾙです（煽り)<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://t.co/ZarxNGh9XU">pic.twitter.com/ZarxNGh9XU</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301020381092872192?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##ウルトラソウルだった場合
ちゃんとウルトラソウルだった場合は「ウルトラソウル」、心がこもってなかったら「ニセモノソウル」と判定されます。
__「ニセモノソウル」判定の学習データが僕の歌声なので、僕がいくら全力で歌って検証してもニセモノ扱いされる__ことに気づきました。
難易度がさっぱり分からないので感想やコメントお待ちしております！！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">外部マイクがなぜか使えないので音声が聞こえませんが、ホンモノのｳﾙﾄﾗｿｳﾙを聞かせるとちゃんとウルトラソウルと判定します。<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/ml5?src=hash&amp;ref_src=twsrc%5Etfw">#ml5</a> <a href="https://twitter.com/hashtag/MachineLearning?src=hash&amp;ref_src=twsrc%5Etfw">#MachineLearning</a> <a href="https://t.co/csk9WWwZVt">pic.twitter.com/csk9WWwZVt</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301021077628313605?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##ニセモノソウルだった場合
ニセモノソウルの場合はこちら。
__パソコンを前にして全力でｳﾙﾄﾗｿｳﾙしてるのに非情な判定__です。
つまりｳﾙﾄﾗｿｳﾙはそんな生半可なものじゃないということですね(?)。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">外部マイクがなぜか使えないので音声が聞こえませんが、パソコンを前にして全力でｳﾙﾄﾗｿｳﾙしてます。<br>ですがニセモノ扱いされて悲しいです。<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/ml5?src=hash&amp;ref_src=twsrc%5Etfw">#ml5</a> <a href="https://twitter.com/hashtag/MachineLearning?src=hash&amp;ref_src=twsrc%5Etfw">#MachineLearning</a> <a href="https://t.co/C6uXZNGpOM">pic.twitter.com/C6uXZNGpOM</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301024938061520897?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##お手本を聞く
思い出しながらソウルするのは難しいので、__是非お手本を聞いてウルトラソウルを感じてください__。
やってみると「ﾊｱｱｨｯｯ」が案外難しいので、ここの攻略に焦点を置いて頑張ってください。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ホンモノどんなんだっけ・・・？というときは手本を聞いてください。<br>しっかりホンモノをｿｳﾙに叩き込んでください。<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/ml5?src=hash&amp;ref_src=twsrc%5Etfw">#ml5</a> <a href="https://twitter.com/hashtag/MachineLearning?src=hash&amp;ref_src=twsrc%5Etfw">#MachineLearning</a> <a href="https://t.co/qs6tYSoMLP">pic.twitter.com/qs6tYSoMLP</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301020686710784001?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#実装こまごま
##学習操作
今回はTeachable MachineというGoogle製サービスを利用して機械学習を行いました。
その場で動画を取り学習データに利用できるなど、サクッとモデルが作れるサービスになっております。
GUIが非常に優秀で本当に使いやすいので、[こちらから](https://teachablemachine.withgoogle.com/)是非是非ハンズオンしてみてください！

Youtubeからサビの部分を再生したものを学習データとして利用しています。
こちらが「ウルトラソウル」判定用の学習データです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">学習データです <a href="https://t.co/lrjd5FgSVm">pic.twitter.com/lrjd5FgSVm</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301031183002738689?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こちらが「ニセモノソウル」判定用の学習データです。
改めて聞くとめちゃめちゃ恥ずかしいですね。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">学習データ2です <a href="https://t.co/pcGNW5joTi">pic.twitter.com/pcGNW5joTi</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301031260278517760?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

学習データがこのようになっているので、真面目なロジックとしては「__B'zの歌声っぽいかvs僕の歌声っぽいか__」で判定しています。
もしニセモノソウルとしか判定されない場合は、__声質を可能な限りB'zに近づけるよう努力してみてください__。

##モデルのロード
Teachable Machineで学習したモデルはクラウドへデプロイできます。
URLが発行され、モデルロード用のjsのコードも用意してくれるのでこれをコピペするだけです。
p5.jsを使って予測結果を描画するコードも出力してくれますﾍﾞﾝﾘｨｨｨ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666990/e599d46a-c369-81ce-2bc4-892038547c7a.png)

画面に表示する中身をlabelとしています。
判定は常に実行され、その結果をlabellistにpushし続けています。
後ほどのロジックでlabellistに入った判定結果を利用してlabelに判定結果を代入し、画面表示を行います。

```js
      //////////////////////////////////////
      // 画面に表示するために使うGlobal変数 //
      //////////////////////////////////////
      let label = 'Now Loading...';
      let labellist = [];

      ///////////////////////////////////////////////
      // Teachable Machineによる判定と表示ロジック ///
      //////////////////////////////////////////////

      // 判定器オブジェクト
      let classifier;
      let soundModel = 'https://teachablemachine.withgoogle.com/models/tJ-euCMSN/';

      //モデルのロード
      function preload() {
        // Load the model
        classifier = ml5.soundClassifier(soundModel + 'model.json');
      }

      //キャンバスの作成・判定
      function setup() {
        let canvas = createCanvas(320, 240);
        canvas.parent("canvas");
        classifier.classify(gotResult);
      }

      //labelを常に描画し続ける処理
      function draw() {
        background(0);
        // Draw the label in the canvas
        fill(255);
        textSize(32);
        textAlign(CENTER, CENTER);
        text(label, width / 2, height / 2);
      }

      // モデルが判定に成功するとここが実行
      function gotResult(error, results) {
        if (error) {
          console.error(error);
          return;
        }
        labellist.push(results[0].label);
      }

```

##音楽の再生
[先日投稿した記事](https://qiita.com/canonno/items/8b0be6309b479ec5cdcc)でYoutube Player APIを勉強したので、音楽再生はこのAPIを利用して実装しました。
ボタンクリックすると音楽が再生されます。
本番かお手本再生かで再生開始時間と終了時間を区別しています。
loadVideoByIDといった便利なメソッドがあるので、是非[公式ドキュメント](https://developers.google.com/youtube/iframe_api_reference?hl=ja)もご覧になってみてくださいね。

```js

     //YoutubePlayerを読み込んだ際に最初に走る処理
      function onYouTubeIframeAPIReady() {
        //player1の定義
        player = new YT.Player('player', {
          height: '0',
          width: '0',
          videoId:'Ujb-ZeX7Mo8',
          events: {
            'onReady': onPlayerReady,
            'onStateChange':onPlayerStateChange
          }
        });
      }

      //ボタンが押されたらplayerを再生
      function playMusic(isOtehon){
        if (isOtehon){
          label = "お手本"
          player.loadVideoById({
            videoId:'Ujb-ZeX7Mo8',
            startSeconds:65.5,
            endSeconds:67.5,  
          })
        }else{
          label = "ｳﾙﾄﾗｿｳﾙ ﾊｲｨｯ\nで判定"
          player.loadVideoById({
            videoId:'Ujb-ZeX7Mo8',
            startSeconds:46,
            endSeconds:65.5,  
          })
        }
      }

```

##ソウル判定
playerの再生が止まったことをトリガーに、ソウル判定を行います。
お手本再生の場合はソウル判定したくないので、__再生終了時点での再生時間を取得して、ウルトラソウル前(だいたい65秒地点ぐらい)で停止した時のみ実行__されるよう実装しています。

先述の「モデルのロード」の章で、判定結果が常にlabellistにpushされ続けると書きました。
「ウルトラソウル」以外の部分の判定結果は要らないので、一旦labellistの中身を空にし、空にした直後3秒間の結果を利用してcheckSoulしています。

```js

      //YoutubePlayerが止まったときに走る処理
      function onPlayerStateChange(event) {
        //60～66秒地点で止まった場合のみ実行。66秒以降に止まった場合、お手本を再生しているのでソウル判定をしない
        if (event.data == YT.PlayerState.ENDED && player.getCurrentTime()>60 &&player.getCurrentTime()<66) {
          console.log(player.getCurrentTime())

          //lablelistを空に
          labellist = []

          //3秒間にlabellistにpushされた中身をみてchecksoul
          setTimeout(checkSoul,3000)
        }
      }
```

checkSoulの中身はこちらです。
3秒間にlabellistに入れられた判定結果を調べて、ホンモノとニセモノの個数を比較して判定しています。
安直な実装ですが、こんなものでも案外うまく判別できるんだなぁと実感しました。

```js

      //ソウル判定
      function checkSoul(){
        //各ソウル判定回数
        let ultrasoul_count = 0;
        let nisemonosoul_count = 0;

        //3秒間にlabellistにpushされた中身を見てカウント
        for (idx=0;idx<labellist.length;idx++){
          if (labellist[idx]=="ホンモノソウル"){
            ultrasoul_count += 1;
          }
          if (labellist[idx]=="ニセモノソウル"){
            nisemonosoul_count +=1;
          } 
        }

        //ウルトラソウルとニセモノソウルの数を比較しlabelに入力
        if (ultrasoul_count> nisemonosoul_count){
          label = "ウルトラソウル"
        }else{
          label = "ニセモノソウル"
        }
      }
```

Youtube Player API非常に便利なんですが、動画の読み込みが遅かったり「再生」を何回も実行したときに予期しない動作をすることが多いですね。
特にsetTimeoutを実装した場合、動画再生のメソッドを実行してからの経過時間を測るようなので、動画の読み込みにかかる時間は考慮してくれません。
今回はその部分をloadVideoByIdでendSecondsを指定することで解決しましたが、もっといろいろなやり方がありそうですね。
勉強してみたいところです。

コード全体はGist([こちらです](https://gist.github.com/canonno/6a34aaf21dccbc37a07a5ddeeafee62e))に載せてあるので、是非是非ご覧になってくださいね。
アプリは[こちらから！](https://competent-northcutt-5b8ae1.netlify.app/ultra_soul.html)。


#余談：なぜウルトラソウルに至ったか
ml5やTeachable Machineを利用して何か作りたいなあと考えて最初に思い浮かんだのが、あの[ズンドコキヨシ](https://qiita.com/shunsugai@github/items/971a15461de29563bf90)でした。
ズン・ズン・ズン・ズン・ドコといった独特のメロディを機械学習させて、__入力した「ズン」がどの「ズン」かを判定するアプリ__を作ろうと思っていました。
で実際実装してみたんですがモデルが全然使い物にならず、面白い結果にもならなそうであえなくボツにしたんです。
Youtubeで「ズン」だけを録音し続けるのつらかったです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ちなみに今回のボツ案「ズンドコキヨシ判定器」です <a href="https://t.co/LdQar9FXbO">pic.twitter.com/LdQar9FXbO</a></p>&mdash; canonno (@canonno_blog) <a href="https://twitter.com/canonno_blog/status/1301042026423545856?ref_src=twsrc%5Etfw">September 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

音楽部分だけ何かに代えられないか？と考え、かつ多くの人が知っていそうな音楽を考えたところ、「ウルトラソウル」が出てきました。
①で何を判定しようか・・・
②えニセモノソウルのゴロ良くね？
③よし決まり！
で決まりました。安直ですよね。

#今後やりたいこと
[ズンドコキヨシまとめ](https://qiita.com/shunsugai@github/items/971a15461de29563bf90)が作られるのと同じノリで__ウルトラソウル流行らないかなあ__とか思ったりしてます。
みんなノリノリになる名曲なので是非Youtube Player APIのハンズオンの時に使ってみてください・・・！

ここまで読んでいただきありがとうございました！
LGTMしていただけると励みになります、是非よろしくお願いします！
コメントもお待ちしております！