Arduinoで遊ぶページ

Arduinoで遊んだ結果を残すページです。
garretlab
WebブラウザでeVY1

概要

Webブラウザ上にピアノの鍵盤を表示して、鍵盤をマウスでクリックすると、クリックした鍵盤に対応する音を、eVY1から出します。

  • 楽器を変えたり、声を出すこともできるようにしました。ただし、「ん」の発音の扱いが怪しいです(摩擦音って?という状態です)。(2014/10/23)
    • 「ん」について少し教えていただきましたが、プログラムには反映できていません。(2015/5/30)
  • 複数のWebブラウザから接続して、(ほぼ)同時に音を出すこともできるはずです。(2014/10/23)
  • ボリューム調整をできるようにしてみました。(2014/10/25)

WebブラウザとArduinoの間の通信には、WebSocketを利用しました。そもそもWebSocketを使う必要性は全くありませんが…

eVY1の実験はこちら。WebSocektの実験はこちら。WebSocketについては、IEで動かないバグがあったのを修正しました。

作るもの

  • ピアノの鍵盤(JavaScriptで作成)
  • WebSocketサーバ(Arduino上に作成)

用意するもの

以下のものを利用しました。

  • Arduino
  • eVY1シールド
  • Ethernetシールド
  • LANケーブル
  • (USBケーブル)

基本的な考え方

とても簡単です。が、JavaScriptはよくわからないので、Webブラウザ上に鍵盤を表示させるのに苦労しました。変なグローバル変数の使い方をしています(JavaScriptのせいではなく、私のせいですが)。JavaScriptについての苦情はご勘弁を(改善案はぜひ教えてください)。

状況把握

Webブラウザ

Webブラウザ上で、鍵盤が押されたかどうかを判定します。JavaScriptでマウスイベントを捕捉しました。

処理決定

Webブラウザ

マウスが押された時は、鍵盤に対応する音を出すMIDIメッセージを、WebSocketを利用して、Arduinoに送信します。

マウスが離された時と、鍵盤から離れたときは、鍵盤に対応する音を消すMIDIメッセージを、Arduinoに送信します。

押された鍵盤の色を変更する処理も入れてみました。

機器操作

Arduino

WebSocketを通じて受信したMIDIメッセージを、そのまま、eVY1に送信します。

設計

ハードウェアの設計

特にありません。ArduinoにEthernetシールドとeVY1シールドを重ねるだけです。

EthernetシールドのLANケーブルのコネクタが少し高く、その上にさらにシールドを重ねると少し干渉します。

プログラムの設計

JavaScriptは解説するほど詳しくはないので、プログラムを見てください。

Arduinoのスケッチはとても簡単で、WebSocket経由で受信したデータを、そのまま、Serialに流し込むだけです。

なので、Javascriptを追加して、MIDIデータを送信するようにするだけで、いろいろな機能を追加することができます。

複数のWebブラウザから接続すると合奏もできるようになるかもしれません。WebSocketサーバが複数クライアントに対応していないので今はできませんが。

MIDIのプログラムチェンジメッセージは利用していないので、デフォルトでピアノの音色で演奏されます。

プログラム・スケッチ

HTML/JavaScript

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta name="GENERATOR" content="JustSystems Homepage Builder Version 15.0.12.0 for Windows">
<style type="text/css">
<!--
#controller1 {
  position:relative;
}
#keyboard {
  position:relative;
}
-->
</style>
<title>wsPiano</title>
<script type="text/javascript">
// ArduinoのIPアドレス
var wsUri = "ws://192.168.11.200/";
var channel = 1;
var group = 0;
var voice = 0;

// 白鍵で出力する音(MIDIの音階)。0x3cはド。
var whiteKeys = [0x3c, 0x3e, 0x40, 0x41, 0x43, 0x45, 0x47, 0x48, 0x4a, 0x4c, 0x4d, 0x4f, 0x51, 0x53, 0x54];
// 黒鍵で出力する音(MIDIの音階)。0xffは黒鍵を表示しない。0x3dはド#。
var blackKeys = [0x3d, 0x3f, 0xff, 0x42, 0x44, 0x46, 0xff, 0x49, 0x4b, 0xff, 0x4e, 0x50, 0x52, 0xff];

// 楽器
var instrumentGroup = ["Piano", "Chromatic Percussion", "Organ", "Guitar", "Bass", "Strings", "Ensemble", "Brass", "Reed", "Pipe", "Synth Lead", "Synth Pad", "Synth Effects", "Ethnic", "Percussive", "Sound effects"];
var instrument = new Object();
instrument["Piano"] = ["Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano", "Electric Piano1", "Electric Piano2", "Harpsichord", "Clavi cord"];
instrument["Chromatic Percussion"] = ["Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", "Xylophone", "Tubular Bells", "Dulcimer"];
instrument["Organ"] = ["Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ", "Reed Organ", "Accordion", "Harmonica", "Tango Accordion"];
instrument["Guitar"] = ["Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)", "Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Distortion Guitar"];
instrument["Bass"] = ["Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2"];
instrument["Strings"] = ["Violin", "Viola", "Cello", "Contrabass", "Tremolo String", "Pizzicato Strings", "Orchestral Harp", "Timpani"];
instrument["Ensemble"] = ["String Ensemble 1", "String Ensemble 2", "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit"];
instrument["Brass"] = ["Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", "Brass Section", "SynthBrass 1", "SynthBrass 2"];
instrument["Reed"] = ["Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet"];
instrument["Pipe"] = ["Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Shakuhachi", "Whistle", "Ocarina"];
instrument["Synth Lead"] = ["Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass + lead)"];
instrument["Synth Pad"] = ["Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)"];
instrument["Synth Effects"] = ["FX 1 (ice rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)"];
instrument["Ethnic"] = ["Sitar", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bag pipe", "Fiddle", "Shanai"];
instrument["Percussive"] = ["Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal"];
instrument["Sound effects"] = ["Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", "Applause", "Gunshot"];

// eVocaloid Phonetic Alphabet
var eVocaloidPA = { "あ":"a", "い":"i", "う":"M", "え":"e", "お":"o",
                  "か":"k a", "き":"k\' i", "く":"k M", "け":"k e", "こ":"k o",
                  "さ":"s a", "し":"S i", "す":"s M", "せ":"s e", "そ":"s o",
                  "た":"t a", "ち":"ts i", "つ":"ts M", "て":"t e", "と":"t o",
                  "な":"n a", "に":"J i", "ぬ":"n M", "ね":"n e", "の":"n o",
                  "は":"h a", "ひ":"C i", "ふ":"p\\ M", "へ":"h e", "ほ":"h o",
                  "ま":"m a", "み":"m\' i", "む":"m M", "め":"m e", "も":"m o",
                  "ら":"4 a", "り":"4\' i", "る":"4 M", "れ":"4 e", "ろ":"4 o",
                  "が":"g a", "ぎ":"g\' i", "ぐ":"g M", "げ":"g e", "ご":"g o",
                  "ざ":"dz a", "じ":"dZ i", "ず":"dz M", "ぜ":"dz e", "ぞ":"dz o",
                  "だ":"d a", "ぢ":"dZ i", "づ":"dz M", "で":"d e", "ど":"d o",
                  "ば":"b a", "び":"b\' i", "ぶ":"b M", "べ":"b e", "ぼ":"b o",
                  "ぱ":"p a", "ぴ":"p\' i", "ぷ":"p M", "ぺ":"p e", "ぽ":"p o",
                  "や":"j a", "ゆ":"j M", "よ":"j o",
                  "わ":"w a", "ゐ":"w i", "ゑ":"w e", "を":"o",
                  "ふぁ":"p\\ a", "つぁ":"ts a", 
                  "うぃ":"w i", "すぃ":"s i", "ずぃ":"dz i", "つぃ":"ts i" ,"てぃ":"t\' i",
                  "でぃ":"d' i", "ふぃ":"p\\\' i", "とぅ":"t M", "どぅ":"d M",
                  "いぇ":"j e", "うぇ":"w e", "きぇ":"k\' e", "しぇ":"S e", "ちぇ":"tS e",
                  "つぇ":"ts e", "てぇ":"t\' e", "にぇ":"J e", "ひぇ":"C e", "みぇ":"m\' e",
                  "りぇ":"4\' e", "ぎぇ":"g\' e", "じぇ":"dZ e", "でぇ":"d\' e", "びぇ":"b\' e",
                  "ぴぇ":"p' e", "ふぇ":"p\\ e",
                  "うぉ":"w o", "つぉ":"ts o", "ふぉ":"p\\ o",
                  "きゃ":"k\' a", "しゃ":"S a", "ちゃ":"tS a", "てゃ":"t\' a", "にゃ":"J a",
                  "ひゃ":"C a", "みゃ":"m\' a", "りゃ":"4\' a", "ぎゃ":"N\' a", "じゃ":"dZ a",
                  "でゃ":"d\' a", "びゃ":"b\' a", "ぴゃ":"p\' a", "ふゃ":"p\\\' a",
                  "きゅ":"k\' M", "しゅ":"S M", "ちゅ":"tS M", "てゅ":"t\' M", "にゅ":"J M",
                  "ひゅ":"C M", "みゅ":"m\' M", "りゅ":"4\' M", "ぎゅ":"g\' M", "じゅ":"dZ M",
                  "でゅ":"d\' M", "びゅ":"b\' M", "ぴゅ":"p\' M", "ふゅ":"p\\' M",
                  "きょ":"k\' o", "しょ":"S o", "ちょ":"tS o", "てょ":"t\' o", "にょ":"J o",
                  "ひょ":"C o", "みょ":"m\' o", "りょ":"4\' o", "ぎょ":"N\' o", "じょ":"dZ o",
                  "でょ":"d\' o", "びょ":"b\' o", "ぴょ":"p\' o",
                  "ん":"n",
}

// WebSocketで接続
var websocket = null;

// 鍵盤の大きさ(実際のピアノの鍵盤は以下の大きさ(比)らしい)
var whiteKeyWidth = 23;
var whiteKeyHeight = 150;
var blackKeyWidth = 10;
var blackKeyHeight = 100;

// 鍵盤の大きさの掛け率
var keyFactor = 2.0;

// 鍵盤をブラウザの端からどのくらい離すか
var keyTopOffset = 5;
var keyLeftOffset = 10;

// 鍵盤の色
var whiteKeyColor = "#ffffff";
var whiteKeyColorPushed = "#eeeeee";
var blackKeyColor = "#000000";
var blackKeyColorPushed = "#777777";

// 2バイトのMIDIコマンド列を送信する。
function channelMessage1(status, data1) {
        var data = new Uint8Array(2);
        data[0] = status;
        data[1] = data1;
        if (websocket) {
                websocket.send(data);
        }
}

// 3バイトのMIDIコマンド列を送信する。
function channelMessage2(status, data1, data2) {
        var data = new Uint8Array(3);
        data[0] = status;
        data[1] = data1;
        data[2] = data2;
        if (websocket) {
                websocket.send(data);
        }
}

function setChannel(object) {
        channel = object.selectedIndex;
        changeProgram();
}

function setVoice(object) {
        voice = Number(group) * 8 + Number(object.options[object.selectedIndex].value);
        changeProgram();
}

function changeProgram() {
        channelMessage1(0xc0 | channel, voice);
}

function setGroup(object) {
        var name = object.options[object.selectedIndex].text;
        var element = document.getElementById("instrument");

        group = object.selectedIndex;
        element.length = 0;

        for (var i = 0; i < instrument[name].length; i++) {
                element.length++;
                element.options[element.length - 1].value = i;
                element.options[element.length - 1].text = instrument[name][i];
        }
        voice = group * 8;
        changeProgram();        
}

function connectToServer() {
        wsUri = document.getElementById("server").value;
        if (websocket == null) {
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(openEvent) {
                        var element = document.getElementById("connectButton");
                        element.disabled = true;
                        element = document.getElementById("disconnectButton");
                        element.disabled = false;
                        changeProgram();
                        setVolume();
                }
                websocket.onclose = function(closeEvent) {
                        var element = document.getElementById("connectButton");
                        element.disabled = false;
                        element = document.getElementById("disconnectButton");
                        element.disabled = true;
                }
        }
}

function diconnectFromServer() {
        if (websocket != null) {
                websocket.close(1000);
                websocket = null;
        }
}

function getPA(index) {
        var s = lyrics.value;
        var current = s[index];
        var next = s[index + 1];

        if (next) { // 後ろに文字があったら、とりあえず2文字でマッチさせてみる
                if (eVocaloidPA[current + next]) {
                        return{kana: current + next, pa: eVocaloidPA[current + next], count: 2};
                }
        }
        return{kana: current, pa: eVocaloidPA[current], count: 1};
}

function pushString(buffer, string) {
        for (var i = 0; i < string.length; i++) {
                buffer.push(string[i].charCodeAt());
        }
}

function sendPhoneticAlphabet(add) {
        var count;
        var buffer = [0xf0, 0x43, 0x79, 0x09, 0x00, 0x50];
        buffer.push(0x10 | add);

// phonetic data from here
        for (var i = 0; i < lyrics.value.length; i += count) {
                var ret1 = getPA(i);
                count = ret1.count;
                if (ret1.pa) {
                        if (ret1.kana == "ん") {
                                var ret2 = getPA(i + count);
                                if (!ret2.pa) {
                                        pushString(buffer, "N\\");
                                } else if (ret2.pa.match(/^a|^i|^M|^e|^o|^j|^w/)) {
                                        pushString(buffer, "N\\");
                                } else if (ret2.pa.match(/^p'|^b'|^m'/)) {
                                        pushString(buffer, "m\'");
                                } else if (ret2.pa.match(/^p|^b|^m/)) {
                                        pushString(buffer, "m");
                                } else if (ret2.pa.match(/^k'|^g'|^n'/)) {
                                        pushString(buffer, "M\'");
                                } else if (ret2.pa.match(/^k|^g|^n/)) {
                                        pushString(buffer, "M");
                                } else if (ret2.pa.match(/^J/)) {
                                        pushString(buffer, "J");
                                } else {
                                        pushString(buffer, "n");
                                }
                        } else {
                                pushString(buffer, ret1.pa);
                        }
                        buffer.push(0x2c);      // デリミタ(,)
                }
        }
// end of data
        buffer.pop();  // 最後の','を取り除く。
        buffer.push(0x00);
        buffer.push(0xf7);
        var data = new Uint8Array(buffer);
        if (websocket) {
                websocket.send(data);
        }
}

function setVolume() {
        var vol = document.getElementById("volume").value;
        channelMessage2(0xb0 | channel, 0x07, vol);
}

</script> </head>
<body>
<div id="controller1">
  <table cellspacing="5">
    <tbody>
      <tr>
        <td>MIDI Server:</td>
        <td>
        <form id="midiServer"><input size="21" type="text" id="server" value=""><input type="button" id="connectButton" value="Connect" onclick="connectToServer()"><input type="button" id="disconnectButton" value="Disonnect" onclick="diconnectFromServer()" disabled></form>
        </td>
      </tr>
      <tr>
        <td>Channel:</td>
        <td><select id="channel" onchange="setChannel(this)">
        </select></td>
      </tr>
      <tr>
        <td>Voice:</td>
        <td><select id="instrumentGroup" onchange="setGroup(this)">
        </select> <select id="instrument" onchange="setVoice(this)">
        </select></td>
      </tr>
      <tr>
        <td>Volume:</td>
        <td><input type="range" id="volume" max="127" value="64" onclick="setVolume()"></td>
      </tr>
    </tbody>
  </table>
</div>
<div id="keyboard" style="width:auto;height:315px"></div>
<script type="text/javascript">
// Channel セレクトボックス
var element = document.getElementById("channel");
for (var i = 0; i < 16; i++) {
        element.length++;
        element.options[element.length - 1].value= i;
        element.options[element.length - 1].text= i;
        if (i == 1) {
                element.options[element.length - 1].selected = true;
        }
}

// instrumentGroup セレクトボックス
var element = document.getElementById("instrumentGroup");
for (var i = 0; i < instrumentGroup.length; i++) {
        element.length++;
        element.options[element.length - 1].value= i;
        element.options[element.length - 1].text= instrumentGroup[i];
}

// instrument セレクトボックス
var element = document.getElementById("instrument");
for (var i = 0; i < instrument["Piano"].length; i++) {
        element.length++;
        element.options[element.length - 1].value= i;
        element.options[element.length - 1].text= instrument["Piano"][i];
}

// wsserver テキストボックス
var element = document.getElementById("server");
element.value=wsUri;

// 白鍵を生成
for (var i = 0; i < whiteKeys.length; i++) {
        addKey(i, whiteKeys[i], whiteKeyWidth, whiteKeyHeight, keyLeftOffset , keyFactor, whiteKeyColor, whiteKeyColorPushed, 0);
}

// 黒鍵を生成
for (var i = 0; i < blackKeys.length; i++) {
        if (blackKeys[i] < 0x80) {
                addKey(i, blackKeys[i], blackKeyWidth, blackKeyHeight, keyLeftOffset , keyFactor, blackKeyColor, blackKeyColorPushed, 1);
        }
}

function addKey(num, note, width, height, offset, factor, color, colorPushed, isBlack) { 
        var element = document.createElement("div"); 

        element.id = note;
        element.style.position = "absolute";
        element.style.top = keyTopOffset + "px";
        element.style.width = width * factor + "px";
        element.style.height = height * factor + "px";
        element.style.backgroundColor = color; 
        element.style.border = "1px solid black";

        if (isBlack) {
                element.style.left = (whiteKeyWidth * keyFactor + 1) * (num + 1) - (blackKeyWidth * keyFactor / 2) + keyLeftOffset + "px";
        } else {
                element.style.left = (whiteKeyWidth * keyFactor + 1) * num + keyLeftOffset + "px";
        }

        document.getElementById("keyboard").appendChild(element);

        element.addEventListener("mousedown", (function(note){
                return function() {
                        // Key on
                        channelMessage2(0x90 | channel, note, 0x7f);
                        document.getElementById(note).style.backgroundColor = colorPushed;
                }
        })(note), false);
        element.addEventListener("mouseup", (function(note){
                return function() {
                        // Key off
                        channelMessage2(0x80 | channel, note, 0x7f);
                        document.getElementById(note).style.backgroundColor = color;
                }
        })(note), false);
        element.addEventListener("mouseout", (function(note){
                return function() {
                        // Key off
                        channelMessage2(0x80 | channel, note, 0x7f);
                        document.getElementById(note).style.backgroundColor = color;            }
        })(note), false);
} 

</script>
<div id="controller2">
  <table border="0">
    <tbody>
      <tr>
        <td>Lyrics:</td>
        <td><input size="40" type="text" id="lyrics" value=""><input type="button" id="sendPhoneticAlphabetNew" value="New" onclick="sendPhoneticAlphabet(0)"><input type="button" id="sendPhoneticAlphabetAdd" value="Add" onclick="sendPhoneticAlphabet(1)"></td>
      </tr>
    </tbody>
  </table>
</div>
</body>
</html>

Arduino

WebSocket関連のプログラムは、こちらを参照してください。

#include <SPI.h>
#include <Ethernet.h>
#include "WebSocket.h"

WebSocket wsServer(80, "midi", NULL, messageHandler, NULL, NULL);

void messageHandler(char *message, int payloadLength, int clientId) {
  for (int i = 0; i < payloadLength; i++) {
    Serial.write((short)message[i] & 0xff);
  }
}

void setup() {
  byte macAddress[] = {0x90, 0xa2, 0xda, 0x0d, 0xd2, 0xef};
  byte ipAddress[] = {192, 168, 11, 200};

  Serial.begin(31250);
  delay(5000);
  Ethernet.begin(macAddress, ipAddress);
  wsServer.begin();
}

void loop() {
  // put your main code here, to run repeatedly:
  int clientId;
  wsServer.available(&clientId);
}

組立

特にありません。ArduinoにEthernetシールドとeVY1シールドを重ねるだけです。

動作している様子

画面だけ表示してもうれしくも、意味もありませんが、一応キャプチャを表示しておきます。チャンネルを0にすると、声が出ます。デフォルトは、「あ」です。

バージョン

Arduino-1.6.4/Arduino Uno

JavaScriptはFirefox 33.0とIE11で動作は確認しました。Firefoxでは、音はなりますが、画面の更新タイミングが少し変です。私のプログラムが悪いのかもしれません。



メニューを表示するためにJavaScriptを有効にしてください。

Arduinoで遊ぶページ
Copyright © 2014 garretlab all rights reserved.
inserted by FC2 system