Web MIDI APIをいじってみるついでになんか作ってみた

  • 1
    いいね
  • 0
    コメント

あらすじ

アドベントカレンダーやってみたい

Web MIDI APIいじってみたいからそれ書こう

せっかくなので手持ちのDJコントローラ使おう

(中略)

んじゃ、YoutubeでVJできるやつ作ってみよう

という流れでハッカソン形式でYoutubeでVJできるやつを作っています(現在進行系)

注:Web MIDI APIの話というより、それを使って何か作ってみたという報告になります。

はじめに

詳しくは知らないですが、VJってのは音楽にあわせて動画をいい感じに再生する人を指すみたいです。
で、その作業の一部分を切り抜くと、こういう作業があるみたいです。(そうじゃない人もいるっていうのは承知の上で)

  1. 曲に合いそうな動画を選ぶ
  2. 複数の動画を重ねる
  3. 動画にエフェクトをかける

1はYoutubeだから選び放題!(コピーなライツとかをインフリンジしないように!)
2はYoutubeのプレイヤーを2つ用意して重ねればヨイ!
3はCSSのfilterでお茶を濁す!
という感じで、なんかいけそうですね。

MIDIについて

細かい説明に入る前に、MIDIの説明を軽くやります。
むしろ、ここがメインコンテンツなので、それよりも下は流し見して頂けるだけでも感謝です。

ざっくーりとMIDIについて書くとすると、語弊がありそうですが、電子楽器を制御するためのプロトコルです。
電子楽器の動作を表現するためのものなので、それらと照らし合わせながら考えるとしっくりくると思います。

MIDIメッセージ

MIDIのデータ送受信は「鍵盤を押した」、「鍵盤を離した」、「ツマミをひねった」という動作の単位をメッセージとしてやりとりします。
メッセージは、

  • ステータスバイト
  • 第一データバイト
  • 第二データバイト

の3つのバイトで構成されます。
ステータスバイトで後述のデータの種類を定義して、それに付随するデータを後の2つのデータバイトで定義します。
データバイトは上位1バイトがステータス、下位1バイトがチャンネルになります。
MIDI機器はSCSIのようにデイジーチェーンができて、同時に複数台あつかえます。
PCDJでいえば、複数のコントローラをつなげたい時とかに機器を識別するためにチャンネルを使います。
今回は1台だけを想定しているので気にしません。

ここからステータスバイトの説明になります。
いろいろ種類がありますが、今回はMixtrackというNumarkのDJコントローラを使うので、

  • ノブ(ひねるやつ)
  • ボタン
  • フェーダー(スライドするやつ)

しかありません。
なので、以下の3つのステータスを覚えておけば問題ないです。

ノートオン

ステータスバイトは0x9nで、鍵盤を押したときの信号です。
第一データバイトはノート番号(鍵盤でいうところの音階)を表します。
第二データバイトはベロシティー(鍵盤でいうところの押した時の強さ)を表します。
ベロシティーはMOMOのVelocity of soundでもあるように速度という意味ですが、鍵盤の叩く速さ=強さという意味なんだとか。

ノートオフ

ステータスバイトは0x8nで、鍵盤を離したときの信号です。
データバイトはノートオンと同じです。

コントロールチェンジ

ステータスバイトは0xBnで、音量などのツマミやフェーダーを動かしたときの信号です。
第一データバイトはコントロール番号(どのツマミやフェーダーか)を表します。
第二データバイトはコントロール値で、どれくらいの量かを表します。

以上がざっくりとした説明です。

ウェブページの用意

今回はMIDI関連のお話をメインにする(のと、html5+VJで検索すると大先輩がたの功績がいっぱい出てくる)ので、簡単に書きます。
用意するのは

  • プレイヤー用ページ
  • コントローラ用ページ

の2つです。
分ける理由としてはプロジェクターに映すことを考えて、プレイヤー側を別ウィンドウにして全画面で表示させるためです。

コントローラ用ページを開いたらプレイヤー用ページが開くっていう仕組みにするのですが、こいつが厄介です。

var player = window.open('./player.html', 'player');

で開くのですが、セキュリティうんちゃかで、

Uncaught DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.

こういうエラーがでます。
同じドメインにする必要があるので、localhostで動かすべくApache等々を動かして開発しませう。

プレイヤー側

こちらは、Youtube > IFrame Player APIのリファレンスを参考に、divタグを2つ用意して、onYouTubeIframeAPIReadyを呼び出す時に各々のdivに別々のプレイヤーを憑依させます。

<div id="fx">
  <div id="deck_a"></div>
  <div id="deck_b"></div>
</div>

こんなのがあったら、以下のJSをscriptタグの間に挟みます。

var tag = document.createElement('script');

tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

var deck_a;
var deck_b;
// divにiframeを憑依させるやつ
function onYouTubeIframeAPIReady() {
  deck_a = new YT.Player('deck_a', {
    height: '360',
    width: '640',
    videoId: '(動画ID)',
    events: {
      'onReady': onPlayerReadyDeckA,
      'onStateChange': onPlayerStateChangeDeckA
    },
    playerVars: {
      controls: 0,
      rel: 0
    }
  });
  deck_b = new YT.Player('deck_b', {
    height: '360',
    width: '640',
    videoId: '(動画ID)',
    events: {
      'onReady': onPlayerReadyDeckB,
      'onStateChange': onPlayerStateChangeDeckB
    },
    playerVars: {
      controls: 0,
      rel: 0
    }
  });
}

// 音を出さずにロードが終わったら再生させる
function onPlayerReadyDeckA(event) {
  event.target.mute();
  event.target.playVideo();
}

// ここがポイント!!!
function onPlayerStateChangeDeckA(event) {
  if (event.data == YT.PlayerState.ENDED) {
    deck_a.seekTo(0, true);
    deck_a.playVideo();
  }
}

function onPlayerReadyDeckB(event) {
  event.target.mute();
  event.target.playVideo();
}

function onPlayerStateChangeDeckB(event) {
  if (event.data == YT.PlayerState.ENDED) {
    deck_b.seekTo(0, true);
    deck_b.playVideo();
  }
}

そうすると、2つのプレイヤーが立ち上がり、各々の動画が再生されます。
ここがポイント!!!とコメントが書かれている
onPlayerStateChangeDeckA
ですが、苦肉の策で入れています。

VJ素材は短い動画を繰り返すものが一般的なようで、ループ機能が必須です。
しかし、Youtubeプレイヤーはループに対して

  • 再生リストが必須
  • ループ時に動画をリロードする

という特性があり、ループ時にローディングのくるくるが表示されてしまいます。
このため、動画が終了したイベントを拾って0秒のポイントにシークさせて再生するという方法でしのいでいます。
それでもシークはバッファリングが走るため、一瞬暗くなります。あきらめました。

deck_a、deck_bのdivをfxというdivでラップしてますが、これはフィルター(エフェクター)用です。
フィルターについてはコントローラ側で説明します。

上記がざっくりとしたプレイヤー側のお話です。

コントローラ側

MIDIの部分は、
アホみたいなWeb MIDI APIのラッパー - Qiita
を使わせていただいたので、メインはどの信号を受けたらどう操作するか、という条件分岐がメインになります。

gitにある私の汚くてセンスのかけらもないコードを見ていただければ、雰囲気はつかめると思います。
一応ポイントですが、

ポイント1

MIDIを受けた時のイベントで

message = e.data[0] & 0xf0;

とあります。
メッセージは0x9nのようにnの部分はチャンネルです。
0xf0でマスクすることでチャンネル部分を削ぎ落として、純粋にステータスだけを読めるようにします。

ポイント2

私の使用しているNumark Mixtrackは、押したボタンを離した時にノートオフを送信せず、「ノートオン+ベロシティー0」をノートオフとしています。
MenZ-DECKというものを作ったときに調べましたが、これも実装としてはアリみたいなのでそういうものだと思って条件分岐を作っています。
ホントなら、

if (message === 0x80 || (message === 0x90 && velocity === 0x00))

とかで条件を用意したほうが丁寧だと思います。
勢いで作っているので、そんな感じがにじみ出ています。すみません。

ポイント3

MIDIのベロシティーやコントロール値は0〜127になります。
一方で、プレイヤーのCSSをいじってエフェクトなりフェードなりをかけています。
これを変換するため、conversion_valueという関数を作っています。
定番な感じもしますが、割り算を理解できないまま小学校を卒業してしまった私には覚えられない(割によく使う)ので、ここもポイントとしました。

さいごに

勢いでつくりましたが、いちおうこんな感じになりました。
スクリーンショット 2016-12-11 13.17.24.png
わかりづらいですが、2つの動画が重なっています。
※動画は
FREE Time Lapse: Clouds and Blue Sky (HD)
FREE Slow motion video:Water drops
を使用しています。

ちなみに、上で名前が出ていましたが、Numark Mixtrackはこんなやつです。
mixtrack.jpg
シンプルかつ要所を押さえたボタン、片手で振り回せるくらいの軽量さがとても好きです。
後継機でこういうやつが出ていないので少し残念です。
ノブが取れてたりするのは、5年間のハードユースの結果です。

それと、とんでもない感じのコードですがgithubにあげました。
aminosan/VaJube

できれば動いている動画をアップロードしたいのですが、今から艦これ的なやつを観に行くので、後日やります。本当です。
あと、毎年友人宅でやっている、たまに全裸で踊るクリスマスパーティーでも使おうかと思います。

それではWeb MIDI APIと共に良いクリスマスを。