Flash : MIDI機器とFlashを連携(その1)
- Posted at: 2009年1月22日 16:19
- Update: 2009年2月13日 04:52

KORGのnanoPADとnanoKONTROLが安価でかわいかったので衝動買い。でも音モノは全然やらないんで、MIDI音源やMIDI機器をFlashを連携させて、インスタレーション用の入力機器にして遊んでみました。というか、前もやったネタなんだけど詳しく解説しときます。
構成は下の図な感じ。MIDIと繋ぐには、AIRなVJアプリ「Visp」のライブラリを流用します。VJアプリとしてはもひとつな感じだけど、MIDI機器を入力にしてムービーをコントロールできる機能があります。例のごとくFlash(AIR)自身では、他の機器にアクセスできません。なもんでVispにはMIDI機器にアクセスするためのゲートウェイ「MIDI Proxy」が用意されています。MIDI ProxyはJavaアプリです。JavaのMIDIライブラリから、MIDI機器から送られるMIDIメッセージ受け取り、XMLに変換します。FlashとはXMLSocketで通信させます。そしてASのライブラリでデータをパースして、各MIDIイベントに対応したイベントを通知します。あとはそのイベントを拾って対応させたデータを反映させるという感じ。

準備と実装するよ
VispのサイトからソースとMIDI Proxyのバイナリをダウンロードしてきます。ライブラリはflex_src/com/以下のファイルをコピーしてきます。あとMIDI機器をPC上で使えるようにセッティング。今回はnanoPAD、nanoKONTROLでやってきます。OS Xの場合、そのままだとMIDI機器をJavaが認識できないんで、Mandolane MIDI SPIかmmjを、Javaの拡張ディレクトリ(/Library/Java/Extensions/)にインストールしときます。
インプットマネージャーを実装するよ
元がAIRなだけに設定保存とかFlashには余分なとこは省いていきます。インプットマネージャー(com/visp/input/InputManager.as)あたりをいじっていけば実装できるはず。MIDIマネージャーを生成→MIDIイベントに対応したイベントを登録→入力があったらイベントを発行/取得→さらにMIDIイベントによって動作別のイベント(RANGE、BANGとか)をチェーン→動作別のイベントを通知したコントロールナンバーやノートを取得→データを反映。となるように実装します。
package {
/**
* MIDI2AS00.as
*
* nanoKONTROL から入力して箱を動かすよ。
*
**/
import com.visp.events.RangeEvent;
import com.visp.midi.ControllerEvent;
import com.visp.midi.MidiManager;
import flash.display.Sprite;
public class MIDI2AS00 extends Sprite
{
private var _midiMgr:MidiManager;
private var _rect:Sprite;
public function MIDI2AS00()
{
// 動かすものを用意
this._rect = new Sprite();
this._rect.graphics.beginFill(0xFF0000);
this._rect.graphics.drawRect(0, 0, 100, 100);
this._rect.graphics.endFill();
this.addChild(this._rect);
// MIDIマネージャーを作成
this._midiMgr = MidiManager.getInstance();
this._midiMgr.initialize();
// コントールチェンジのイベントを登録
this._midiMgr.addEventListener(ControllerEvent.CONTROLLER, _handleController);
// レンジイベントを登録
this.addEventListener(RangeEvent.RANGE, _handleRange);
}
// コントロールチェンジ(MIDIイベント)発生したら
private function _handleController(e:ControllerEvent) : void
{
// id:コントローラーナンバー(スライダーとか)
// value:その値
if(e.id) {
// レンジイベントを発行して数字の範囲を丸める
dispatchEvent(new RangeEvent(RangeEvent.RANGE, e.id, e.value, true));
}
}
// レンジイベントが発生したら
private function _handleRange(e:RangeEvent):void
{
// コントロールナンバーをキーにして動きをつける
switch(e.id) {
case 2:
this._rect.x = 255 * e.value + 100;
break;
case 3:
this._rect.y = 255 * e.value + 100;
break;
case 14:
this._rect.rotation = 360 * e.value;
break;
}
//trace(e.id, e.value);
}
}
}
nanoPAD、nanoKONTROLのスライダーやパッドのコントロールナンバー、ノートナンバー、MIDIイベント等は、KORGから配布されているnanoシリーズ用の「KORG KONTROL Editor」で確認と変更ができます。
使ってみる
大阪てら子19でちょっとだけデモったやつを置いときますよっと。
nanoKONTROLを使ったサンプル
スライダーの1〜6ではちゅねがグルグル回る。だけ。カメラをぐるぐる回すはずが、よくわからんかったので放置。ミキサー卓ごっこができそう。
package {
/**
* MIDI2AS02.as
*
* nanoKONTROLから入力してはちゅねを動かすよ。
*
**/
import com.visp.events.RangeEvent;
import com.visp.midi.ControllerEvent;
import com.visp.midi.MidiManager;
import org.papervision3d.view.stats.StatsView;
import flash.display.Sprite;
[SWF(width='800',height='600',backgroundColor='0x000000',frameRate='60')]
public class MIDI2AS02 extends Sprite
{
private var _midiMgr:MidiManager;
private var _3dObj:MikuPv3dObject;
public function MIDI2AS02()
{
// 3Dオブジェクトを配置
this._3dObj = new MikuPv3dObject();
this.addChild(_3dObj);
var _stats:StatsView = new StatsView(this._3dObj.renderer);
this.addChild(_stats);
// MIDIマネージャーを作成
this._midiMgr = MidiManager.getInstance();
this._midiMgr.initialize();
// コントロールチェンジのイベントを登録
this._midiMgr.addEventListener(ControllerEvent.CONTROLLER, _handleController);
// RANGEイベントを登録
this.addEventListener(RangeEvent.RANGE, _handleRange);
}
// コントロールチェンジ(MIDIイベント)発生したら
private function _handleController(e:ControllerEvent) : void
{
/**
* id:コントローラーナンバー(スライダーとか)
* value:その値値
**/
if(e.id) {
// レンジイベントを発行して数字の範囲を丸める
dispatchEvent(new RangeEvent(RangeEvent.RANGE, e.id, e.value, true));
}
}
private function _handleRange(e:RangeEvent):void
{
// コントロールナンバーをキーにして動きをつける
switch(e.id) {
case 2:
this._3dObj.setTracking(e.value, 0, 0, 0, 0, 0);
break;
case 3:
this._3dObj.setTracking(0, e.value, 0, 0, 0, 0);
break;
case 4:
this._3dObj.setTracking(0, 0, e.value, 0, 0, 0);
break;
case 5:
this._3dObj.setTracking(0, 0, 0, e.value, 0, 0);
break;
case 6:
this._3dObj.setTracking(0, 0, 0, 0, e.value, 0);
break;
case 8:
this._3dObj.setTracking(0, 0, 0, 0, 0, e.value);
break;
}
//trace(e.id, e.value);
}
}
}
// 3Dオブジェクトのクラス
import flash.display.*;
import flash.events.*;
import org.papervision3d.objects.parsers.Collada;
import org.papervision3d.view.BasicView;
import org.papervision3d.view.stats.StatsView;
class MikuPv3dObject extends BasicView
{
private var _rotationX:Number;
private var _rotationY:Number;
private var _rotationZ:Number;
private var _x:Number;
private var _y:Number;
private var _z:Number;
private var _cmodel:Collada;
private var rot:Number;
public function MikuPv3dObject()
{
//viewportの定義とカメラタイプ定義
//super (0,0,true,false,"CAMERA3D");
super (0,0,true,false,"Target");
init();
init3D();
}
public function init():void
{
this._rotationX = 0;
this._rotationY = 0;
this._rotationZ = 0;
this._x = 0;
this._y = 0;
this._z = 0;
}
public function init3D():void
{
//カメラ設定
camera.z = -300;
camera.focus = 570;
camera.zoom = 2;
// はちゅね召還
this._cmodel = new Collada("negimiku.dae");
this._cmodel.scale = 0.05;
this.scene.addChild(_cmodel);
//レンダリング開始
startRendering();
}
// トラッキング情報を設定
public function setTracking(vYaw:Number, vRoll:Number, vPitch:Number, vx:Number, vy:Number, vz:Number):void
{
if(vYaw > 0) {
this._rotationY = int(vYaw * 360);
}
if(vRoll > 0) {
this._rotationZ = int(vRoll * 360);
}
if(vPitch > 0) {
this._rotationX = int(vPitch * 360);
}
if(vx > 0) {
this._x = int(vx * 360) - 180;
trace(this._x);
}
if(vy > 0) {
this._y = int(vy * 360) - 180;
}
if(vz > 0) {
this._z = int(vz * 360) - 480;
}
}
// オブジェクトの位置を更新
override protected function onRenderTick(event:Event=null):void
{
this._cmodel.rotationX = this._rotationX;
this._cmodel.rotationY = this._rotationY;
this._cmodel.rotationZ = this._rotationZ;
this.camera.x = this._x;
this.camera.y = this._y;
this.camera.z = this._z;
super.onRenderTick(event);
}
}
nanoPADを使ったサンプル
BOX2D触ってみたかったんで、パッドを叩いたらボールがはねるってのを作るはずが、ただ落ちるだけに。。1〜4のパッドを叩くと丸、三角、四角、棒、星の形のオブジェクトが落ちてきます。叩く強さによって大きさが変わります。太鼓の達人とかできそう。
package {
/**
* MIDI2AS01.as
*
* nanoPADから入力していろいろを降らすよ。
*
**/
import com.visp.events.BangEvent;
import com.visp.midi.MidiManager;
import com.visp.midi.NoteEvent;
import flash.display.Sprite;
[SWF(width='800',height='600',backgroundColor='0x000000',frameRate='60')]
public class MIDI2AS01 extends Sprite
{
private var _midiMgr:MidiManager;
private var _b2Set:b2Set;
public function MIDI2AS01()
{
_b2Set = new b2Set();
this.addChild(_b2Set);
this._midiMgr = MidiManager.getInstance();
this._midiMgr.initialize();
this._midiMgr.addEventListener(NoteEvent.NOTE_ON, _handleNoteOn);
this._midiMgr.addEventListener(NoteEvent.NOTE_OFF, _handleNoteOff);
this.addEventListener(BangEvent.BANG_ON, _handleBangOn);
}
private function _handleNoteOn(e:NoteEvent) : void
{
if(e.pitch) {
var bangOn : BangEvent = new BangEvent(BangEvent.BANG_ON, e.pitch, true);
bangOn.velocity = e.velocity;
dispatchEvent(bangOn);
}
}
private function _handleNoteOff(e : NoteEvent) : void
{
if(e.pitch){
var bangOff : BangEvent = new BangEvent(BangEvent.BANG_OFF, e.pitch, true);
dispatchEvent(bangOff);
}
}
private function _handleBangOn(e:BangEvent):void
{
switch(e.id) {
case 39:
_b2Set.createBoxObject(0, e.velocity);
break;
case 48:
_b2Set.createBoxObject(1, e.velocity);
break;
case 45:
_b2Set.createBoxObject(2, e.velocity);
break;
case 43:
_b2Set.createBoxObject(3, e.velocity);
break;
case 51:
_b2Set.createBoxObject(4, e.velocity);
break;
}
}
}
}
// Box2Dのセット
import Box2D.Collision.Shapes.b2CircleDef;
import Box2D.Collision.Shapes.b2PolygonDef;
import Box2D.Collision.b2AABB;
import Box2D.Common.Math.b2Vec2;
import Box2D.Dynamics.b2Body;
import Box2D.Dynamics.b2BodyDef;
import Box2D.Dynamics.b2DebugDraw;
import Box2D.Dynamics.b2World;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.geom.Point;
class b2Set extends Sprite
{
// 物理エンジンの管理クラス
private var world:b2World;
// 円の中心
private var circleCenter:Point = new Point();
// 物理エンジン内の1mを表すためのピクセル数
private static const DRAW_SCALE:Number = 100;
public function b2Set()
{
var worldAABB:b2AABB = new b2AABB();
worldAABB.lowerBound.Set(-100, -100);
worldAABB.upperBound.Set(100, 100);
// 重力を定義する
var gravity:b2Vec2 = new b2Vec2(0, 10);
// 物理エンジン全体のセットアップ
world = new b2World(worldAABB, gravity, true);
// 床の場所を定義する
var floorBodyDef:b2BodyDef = new b2BodyDef();
floorBodyDef.position.Set(4, 5);
// 床の形を定義する
var floorShapeDef:b2PolygonDef = new b2PolygonDef();
floorShapeDef.SetAsBox(3.5, 0.1);
// 床を設置する
var floor:b2Body = world.CreateBody(floorBodyDef);
floor.CreateShape(floorShapeDef);
// 描画設定
var debugDraw:b2DebugDraw = new b2DebugDraw();
debugDraw.m_sprite = this;
debugDraw.m_drawScale = 100; // 1mを100ピクセルにする
debugDraw.m_fillAlpha = 0.3; // 不透明度
debugDraw.m_lineThickness = 1; // 線の太さ
debugDraw.m_drawFlags = b2DebugDraw.e_shapeBit;
world.SetDebugDraw(debugDraw);
addEventListener(Event.ENTER_FRAME, onEnterframe);
}
public function createBoxObject(shape:int, velocity:Number):void
{
var x:Number = (Math.random() * stage.stageWidth) / DRAW_SCALE;
var y:Number = 50 / DRAW_SCALE;
switch(shape) {
case 0:
var radius:Number = 0.9 * velocity;
var _ball:Ball = new Ball(x, y, radius, 0.8, 0.5);
_ball.setObject(world);
break;
case 1:
var hy:Number = 1.5 * velocity;
var _box1:Box = new Box(x, y, 0.1, hy, 0.8, 0.5);
_box1.setObject(world);
break;
case 2:
var len:Number = 1.2 * velocity;
var _triangle1:Triangle = new Triangle(x, y, len, len, 0.5);
_triangle1.setObject(world);
break;
case 3:
var _star1:Star = new Star(x, y, 0.8, 0.8, 0.5, world);
_star1.setObject(world);
break;
case 4:
var bsize:Number = 0.8 * velocity;
var _box2:Box = new Box(x, y, bsize, bsize, 0.8, 0.5);
_box2.setObject(world);
break;
}
}
private function onEnterframe(e:Event):void
{
if (world == null) {
return;
}
// Flashはデフォルトで秒間24フレームなので、
// 物理シミュレーションを1/24秒進める
world.Step(1 / 30, 10);
}
}
import Box2D.Dynamics.b2BodyDef;
import Box2D.Collision.Shapes.b2CircleDef;
import Box2D.Dynamics.b2Body;
import Box2D.Dynamics.b2World;
import Box2D.Collision.Shapes.b2PolygonDef;
import Box2D.Common.Math.b2Vec2;
import flash.geom.Point;
// いろんな形
class Ball
{
public var bodyDef:b2BodyDef;
public var shapeDef:b2CircleDef
public var body:b2Body;
public function Ball(x:Number, y:Number, radius:Number, density:Number, restitution:Number)
{
// 円の場所を設定する
bodyDef = new b2BodyDef;
bodyDef.position.Set(x, y);
// 円の大きさなどを設定する
shapeDef = new b2CircleDef();
shapeDef.radius = radius;
shapeDef.density = 0.8; // 密度 [kg/m^2]
shapeDef.restitution = 0.8; // 反発係数、通常は0〜1
}
public function setObject(target:b2World):void
{
body = target.CreateBody(this.bodyDef);
body.CreateShape(this.shapeDef);
body.SetMassFromShapes();
}
}
class Box
{
private var boxBodyDef:b2BodyDef;
private var boxShapeDef:b2PolygonDef;
private var boxBody:b2Body;
public function Box(x:Number, y:Number, hx:Number, hy:Number, density:Number, restitution:Number)
{
// 箱の場所を設定する
boxBodyDef = new b2BodyDef();
boxBodyDef.position.Set(x, y);
// 箱の大きさなどを設定する
boxShapeDef = new b2PolygonDef();
boxShapeDef.SetAsOrientedBox(hx, hy, new b2Vec2(0, 0), 0.8);
boxShapeDef.density = density; // 密度 [kg/m^2] 通常は1
boxShapeDef.restitution = restitution; // 反発係数、通常は0〜1
}
public function setObject(target:b2World):void {
boxBody = target.CreateBody(boxBodyDef);
boxBody.CreateShape(boxShapeDef);
boxBody.SetMassFromShapes();
}
}
class Triangle {
public var triangleBodyDef:b2BodyDef;
public var triangleShapeDef:b2PolygonDef;
public var triangleBody:b2Body;
public function Triangle(x:Number, y:Number, size:Number, density:Number, restitution:Number)
{
// 三角形の場所を設定する
triangleBodyDef = new b2BodyDef();
triangleBodyDef.position.Set(x, y);
// 三角形の大きさなどを設定する
triangleShapeDef = new b2PolygonDef();
triangleShapeDef.vertexCount = 3;
triangleShapeDef.vertices[0].Set(0, 0);
triangleShapeDef.vertices[1].Set(size, 0);
triangleShapeDef.vertices[2].Set(0, size);
triangleShapeDef.density = density; // 密度 [kg/m^2] 通常は1
triangleShapeDef.restitution = restitution; // 反発係数、通常は0〜1
triangleShapeDef.friction = 0.1; // 摩擦係数
}
public function setObject(target:b2World):void
{
triangleBody = target.CreateBody(triangleBodyDef);
triangleBody.CreateShape(triangleShapeDef);
triangleBody.SetMassFromShapes();
}
}
class Star
{
public var starBodyDef:b2BodyDef;
public var starShapeDef:b2PolygonDef;
public var starBody:b2Body;
public function Star(x:Number, y:Number, size:Number, density:Number, restitution:Number, target:b2World)
{
// 星の場所を設定する
starBodyDef = new b2BodyDef();
starBodyDef.position.Set(x, y);
starBody = target.CreateBody(starBodyDef);
// 星の大きさなどを設定する
starShapeDef = new b2PolygonDef();
starShapeDef.vertexCount = 3;
starShapeDef.vertices[0].Set(0.45, 0);
starShapeDef.vertices[1].Set(0.62, 0.50);
starShapeDef.vertices[2].Set(0.29, 0.50);
starShapeDef.density = density;
starShapeDef.friction = 0.5;
starShapeDef.restitution = restitution;
starBody.CreateShape(starShapeDef);
starShapeDef = new b2PolygonDef();
starShapeDef.vertexCount = 3;
starShapeDef.vertices[0].Set(0.90, 0.34);
starShapeDef.vertices[1].Set(0.50, 0.66);
starShapeDef.vertices[2].Set(0.40, 0.34);
starShapeDef.density = density;
starShapeDef.friction = 0.5;
starShapeDef.restitution = restitution;
starBody.CreateShape(starShapeDef);
starShapeDef = new b2PolygonDef();
starShapeDef.vertexCount = 3;
starShapeDef.vertices[0].Set(0.58, 0.40);
starShapeDef.vertices[1].Set(0.73, 0.90);
starShapeDef.vertices[2].Set(0.32, 0.60);
starShapeDef.density = density;
starShapeDef.friction = 0.5;
starShapeDef.restitution = restitution;
starBody.CreateShape(starShapeDef);
starShapeDef = new b2PolygonDef();
starShapeDef.vertexCount = 3;
starShapeDef.vertices[0].Set(0.32, 0.40);
starShapeDef.vertices[1].Set(0.58, 0.60);
starShapeDef.vertices[2].Set(0.17, 0.90);
starShapeDef.density = density;
starShapeDef.friction = 0.5;
starShapeDef.restitution = restitution;
starBody.CreateShape(starShapeDef);
starShapeDef = new b2PolygonDef();
starShapeDef.vertexCount = 3;
starShapeDef.vertices[0].Set(0.50, 0.34);
starShapeDef.vertices[1].Set(0.40, 0.66);
starShapeDef.vertices[2].Set(0, 0.34);
starShapeDef.density = density;
starShapeDef.friction = 0.5;
starShapeDef.restitution = restitution;
starBody.CreateShape(starShapeDef);
}
public function setObject(target:b2World):void
{
starBody.SetMassFromShapes();
}
}
とりあえずつながったけど。。
いつもの連携ネタですね。今度はMIDI。入力データにMIDIメッセージが使えるのがミソ。ゲートウェイにJavaを使うんでMac/Winで使えるのねん。でもいろいろと問題が山積み。
・同時に発生したイベントが送れない?→MIDI Proxy側をいじらないとだめぽい。
・1チャンネルしか使えない?→同上
・出力は?→機材がないから試してない。→たぶんむり。
・遅い→TCPが遅い?。UDP使えたら速くなるかなぁ。→Stratus。
んー気が向けば手を入れてきます。。
関連エントリ:
- (その1) - 準備とか実装
- (その2) - キーボードも使ってみる
参考資料: