yoppa.org


openFrameworksで、Leap Motionを使ってみた: その2 – Leap Motion SDKから情報を取得

screenshot_651

前回のエントリーが好評だったので、さらにLeap Motionについて。

前回紹介したように、ofxLeapMotionでは、あらかじめ用意されているofxLeapMotionSimpleHandクラスを使用すると、とても簡単に手の座標などを取得できる。とはいえ、Leap MotionのSDKで用意されているフルの機能を活用できるところまでは充実していないよう。

しかし、ofxLeapMotionでは、Leap Motion SDKの情報を直接取得できる方法も用意されている。これを使用するとより多彩な情報が取得可能。詳しくは、Leap Motionから提供されているDocumentationを参照。

下記のサンプルでは、まずLeapの手(Hand)を取得して、それぞれの指の位置を取得したあと、指全体で包みこむ仮想の球体の情報を取得して表示している。

やってみると、直接SDKを参照する方法でもかなりシンプルに記述できるので、ある程度慣れてきたらofxLeapMotionSimpleHandを使用するのではなくこちらの方法のほうが何かと便利かもしれない。

testApp.h

#pragma once

#include "ofMain.h"
#include "ofxLeapMotion.h"

class testApp : public ofBaseApp{
  
public:
  void setup();
  void update();
  void draw();
  
  void keyPressed  (int key);
  void keyReleased(int key);
  void mouseMoved(int x, int y );
  void mouseDragged(int x, int y, int button);
  void mousePressed(int x, int y, int button);
  void mouseReleased(int x, int y, int button);
  void windowResized(int w, int h);
  void dragEvent(ofDragInfo dragInfo);
  void gotMessage(ofMessage msg);
  void exit();
  
  ofxLeapMotion leap; // Leap Motionのメインクラスをインスタンス化
  // vector <ofxLeapMotionSimpleHand> simpleHands; // シンプルな手のモデルのvector配列
  ofEasyCam cam; //カメラ
  ofLight light; //ライト
  vector <ofVec3f> fingerPos; // 指の位置の配列
  vector <ofVec3f> spherePos; // 手が取り囲む球体の位置の配列
  vector <float> sphereSize;  // 手が取り囲む球体の大きさの配列
};

testApp.cpp

#include "testApp.h"

void testApp::setup(){
  // 画面設定
  ofSetFrameRate(60);
  ofSetVerticalSync(true);
  ofBackground(31);
  // 照明とカメラ
  ofEnableLighting();
  light.setPosition(200, 300, 50);
  light.enable();
  cam.setOrientation(ofPoint(-20, 0, 0));
  // GL設定
  glEnable(GL_DEPTH_TEST);

  // Leap Motion開始
  leap.open();
}

void testApp::update(){
  // Leap Motion SDKで用意されている手(Hand)のクラスを取得してvector配列へ
  vector <Hand> hands = leap.getLeapHands();
  
  // 手が検出されたら
  if( leap.isFrameNew() && hands.size() ){
    // vector配列に記憶した座標をクリア
    fingerPos.clear();
    spherePos.clear();
    sphereSize.clear();
    
    // 画面の大きさにあわせて、スケールをマッピング
    leap.setMappingX(-230, 230, -ofGetWidth()/2, ofGetWidth()/2);
    leap.setMappingY(90, 490, -ofGetHeight()/2, ofGetHeight()/2);
    leap.setMappingZ(-150, 150, -200, 200);

    for(int i = 0; i < hands.size(); i++){
      // 指の位置を取得
      for(int j = 0; j < hands[i].fingers().count(); j++){
        ofVec3f pt;
        const Finger & finger = hands[i].fingers()[j];
        pt = leap.getMappedofPoint( finger.tipPosition() );
        fingerPos.push_back(pt);
      }
      
      // 指がとりかこむ球体を取得
      ofVec3f sp = leap.getMappedofPoint(hands[i].sphereCenter());
      float r = hands[i].sphereRadius();
      spherePos.push_back(sp);
      sphereSize.push_back(r);
    }
  }
  
  // ofxLeapMotionに現在のフレームは古くなったことを通知
  leap.markFrameAsOld();
}

void testApp::draw(){
  cam.begin();
  // 検出された指の数だけくりかえし
  for(int i = 0; i < fingerPos.size(); i++){
    // 検出された位置に球を描画
    ofSpherePrimitive sphere;
    sphere.setPosition(fingerPos[i].x, fingerPos[i].y, fingerPos[i].z);
    sphere.draw();
  }
  
  // 検出された指がとりかこむ球
  for(int i = 0; i < spherePos.size(); i++){
    // 検出された位置に球を描画
    ofSpherePrimitive sphere;
    sphere.setPosition(spherePos[i].x, spherePos[i].y, spherePos[i].z);
    sphere.setRadius(sphereSize[i]*1.5); //ここのスケールは今は目分量…
    sphere.draw();
  }
  cam.end();
}

openFrameworksで、Leap Motionを使ってみた!

待ちに待ったLeap Motionがようやく届いたので、ちょびっとだけ触ってみた。

インストールとセットアップはとにかく簡単。Leap Motionのセットアップページに行って、一式ダウンロードとインストールするだけで、すぐに動作する。デモアプリの完成度もなかなかのもの。ユーザーにストレスを感じさせない環境構築までのナビゲーションのスムーズさに唸らされる。

screenshot_647

ひと通り遊んだところで、openFrameworksからLeap Motionを使ってみることに。そのものズバリなofxLeapMotionというアドオンが開発されているので、それをそのまま利用できる。ただし、いくつかバージョンがありいくつか試した中で下記のリポジトリのバージョンが問題なくビルドできて、Leap Motionからのメッセージも受信できた。

まずは、Hello World的なサンプルとして、手を認識して表示してみる。

ofxLeapMotionでは2通りの方法が用意されている。1つ目は、ofxLeapMotion内で定義されているofxLeapMotionSimpleHandというシンプルな手のモデルを利用する方法。2つ目は、LeapMotionの純正のC++ SDKで用意されている機能にアクセスして表示する方法。

まずは、シンプルなofxLeapMotionSimpleHandを利用する方法で描画してみる。やり方はとても簡単で、ofxLeapMotionSimpleHandを認識したら、そのインスタンスに対してdebugDraw()というメソッドを呼びだせば簡単な手の3Dモデルを表示してくれる。

こんな感じ。

testApp.h

#pragma once

#include "ofMain.h"
#include "ofxLeapMotion.h"

class testApp : public ofBaseApp{

  public:
    void setup();
    void update();
    void draw();

    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    void dragEvent(ofDragInfo dragInfo);
    void gotMessage(ofMessage msg);
    void exit();

    ofxLeapMotion leap; // Leap Motionのメインクラスをインスタンス化
    vector <ofxLeapMotionSimpleHand> simpleHands; // シンプルな手のモデルのvector配列
    ofEasyCam cam; //カメラ
    ofLight light; //ライト
};

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    // 画面設定
    ofSetFrameRate(60);
    ofSetVerticalSync(true);
    ofBackground(31);
    // 照明とカメラ
    ofEnableLighting();
    light.setPosition(200, 300, 50);
    light.enable();
    cam.setOrientation(ofPoint(-20, 0, 0));
    // GL設定
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_NORMALIZE);
    // Leap Motion開始
    leap.open();
}

void testApp::update(){
    // 検出された手の数だけ、ofxLeapMotionSimpleHandのvector配列に追加
    simpleHands = leap.getSimpleHands();

    // フレーム更新して、手が検出されたら
    if( leap.isFrameNew() && simpleHands.size() ){
        // 画面の大きさにあわせて、スケールをマッピング
        leap.setMappingX(-230, 230, -ofGetWidth()/2, ofGetWidth()/2);
        leap.setMappingY(90, 490, -ofGetHeight()/2, ofGetHeight()/2);
        leap.setMappingZ(-150, 150, -200, 200);
    }

    // ofxLeapMotionに現在のフレームは古くなったことを通知
    leap.markFrameAsOld();
}

void testApp::draw(){
    // 検出された数だけ、手を描画
    cam.begin();
    for(int i = 0; i < simpleHands.size(); i++){
        simpleHands[i].debugDraw();
    }
    cam.end();
}

実行結果はこんな感じ。シンプルだけど、手の平と各指先、そして指先の動きのベクトルが表示されている。

screenshot_648

もちろん、ofxLeapMotionSimpleHandを利用して、指先や手の平などそれぞれのパーツの座標を個別にとり出すことも簡単。

testApp.h

#pragma once

#include "ofMain.h"
#include "ofxLeapMotion.h"

class testApp : public ofBaseApp{

  public:
    void setup();
    void update();
    void draw();

    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    void dragEvent(ofDragInfo dragInfo);
    void gotMessage(ofMessage msg);
    void exit();

    ofxLeapMotion leap; // Leap Motionのメインクラスをインスタンス化
    vector <ofxLeapMotionSimpleHand> simpleHands; // シンプルな手のモデルのvector配列
    ofEasyCam cam; //カメラ
    ofLight light; //ライト
    vector <ofVec3f> fingerPos;
};

testApp.cpp

include “testApp.h”

#include "testApp.h"

void testApp::setup(){
  // 画面設定
  ofSetFrameRate(60);
  ofSetVerticalSync(true);
  ofBackground(31);
  // 照明とカメラ
  ofEnableLighting();
  light.setPosition(200, 300, 50);
  light.enable();
  cam.setOrientation(ofPoint(-20, 0, 0));
  // GL設定
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_NORMALIZE);
  // Leap Motion開始
  leap.open();
}

void testApp::update(){
  // 検出された手の数だけ、ofxLeapMotionSimpleHandのvector配列に追加
  simpleHands = leap.getSimpleHands();
  
  // フレーム更新して、手が検出されたら
  if( leap.isFrameNew() && simpleHands.size() ){
    //指の位置をクリア
    fingerPos.clear();
    
    // 画面の大きさにあわせて、スケールをマッピング
    leap.setMappingX(-230, 230, -ofGetWidth()/2, ofGetWidth()/2);
    leap.setMappingY(90, 490, -ofGetHeight()/2, ofGetHeight()/2);
    leap.setMappingZ(-150, 150, -200, 200);
    
    // 検出された手の数だけくりかえし
    for(int i = 0; i < simpleHands.size(); i++){
      // 検出された指の数だけくりかえし
      for(int j = 0; j < simpleHands[i].fingers.size(); j++){
        // 位置をvectorに保存
        ofVec3f pos = simpleHands[i].fingers[j].pos;
        fingerPos.push_back(pos);
      }
    }
  }
  
  // ofxLeapMotionに現在のフレームは古くなったことを通知
  leap.markFrameAsOld();
}

void testApp::draw(){
  cam.begin();
  // 検出された指の数だけくりかえし
  for(int i = 0; i < fingerPos.size(); i++){
    // 検出された位置に立方体を描画
    ofBoxPrimitive box;
    box.setPosition(fingerPos[i].x, fingerPos[i].y, fingerPos[i].z);
    box.set(20);
    box.draw();
  }
  cam.end();
}

指先だけ独立して検出できた! 簡単!

screenshot_649

ジェスチャーの検知や、Leap Motionから提供されている独自のSDKを呼び出して使用するやり方は、また今度。とりあえず、Leap Motion楽しい! ということで…

07.25: コードを若干修正


Toucheセンサーを使う 4 – Touche for Arduinoで音を扱う2、複数のサウンドファイルの切り替え

今回は、前回のToucheセンサーで音を扱うという内容の補足をします。前回は、特定の時点でのセンサーの状態つまりジェスチャーと、現時点との違いをもとに、オシレーターの周波数を変化させたり、FM合成を試みてみました。

今回はさらに高度なジェスチャー検知と、サウンドファイルの再生を組合せ、特定のジェスチャーに反応して音が鳴るようにしてみたいと思います。

ここでポイントとなるのがジェスチャー認識の部分です。この部分をまっさらな状態から作ることは不可能ではないですが、なかなか大変です。そこで今回はTouche for Arduinoのサンプルプログラムを改造して、そこに音を鳴る仕組みを付加するという方法を採用してみました。

今回のサンプルの原型となったプログラムは下記からダウンロード可能です。

このサンプルでは、4つのジェスチャーを登録して、現在のセンサーの状態との距離を算出し、それによってボタンの色を塗りわけています。

複数のサンプルファイルを、ジェスチャーを判別して再生

下記のプログラムが、原型となったオリジナルのプログラムにサウンド再生機能を付加したサンプルです。

MinimのAudioPlayerを配列として定義して、ジェスチャーの状態によって、消音(ミュート)と再生(unmute)を切り替えることで再生する音を変更しています。

Processing_graph.pde

import ddf.minim.*;

// Minim関連
Minim minim;
AudioPlayer[] player = new AudioPlayer[4];

Graph MyArduinoGraph = new Graph(150, 80, 500, 300, color (31, 127, 255));
float[] gestureOne=null;
float[] gestureTwo = null;
float[] gestureThree = null;
float[][] gesturePoints = new float[4][2];
float[] gestureDist = new float[4];
String[] names = {
  "Nothing", "Touch", "Grab", "In water"
};

void setup() {
  //!!!!!!!!!!!!!!!!!!!!!!!!!!
  // ポート番号を必ず指定すること
  //!!!!!!!!!!!!!!!!!!!!!!!!!!
  PortSelected=1;

  size(1000, 500);

  //グラフ初期化
  MyArduinoGraph.xLabel="Readnumber";
  MyArduinoGraph.yLabel="Amp";
  MyArduinoGraph.Title=" Graph";
  noLoop();
  SerialPortSetup();

  //Minim初期化
  minim = new Minim(this);

  //サウンドファイル読込み
  player[0] = minim.loadFile("anton.aif");
  player[1] = minim.loadFile("cello-f2.aif");
  player[2] = minim.loadFile("cherokee.aif");
  player[3] = minim.loadFile("drumLoop.aif");
  //それぞれのプレーヤーをループ再生しミュートしておく
  for(int i = 0; i < player.length; i++){
    player[i].loop();
    player[i].mute();
  }
}

void draw() {
  background(255);

   //グラフ描画
   if ( DataRecieved3 ) {
    pushMatrix();
    pushStyle();
    MyArduinoGraph.yMax=1000;      
    MyArduinoGraph.yMin=-200;      
    MyArduinoGraph.xMax=int (max(Time3));
    MyArduinoGraph.DrawAxis();    
    MyArduinoGraph.smoothLine(Time3, Voltage3);
    popStyle();
    popMatrix();

    float gestureOneDiff =0;
    float gestureTwoDiff =0;
    float gestureThreeDiff =0;

    //ジェスチャーの比較
    float totalDist = 0;
    int currentMax = 0;
    float currentMaxValue = -1;
    for (int i = 0; i < 4;i++) {
      //  gesturePoints[i][0] = 
      if (mousePressed && mouseX > 750 && mouseX<800 && mouseY > 100*(i+1) && mouseY < 100*(i+1) + 50) {
        fill(255, 0, 0);
        gesturePoints[i][0] = Time3[MyArduinoGraph.maxI];
        gesturePoints[i][1] = Voltage3[MyArduinoGraph.maxI];
      }
      else {
        fill(255, 255, 255);
      }

      //それぞれのジェスチャーからの距離を算出
      gestureDist[i] = dist(Time3[MyArduinoGraph.maxI], Voltage3[MyArduinoGraph.maxI], gesturePoints[i][0], gesturePoints[i][1]);
      totalDist = totalDist + gestureDist[i];
      if (gestureDist[i] < currentMaxValue || i == 0) {
        currentMax = i;
        currentMaxValue =  gestureDist[i];
      }
    }
    totalDist=totalDist /3;

    for (int i = 0; i < 4;i++) {
      float currentAmmount = 0;
      currentAmmount = 1-gestureDist[i]/totalDist;
      if (currentMax == i) {
        fill(0, 0, 0);
        fill(currentAmmount*255.0f, 0, 0);

        //いったん全てのプレーヤーをミュート
        for(int j = 0; j < player.length; j++){
          player[j].mute();
        }

        //該当するプレーヤーのみミュートを解除
        player[i].unmute();

      }
      else {
        fill(255, 255, 255);
      }

      stroke(0, 0, 0);
      rect(750, 100 * (i+1), 50, 50);
      fill(0, 0, 0);
      textSize(30);
      text(names[i], 810, 100 * (i+1)+25);

      fill(255, 0, 0);
    }
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

gesture_sound01.jpg

サウンドの音量と画面の調整

さらにもう少し工夫してみましょう。

下記の例では、サウンドの切り替えを単純にON/OFFの切り替えではなく、ジェスチャーの距離に応じて音量(Gain)を変化させて、その一致度を表現しています。また、画面の表示も工夫して、ボタンや波形表示を改良しています。

Processing_graph.pde

import ddf.minim.*;

// Minim関連
Minim minim;
AudioPlayer[] player = new AudioPlayer[4];

//Graph MyArduinoGraph = new Graph(150, 80, 500, 300, color (20, 20, 200));
Graph MyArduinoGraph;
float[] gestureOne=null;
float[] gestureTwo = null;
float[] gestureThree = null;
float[][] gesturePoints = new float[4][2];
float[] gestureDist = new float[4];
String[] names = {
  "Nothing", "Touch", "Grab", "In water"
};

void setup() {
  //!!!!!!!!!!!!!!!!!!!!!!!!!!
  // ポート番号を必ず指定すること
  //!!!!!!!!!!!!!!!!!!!!!!!!!!
  PortSelected=0;

  size(1024, 768);
  MyArduinoGraph = new Graph(int(width*0.1), int(height*0.1), int(width/3*2-width*0.1), int(height/2-height*0.1), color(#3399ff));

  //グラフ初期化
  MyArduinoGraph.xLabel="Readnumber";
  MyArduinoGraph.yLabel="Amp";
  MyArduinoGraph.Title="Touche graph";
  noLoop();
  SerialPortSetup();

  //Minim初期化
  minim = new Minim(this);

  //サウンドファイル読込み
  player[0] = minim.loadFile("drumLoop.aif");
  player[1] = minim.loadFile("cherokee.aif");
  player[2] = minim.loadFile("cello-f2.aif");
  player[3] = minim.loadFile("anton.aif");

  for(int i = 0; i < player.length; i++){
    player[i].loop();
    player[i].mute();
  }
}

void draw() {
  background(255);

   //グラフ描画

   if ( DataRecieved3 ) {
    pushMatrix();
    pushStyle();
    MyArduinoGraph.yMax=1000;      
    MyArduinoGraph.yMin=-200;      
    MyArduinoGraph.xMax=int (max(Time3));
    MyArduinoGraph.DrawAxis();    
    MyArduinoGraph.smoothLine(Time3, Voltage3);
    popStyle();
    popMatrix();

    float gestureOneDiff =0;
    float gestureTwoDiff =0;
    float gestureThreeDiff =0;

    //Gesture compare
    float totalDist = 0;
    int currentMax = 0;
    float currentMaxValue = -1;
    for (int i = 0; i < 4;i++) {

      if (mousePressed && mouseX > width/4*3 && mouseX<width && mouseY > 100*i && mouseY < 100*(i+1)) {
        fill(255, 0, 0);
        gesturePoints[i][0] = Time3[MyArduinoGraph.maxI];
        gesturePoints[i][1] = Voltage3[MyArduinoGraph.maxI];
      }
      else {
        fill(255, 255, 255);
      }

      //それぞれのジェスチャーの距離を算出
      gestureDist[i] = dist(Time3[MyArduinoGraph.maxI], Voltage3[MyArduinoGraph.maxI], gesturePoints[i][0], gesturePoints[i][1]);
      totalDist = totalDist + gestureDist[i];
      if (gestureDist[i] < currentMaxValue || i == 0) {
        currentMax = i;
        currentMaxValue =  gestureDist[i];
      }
    }
    totalDist=totalDist /3;

    for (int i = 0; i < 4;i++) {
      float currentAmmount = 0;
      currentAmmount = 1-gestureDist[i]/totalDist;

      //全てのプレイヤーの音量を一旦下げる
      player[i].setGain(-120);

      if (currentMax == i) {
        fill(0, 0, 0);
        fill(31, 127, 255, currentAmmount*255.0f);

        //ジェスチャーとの距離に応じて音量(Gain)を算出
        float val = map(currentAmmount, 0, 1, -48, 0);
        //計算した音量を適用
        player[i].setGain(val);
      }
      else {
        fill(255, 255, 255);
      }

      stroke(0, 0, 0);

      rect(width/4*3, 100* i + 10, width/4-10, 90);
      fill(0, 0, 0);
      textSize(20);
      text(names[i], width/4*3+20, 100* i + 10 + 50);

      fill(255, 0, 0);
    }
  }

  //波形を表示
  pushMatrix();
  translate(20, height/4*3);
  noFill();
  stroke(127);
  rect(0, -height/4*0.75, width/3*2, height/2 * 0.75);
  fill(#3399ff);
  noStroke();
  //バッファーに格納されたサンプル数だけくりかえし
  for (int j = 0; j < player.length; j++) {
    for (int i = 0; i < player[j].bufferSize(); i++) {
      // それぞれのバッファーでのX座標を探す
      float x  =  map( i, 0, player[j].bufferSize(), 0, width/3*2 );
      float amp = player[j].mix.get(i) * map(player[j].getGain(), -80, 0, 0, 1);
      float y = map(amp, -1, 1, -height/4, height/4);
      ellipse(x, y, 2, 2);
    }
  }
  popMatrix();
}

void stop() {
  myPort.stop();
  super.stop();
}

gesture_sound01.jpg


目指せハカセ!

image

FacebookやTwitterには既に書いたのですが、無事大学院合格しました!!

今年の9月から、慶應義塾大学政策・メディア研究科の後期博士課程社会人コースに所属します。応援していただいた方、相談に乗っていただいた方、推薦書を書いていただいた方、多くの方々のサポートのお陰です。本当にありがとうございます。

面接当日の朝、原因不明の足の激痛に襲われ立ち上がるのもつらい状態。これは会場まで辿りつけないのではと一瞬目の前が真っ暗になったのですが、アイシングしたり鎮痛剤を飲んで徐々になんとか歩けるようになり、電車とタクシーを乗り継ぎ、なんとか面接まで漕ぎ着けました。危なかった…

思い返せば、修士課程を修了したのが1996年なので、17年ぶりの母校への帰還。まあ何というか、あっという間のようでもあり、長い時間のようでもあり…

とはいえ、まだようやくスタートラインに立ったところなので、本番はこれからといった感じです。もう41歳なので、あまりのんびりしていると徐々に思考能力も低下していきそう。できるだけ集中して学位取得を目指して頑張っていかねばと思います。

今後もこのブログに途中経過をぼちぼち書いていく予定。


Processingによるネットワーク情報のビジュアライズ 2

最終課題の提出方法

最終課題は以下の方法で制作・提出してください。

最終課題テーマ「芸大のネットワーク情報をビジュアライズ」

配布したJSONデータを利用して、芸大のネットワーク情報をビジュアライズ(可視化)してください。この授業ではProcessingを視覚化のツールとして使用しましたが、その他のツールや自分で独自にプログラミングしても構いません。ただし、最終的にWebブラウザで閲覧できる形式にしてください。

提出期限: 2013年7月31日

提出方法: 作成したプログラム一式をZip圧縮して、以下のメールアドレスに送付してください。

  • tadokoro+geidai13[at]gmail.com …※ [at]を@に変換

今回も芸大のネットワークの状況をビジュアライズ(視覚化)する手法について、サンプルをみながら解説していきます。

サンプルファイルのダウンロード

Processingのソースコードは下記のリンクからダウンロードしてください。

ネットワーク情報を、時間軸でプロット

まず初めに、これまでは取得した値をそのまま大きさや形、色にしていたものを、時間の経過による変化がわかるようプロットしてみましょう。Processingでは、draw()関数の中でbackground()を呼びだすのをやめると、過去の描画がそのまま残ります。この原理を利用して、簡単にプロットしていくことが可能です。

JSONArray values;

void setup() {

  size(800, 600);
  frameRate(30);
  values = loadJSONArray("packets.json");
  noStroke();
  background(0);
}

void draw() {
  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");
  fill(0);
  rect(0, 0, width, 60);
  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  noStroke();
  rect(0, 0, barWidth, 5);

  float x = map(num, 0, values.size(), 0, width);
  float iy = map(InOctets, 0, 100000000, height, 0);
  float oy = map(OutOctets, 0, 100000000, height, 0);

  fill(0, 127, 255, 127);
  ellipse(x, iy, 4, 4);

  fill(255, 127, 0, 127);
  ellipse(x, oy, 4, 4);

  if (num >= values.size()-1) {
    fill(0);
    rect(0, 0, width, height);
  }
}

geidai_net01

一つ前の時点での値を記録しておくことで、点ではなく2点間を結んだ線を描くことが可能です。これを連ねていくことで、線によるグラフになります。

JSONArray values;
float lastX, lastInY, lastOutY;

void setup() {
  size(800, 600);
  frameRate(30);
  values = loadJSONArray("packets.json");
  smooth();
  background(0);
}

void draw() {
  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");

  noStroke();
  fill(0);
  rect(0, 0, width, 60);
  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  noStroke();
  rect(0, 0, barWidth, 5);

  float x = map(num, 0, values.size(), 0, width);
  float iy = map(InOctets, 0, 100000000, height, 0);
  float oy = map(OutOctets, 0, 100000000, height, 0);

  if (num > 0 && num < values.size()-1) {
    stroke(0, 127, 255, 180);
    line(x, iy, lastX, lastInY);

    stroke(255, 127, 0, 180);
    line(x, oy, lastX, lastOutY);
  }

  lastX = x;
  lastInY = iy;
  lastOutY = oy;

  if (num >= values.size()-1) {
    fill(0);
    noStroke();
    rect(0, 0, width, height);
  }
}

geidai_net02

時間変化を円状に変形

では、この時間変化のプロットをすこし工夫して、円環状にデータが並んでいくようにしてみましょう。車に搭載された「タコグラフ(運行記録計)」のように、少しずつ回転させながら値をプロットしていっていると考えるとシンプルに実現可能です。

JSONArray values;

void setup() {

  size(800, 600);
  frameRate(30);
  values = loadJSONArray("packets.json");
  noStroke();
  background(0);
}

void draw() {
  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");
  fill(0);
  noStroke();
  rect(0, 0, width, 60);
  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  noStroke();
  rect(0, 0, barWidth, 5);

  float rot = map(num, 0, values.size(), 0, 2.0*PI);
  float iy = map(InOctets, 0, 100000000, 0, -height);
  float oy = map(OutOctets, 0, 100000000, 0, -height);

  pushMatrix();
  translate(width/2, height/2);
  rotate(rot);
  strokeWeight(0.25);
  stroke(255, 127, 31);
  line(0, 0, 0, oy);

  stroke(31, 127, 255);
  line(0, 0, 0, iy);

  popMatrix();

  if (num >= values.size()-1) {
    fill(0);
    rect(0, 0, width, height);
  }
}

geidai_net03

入力パケットと出力パケットの数を合計したものを1本の棒にして、円環状に記録していくと、さらに面白い結果になりました。

JSONArray values;

void setup() {

  size(800, 600);
  frameRate(30);
  values = loadJSONArray("packets.json");
  noStroke();
  background(0);
}

void draw() {
  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");
  fill(0);
  noStroke();
  rect(0, 0, width, 60);
  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  noStroke();
  rect(0, 0, barWidth, 5);

  float rot = map(num, 0, values.size(), 0, 2.0*PI);
  float iy = map(InOctets, 0, 100000000, 0, -height/2);
  float oy = map(OutOctets, 0, 100000000, 0, -height/2);

  pushMatrix();
  translate(width/2, height/2);
  rotate(rot);
  strokeWeight(0.25);
  stroke(255, 127, 31);
  line(0, 0, 0, oy);

  stroke(31, 127, 255);
  line(0, oy, 0, oy+iy);

  popMatrix();

  if (num >= values.size()-1) {
    fill(0);
    rect(0, 0, width, height);
  }
}

geidai_net04


Toucheセンサーを使う 3 – Touche for Arduinoで音を扱う

今回は、Touche (発音は「タッチー」ではなく「トゥシェ」が正しいようです) センサーの活用の3回目として、音を扱ってみたいと思います。具体的にはArduinoから送られてきた情報を、Processingを用いて音に変化していきます。

Processingで音を扱う方法は、いろいろ存在します。例えば第1クォーターのSound & Software Art WorkshopでとりあげたPure DataをProcessingからコントロールして音を出す方法などが考えられます。こうした外部とのアプリケーションとの連携をする場合には、Open Sound Control(OSC)という共通のプロトコルを使用します。

しかし、今回はあくまでProcessing単体での音響合成やサウンドファイルの再生に限定して、挑戦していきたいと思います。Processingで音を扱うためのライブラリはいくつか存在しますが、現時点で最も一般的な「Minim」というライブラリを使用して音をとり扱っていきたいと思います。

Minimライブラリについて

MinimはJavaSound APIを利用したProcessingの外部ライブラリで、現在はProcessing本体のパッケージに最初から含まれています。Minimには音に関する様々な機能が用意されています。主な機能として下記のものが挙げられます。

  • サウンドの再生 ‒ WAV, AIFF, AU, SND, MP3形式のサウンドファイルを読み込んで再生
  • 録音:入力された音を、ディスクに直接、またはメモリー上のバッファーに録音可能
  • オーディオ入力:モノラル、ステレオのオーディオ入力
  • オーディオ出力:モノラル、ステレオでサウンドを出力
  • 音響合成:シンプルな音響合成のための機能
  • エフェクト:サウンドエフェクトのためのインタフェイス
  • FFT:リアルタイムにスペクトル解析が可能

Minimについての詳細は下記のプロジェクトページを参照してください。

念の為、使用しているProcessingがMinimのライブラリ群が使用できる状態か確認してみましょう。Processingのメニューから「Spetch」→「Import Library…」で表示されるメニューの中に「minim」が入っていればOKです。

image

Minimライブラリを使ってみる

最新版のProcessing(現時点ではv.2.0.1)には、Minimライブラリーのサンプルがいくつも収録されています。これらの内容を確認してみましょう。

「File」→「Examples」でサンプルのウィンドウを開いて、その中にある「Library」→「minim」を表示します。

image

サウンドファイルの再生と、波形の描画

ではまず、シンプルな例としてサウンドファイルを読み込んで、再生してみましょう。また、再生の際にサウンドファイルの波形を表示するようにしています。

// Minimサンプル1:
// サウンドファイルの再生と、波形の描画

import ddf.minim.*;

Minim minim;
AudioPlayer player;

void setup(){
  size(512, 200, P3D);

  // minimクラスをインスタンス化(初期化)、ファイルのデータを直接指定する必要があるので「this」を引数に指定
  minim = new Minim(this);

  // loadImageで画像ファイルを読み込む際の要領でサウンドファイルを読み込む
  // サウンドファイルは、スケッチ内の「data」フォルダ内に入れるのが普通だが
  // フルパスやURLで記述することも可能
  player = minim.loadFile("marcus_kellis_theme.mp3");

  // サウンドの再生
  player.play();
}

void draw(){
  background(0);
  stroke(255);

  // 波形を描画
  for(int i = 0; i < player.bufferSize() - 1; i++){
    float x1 = map( i, 0, player.bufferSize(), 0, width );
    float x2 = map( i+1, 0, player.bufferSize(), 0, width );
    line( x1, 50 + player.left.get(i)*50, x2, 50 + player.left.get(i+1)*50 );
    line( x1, 150 + player.right.get(i)*50, x2, 150 + player.right.get(i+1)*50 );
  }
}

複数の音を再生

Audio Playerをたくさん用意することで複数のサウンドファイルを再生することも可能です。この例では、バスドラムとスネアの音を読み込んで、キーボードからの入力をきっかけにしてサウンドファイルを再生しています。

// 複数のサウンドファイルを鳴らす

import ddf.minim.*;

Minim minim;
AudioSample kick;
AudioSample snare;

void setup(){
  size(512, 200, P3D);
  minim = new Minim(this);

  // バスドラムの音を読込み
  kick = minim.loadSample( "BD.mp3", // ファイル名
                            512      // バッファサイズ
                            );

  // ファイルが存在しない場合、エラーメッセージを返す
  if ( kick == null ){
   println("Didn't get kick!");
 }
  // スネアの音を読込み
  snare = minim.loadSample("SD.wav", 512);
  // ファイルが存在しない場合、エラーメッセージを返す
  if ( snare == null ){
    println("Didn't get snare!");

  }
}
void draw(){
  background(0);
  stroke(255);

  // それぞれのサウンドの波形の描画
  for (int i = 0; i < kick.bufferSize() - 1; i++) {
    float x1 = map(i, 0, kick.bufferSize(), 0, width);
    float x2 = map(i+1, 0, kick.bufferSize(), 0, width);
    line(x1, 50 - kick.mix.get(i)*50, x2, 50 - kick.mix.get(i+1)*50);
    line(x1, 150 - snare.mix.get(i)*50, x2, 150 - snare.mix.get(i+1)*50);
  }
}

// キー入力でサウンド再生
void keyPressed(){
  if ( key == 's' ){
    snare.trigger();
  }
  if ( key == 'k' ){
    kick.trigger();
  }
}

音響合成1: Sin波を合成

では次に、Sin波を生成し音として出力してみましょう。生成したSin波は、マウスの位置で音量と周波数を変更できるようにしています。

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
SineWave sine;

void setup() {
  size(512, 200, P3D);
  frameRate(60);
  smooth();
  strokeWeight(2);

  minim = new Minim(this);
  out = minim.getLineOut();
  sine = new SineWave(440, 1.0, out.sampleRate());
  sine.portamento(200);
  out.addSignal(sine);
}

void draw() {
  //波形を表示
  background(0);
  stroke(255);
  //バッファーに格納されたサンプル数だけくりかえし
  for (int i = 0; i < out.bufferSize() - 1; i++) {
    // それぞれのバッファーでのX座標を探す
    float x1  =  map( i, 0, out.bufferSize(), 0, width );
    float x2  =  map( i+1, 0, out.bufferSize(), 0, width );
    // 次の値へ向けて線を描く
    line( x1, 50 + out.left.get(i)*50, x2, 50 + out.left.get(i+1)*50);
    line( x1, 150 + out.right.get(i)*50, x2, 150 + out.right.get(i+1)*50);
  }
}

void mouseMoved() {
  // 周波数をマウスのX座標で変化させる
  float freq = map(mouseX, 0, width, 20, 1000);
  sine.setFreq(freq);
  // 音量をマウスのY座標で変化させる
  float amp = map(mouseY, 0, height, 1, 0);
  sine.setAmp(amp);
}

音響合成2: FM合成

もう少し複雑な音響合成のサンプルを作成してみましょう。下記のプログラムはFM合成(Frequency Modulation Synthesis)による音響合成のサンプルです。

// FM合成

import ddf.minim.*;
import ddf.minim.ugens.*;

Minim minim;
AudioOutput out;

// オシレーター
Oscil fm;

void setup(){
  size( 512, 200, P3D );

  // Minimクラスのインスタンス化(初期化)
  minim = new Minim( this );
  // 出力先を生成
  out   = minim.getLineOut();
  // モジュレータ用のオシレーター
  Oscil wave = new Oscil( 200, 0.8, Waves.SINE );
  // キャリア用のオスレータを生成
  fm = new Oscil( 10, 2, Waves.SINE );
  // モジュレータの値の最小値を200Hzに
  fm.offset.setLastValue( 200 );
  // キャリアの周波数にモジュレータを設定( = 周波数変調)
  fm.patch( wave.frequency );
  // and patch wave to the output
  wave.patch( out );
}

// 波形を描画
void draw(){
  background( 0 );
  stroke( 255 );
  for( int i = 0; i < out.bufferSize() - 1; i++ ) {
    float x1  =  map( i, 0, out.bufferSize(), 0, width );
    float x2  =  map( i+1, 0, out.bufferSize(), 0, width );
    line( x1, 50 + out.left.get(i)*50, x2, 50 + out.left.get(i+1)*50);
    line( x1, 150 + out.right.get(i)*50, x2, 150 + out.right.get(i+1)*50);
  }  
}

// マウスの位置によってFMのパラメータを変化させる
void mouseMoved(){
  float modulateAmount = map( mouseY, 0, height, 500, 1 );
  float modulateFrequency = map( mouseX, 0, width, 0.1, 100 );

  fm.frequency.setLastValue( modulateFrequency );
  fm.amplitude.setLastValue( modulateAmount );
}

Toucheの情報をもとに、Minimで音響生成

Minim単体の機能はだいたい把握できたところで、Touche for Arduinoからの情報でMinimの音響合成をする方法について考えていきましょう。まずは、前回同様にToucheの回路をくみたてましょう。

image

前回使用したソフトウェアもダウンロードしておきましょう。

まずはToucheの動作を確認して問題なければ、Minimとの連携に進みましょう。早速Toucheで検出した値と、Minimでの音響合成を合体させていきましょう。

今回はまず、Touche for Arduinoからの値を受けて、記録した場所からの距離を算出する部分のみを抜き出した制作テンプレートを用意しました。ここにMinimの機能を加えていきたいと思います。

Processing_Gprah.pde

/*
 * Touche for Arduino
 * Project Template
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  //ポートを設定
  PortSelected=0; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float dist = dist(x, y, rx, ry);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

ToucheとMinimの融合1 – 植物テルミン

まず初めに簡単なサンプルで、連携を確認していきたいと思います。現在のToucheから送られてきている電圧の値でオシレーターの周波数を変化させて「植物テルミン」をつくってみましょう。記憶した時点でのToucheセンサーと現在との距離を、Sin波の周波数に対応させてみましょう。

/*
 * Touche for Arduino
 * Touche Theremin
 *
 */

import ddf.minim.*;
import ddf.minim.signals.*;

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

Minim minim;
AudioOutput out;
SineWave sine;

void setup() {
  //画面サイズ
  size(800, 600); 
  //ポートを設定
  PortSelected=0; 
  //シリアルポートを初期化
  SerialPortSetup();

  minim = new Minim(this);
  out = minim.getLineOut();
  sine = new SineWave(440, 1.0, out.sampleRate());
  sine.portamento(200);
  out.addSignal(sine);
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float dist = dist(x, y, rx, ry);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);

    // Sin波の周波数をセンサーの値の距離に対応させる
    float freq = map(dist, 0, 1000, 20, 1000);
    sine.setFreq(freq);
  }

  //波形を表示
  background(0);
  stroke(255);
  //バッファーに格納されたサンプル数だけくりかえし
  for (int i = 0; i < out.bufferSize() - 1; i++) {
    // それぞれのバッファーでのX座標を探す
    float x1  =  map( i, 0, out.bufferSize(), 0, width );
    float x2  =  map( i+1, 0, out.bufferSize(), 0, width );
    // 次の値へ向けて線を描く
    line( x1, 50 + out.left.get(i)*50, x2, 50 + out.left.get(i+1)*50);
    line( x1, 150 + out.right.get(i)*50, x2, 150 + out.right.get(i+1)*50);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

ToucheとMinimの融合2 – 植物テルミン FM版

FM合成版の植物テルミンも作ってみました。キャリアの周波数とモジュレータの音量がToucheの状態によって変化するようにしています。

/*
 * Touche for Arduino
 * Touche FM Theremin
 *
 */

import ddf.minim.*;
import ddf.minim.ugens.*;

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

Minim minim;
AudioOutput out;
Oscil fm;

void setup() {
  //画面サイズ
  size(800, 600); 
  //ポートを設定
  PortSelected=0; 
  //シリアルポートを初期化
  SerialPortSetup();

  // Minimクラスのインスタンス化(初期化)
  minim = new Minim( this );
  // 出力先を生成
  out   = minim.getLineOut();
  // モジュレータ用のオシレーター
  Oscil wave = new Oscil( 200, 0.8, Waves.SINE );
  // キャリア用のオスレータを生成
  fm = new Oscil( 10, 2, Waves.SINE );
  // モジュレータの値の最小値を100Hzに
  fm.offset.setLastValue( 100 );
  // キャリアの周波数にモジュレータを設定( = 周波数変調)
  fm.patch( wave.frequency );
  // and patch wave to the output
  wave.patch( out );
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float dist = dist(x, y, rx, ry);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);
    float modulateAmount = map( dist, 0, 1000, 0, 1000 );
    float modulateFrequency = map( timeMax, 0, 159, 0.1, 100 );

    fm.frequency.setLastValue( modulateFrequency );
    fm.amplitude.setLastValue( modulateAmount );
  }

  //波形を表示
  background(0);
  stroke(255);
  noFill();
  //Y座標の原点を画面の中心に移動
  translate(0, height/2);
  //バッファーに格納されたサンプル数だけくりかえし
  for (int i = 0; i < out.bufferSize() - 1; i++) {
    //サンプル数から、画面の幅いっぱいに波形を表示するようにマッピング
    float x = map(i, 0, out.bufferSize(), 0, width);
    //画面の高さいっぱになるように、サンプルの値をマッピング
    float y = map(out.mix.get(i), 0, 1.0, 0, height/4);
    //値をプロット
    point(x, y);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}


Processingによるネットワーク情報のビジュアライズ

今回は、最終課題として出題した「東京藝術大学のネットワーク情報のビジュアライズ」という課題に向けて、どのようにして制作していけばよいのか、その方法について考えていきます。実際のProcessingのコードの内容を解説しながら、その方法について解説していきます。

サンプルファイルのダウンロード

Processingのソースコードは下記のリンクからダウンロードしてください。

example01: データを読み込む

JSONArray values;

void setup() {

  values = loadJSONArray("packets.json");

  for (int i = 0; i < values.size(); i++) {

    JSONObject packet = values.getJSONObject(i); 

    int year = packet.getInt("year"); 
    int month = packet.getInt("month"); 
    int day = packet.getInt("day"); 
    int hour = packet.getInt("hour"); 
    int min = packet.getInt("min"); 
    int InOctets = packet.getInt("InOctets");
    int OutOctets = packet.getInt("OutOctets");

    print("year: " + year + ", "); 
    print("month: " + month + ", "); 
    print("day: " + day + ", "); 
    print("hour: " + hour + ", "); 
    print("min: " + min + ", "); 
    print("InOctets: " + InOctets + ", ");  
    println("OutOctets: " + OutOctets);
  }
}

example02: 情報を画面に表示

JSONArray values;

void setup() {
  size(800, 600);
  frameRate(30);
  values = loadJSONArray("packets.json");
  noStroke();
}

void draw() {
  background(31);
  fill(255);
  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  rect(0, 0, barWidth, 5);
}

example03: 円の大きさで表現

JSONArray values;

void setup() {
  size(800, 600);
  frameRate(30);
  stroke(127);
  values = loadJSONArray("packets.json");
}

void draw() {
  background(31);
  fill(255);
  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);
  
  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  rect(0, 0, barWidth, 5);

  float in = map(InOctets, 0, 40000000, 0, height);
  fill(0, 0, 255, 127);
  ellipse(width/5 * 2, height/2, in, in);

  float out = map(OutOctets, 0, 40000000, 0, height);
  fill(255, 0, 0, 127);
  ellipse(width/5 * 3, height/2, out, out);
}

example04: 色で表現

JSONArray values;

void setup() {

  size(800, 600);
  frameRate(30);
  noStroke();
  values = loadJSONArray("packets.json");
}

void draw() {
  background(0);

  int num = frameCount % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");

  float in = map(InOctets, 0, 40000000, 0, 255);
  fill(0, 0, 255, in);
  rect(0, 0, width/2, height);

  float out = map(OutOctets, 0, 40000000, 0, 255);
  fill(255, 0, 0, out);
  rect(width/2, 0, width/2, height);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  rect(0, 0, barWidth, 5);
  
  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min, 20, 30);
  text("InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 50);
}

example05: パーティクル1

ArrayList<Particle> particles;
JSONArray values;

void setup() {
  size(1024, 768);
  frameRate(30);
  noStroke();
  values = loadJSONArray("packets.json");
  particles = new ArrayList<Particle>();
  background(0);
}

void draw() {
  noStroke();
  fill(0, 0, 0, 20);
  rect(0, 0, width, height);

  fill(0);
  rect(0, 0, width, 30);

  int num = frameCount / 60 % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  noStroke();
  rect(0, 0, barWidth, 5);

  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min 
  + ", InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 18);

  float in = map(InOctets, 0, 100000000, 0, 10);
  float out = map(OutOctets, 0, 100000000, 0, 10);

  for (int i = 0; i < int(in); i++) {
    particles.add(
    new Particle(new PVector(0, random(30,height)), new PVector(random(3, 20), 0))
      );
  }
  for (int i = 0; i < int(out); i++) {
    particles.add(
    new Particle(new PVector(width, random(30,height)), new PVector(random(-20, -3), 0))
      );
  }
  
  stroke(255);
  for (int i = particles.size()-1; i >= 0; i--) { 
    Particle pt = particles.get(i);
    pt.display();
    if (pt.finished) {
      particles.remove(i);
    }
  }
}

class Particle {
  PVector location;
  PVector velocity;
  boolean finished;
  
  Particle(PVector _location, PVector _velocity) {
    location = _location;
    velocity = _velocity;
    finished = false;
  }

  void display() {
    point(location.x, location.y);
    //point(random(width), random(height));
    location.add(velocity);
    if (location.x > width || location.x < 0) {
      finished = true;
    }    
  }
  
  boolean finished(){
    return finished;
  }
}

example06: パーティクル2

ArrayList<Particle> particles;
JSONArray values;

void setup() {
  size(1024, 768);
  frameRate(30);
  noStroke();
  values = loadJSONArray("packets.json");
  particles = new ArrayList<Particle>();
  background(0);
}

void draw() {
  noStroke();
  fill(0, 0, 0, 20);
  rect(0, 0, width, height);

  int num = frameCount / 10 % values.size();
  JSONObject packet = values.getJSONObject(num);
  int year = packet.getInt("year"); 
  int month = packet.getInt("month"); 
  int day = packet.getInt("day"); 
  int hour = packet.getInt("hour"); 
  int min = packet.getInt("min"); 
  int InOctets = packet.getInt("InOctets");
  int OutOctets = packet.getInt("OutOctets");

  float in = map(InOctets, 0, 100000000, 0, 20);
  float out = map(OutOctets, 0, 100000000, 0, 20);

  for (int i = 0; i < int(in); i++) {
    particles.add(
    new Particle(new PVector(random(width), 0), new PVector(0, random(3, 10)))
      );
  }
  for (int i = 0; i < int(out); i++) {
    particles.add(
    new Particle(new PVector(random(width), height), new PVector(0, random(-10, -3)))
      );
  }

  stroke(255);
  for (int i = particles.size()-1; i >= 0; i--) { 
    Particle pt = particles.get(i);
    pt.display();
    if (pt.finished) {
      particles.remove(i);
    }
  }

  noStroke();
  fill(0);
  rect(0, 0, width, 30);

  float barWidth = map(num, 0, values.size(), 0, width);
  fill(127);
  noStroke();
  rect(0, 0, barWidth, 5);

  fill(255);
  text(year + "/" + month + "/" + day + ", " + hour + ":" + min 
    + ", InOctets: " + InOctets + ", " + "OutOctets: " + OutOctets, 20, 18);
}

class Particle {
  PVector location;
  PVector velocity;
  boolean finished;

  Particle(PVector _location, PVector _velocity) {
    location = _location;
    velocity = _velocity;
    finished = false;
  }

  void display() {
    point(location.x, location.y);
    //point(random(width), random(height));
    location.add(velocity);
    if (location.x > width || location.x < 0) {
      finished = true;
    }   
    if (location.y > height || location.y < 0) {
      finished = true;
    }
  }

  boolean finished() {
    return finished;
  }
}

Toucheセンサーを使う 2 – Touche for Arduinoの値をつかってProcessingで表現する

前回はTouche for Arduinoセンサーをつかって、様々なジェスチャーを検出することができました。今回はTouche for ArduinoのセンサーからProcessingにどのような値が送られてきているのか、プログラムを解析しながら明らかにしていきます。また、その値をどのように利用したら、Processingで面白い表現ができるのか考えていきましょう。

サンプルプログラムのダウンロード

今回とりあげたサンプルは下記のリンクからダウンロードしてください。

前回までの復習: Touche for Arduinoの作成

まずは、前回と同様、Touche for Arduinoの回路をブレッドボードに組み立てましょう。

image

ソフトウェアは前回と同様下記のGithubアカウントからダウンロードして使用します。

前回はこのプログラムの中で、主にジェスチャーを判別する部分について解説しました。ポイントは、検出したグラフで電圧が最大のピークになっている部分の(x, y)座標を記録して、新たなジェスチャーと記録したジェスチャーの距離を算出しているという部分です。以下のコメント入りのソースで再度確認しましょう。

Processing側 – Processing_graph.pde

/*
 * Processing_graph.pde
 *
 */

// グラフ描画のクラスのインスタンス化(初期化)
Graph MyArduinoGraph = new Graph(150, 80, 500, 300, color (200, 20, 20));
// 4つのジェスチャーを記録する配列
float[][] gesturePoints = new float[4][2];
// 現在のジェスチャーと登録したジェスチャーとの距離
float[] gestureDist = new float[4];
// それぞれのジェスチャーの名前の文字列
String[] names = {
  "Nothing", "Touch", "Grab", "In water"
};

void setup() {
  //画面サイズ
  size(1000, 500); 
  // グラフのラベル設定
  MyArduinoGraph.xLabel="Readnumber";
  MyArduinoGraph.yLabel="Amp";
  MyArduinoGraph.Title=" Graph";  
  noLoop();

  //ポートを設定
  PortSelected=4; 

  //シリアルポートを初期化
  SerialPortSetup();
}


void draw() {
  background(255);

  /* ====================================================================
   グラフを描画
   ====================================================================  */

   if ( DataRecieved3 ) {
    pushMatrix();
    pushStyle();
    MyArduinoGraph.yMax=100;      
    MyArduinoGraph.yMin=-10;      
    MyArduinoGraph.xMax=int (max(Time3));
    MyArduinoGraph.DrawAxis();  
    MyArduinoGraph.smoothLine(Time3, Voltage3);
    popStyle();
    popMatrix();

    float gestureOneDiff =0;
    float gestureTwoDiff =0;
    float gestureThreeDiff =0;

    /* ====================================================================
     ジェスチャーを比較
     ====================================================================  */
     float totalDist = 0;
     int currentMax = 0;
     float currentMaxValue = -1;
    for (int i = 0; i < 4;i++) { //4つの登録したジェスチャーを比較
      //ボタンをマウスでクリックしたときには、現在のジェスチャーを配列に記録
      if (mousePressed && mouseX > 750 && mouseX<800 && mouseY > 100*(i+1) && mouseY < 100*(i+1) + 50) {
        fill(255, 0, 0);
        gesturePoints[i][0] = Time3[MyArduinoGraph.maxI];
        gesturePoints[i][1] = Voltage3[MyArduinoGraph.maxI];
      } 
      else {
        fill(255, 255, 255);
      }
      //それぞれの点と現在の状態の距離を算出
      gestureDist[i] = dist(Time3[MyArduinoGraph.maxI], Voltage3[MyArduinoGraph.maxI], gesturePoints[i][0], gesturePoints[i][1]);
      //距離の合計を算出
      totalDist = totalDist + gestureDist[i];
      //最大値を算出
      if (gestureDist[i] < currentMaxValue || i == 0) {
        currentMax = i;
        currentMaxValue =  gestureDist[i];
      }
    }
    totalDist=totalDist /3;
    // 現在のジェスチャーと登録したジェスチャーの距離から、ボタンの色を描画
    for (int i = 0; i < 4;i++) {
      float currentAmmount = 0;
      currentAmmount = 1-gestureDist[i]/totalDist;
      if (currentMax == i) {
        fill(currentAmmount*255.0f, 0, 0);
      } 
      else {
        fill(255, 255, 255);
      }
      stroke(0, 0, 0);
      rect(750, 100 * (i+1), 50, 50);
      fill(0, 0, 0);
      textSize(30);
      text(names[i], 810, 100 * (i+1)+25);

      fill(255, 0, 0);
    }
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

今回はもう少しこのプログラムについて、突っ込んで内容を解析していきます。それによって、オリジナルなビジュアライズや音響合成をする際のヒントにしていきたいと思います。

まず、ArduinoからProcessingへ、何のデータがどのような形式でデータが送出されているのか解析してみましょう。まず送信側のArduinoからみていきましょう。実際に値を送信している部分、”SendData.ino” の中身をみていきます。

Arduino側 – SendData.ino

byte yMSB=0, yLSB=0, xMSB=0, xLSB=0, zeroByte=128, Checksum=0;

void SendData(int Command, unsigned int yValue,unsigned int xValue){
    yLSB=lowByte(yValue);   //yの値(16bit)の後半8bit
    yMSB=highByte(yValue);  //yの値(16bit)の前半8bit
    xLSB=lowByte(xValue);   //xの値(16bit)の後半8bit
    xMSB=highByte(xValue);  //xの値(16bit)の前半8bit

    //空白(0Byte)の値がある場所を記録
    zeroByte = 128;                         // 10000000
    if(yLSB==0){ yLSB=1; zeroByte=zeroByte+1;} // 1bit目を1に(10000001)
    if(yMSB==0){ yMSB=1; zeroByte=zeroByte+2;} // 2bit目を1に(10000010)
    if(xLSB==0){ xLSB=1; zeroByte=zeroByte+4;} // 3bit目を1に(10000100)
    if(xMSB==0){ xMSB=1; zeroByte=zeroByte+8;} // 4bit目を1に(10001000)

    Checksum = (Command + yMSB + yLSB + xMSB + xLSB + zeroByte)%255;

    if( Checksum !=0 ){
        Serial.write(byte(0));          // 先頭のビット
        Serial.write(byte(Command));      // どのグラフを描画するのかを指定するコマンド

        Serial.write(byte(yMSB));        // Yの値の前半8bit(1Byte)を送信
        Serial.write(byte(yLSB));        // Yの値の後半8bit(1Byte)を送信
        Serial.write(byte(xMSB));        // Xの値の前半8bit(1Byte)を送信
        Serial.write(byte(xLSB));        // Xの値の後半8bit(1Byte)を送信

        Serial.write(byte(zeroByte));    // どの値に0があるのかを送信
        Serial.write(byte(Checksum));    // チェック用バイト
    }
}

void PlottArray(unsigned int Cmd,float Array1[],float Array2[]){
    SendData(Cmd+1, 1,1);
    delay(1);
    for(int x=0;  x < sizeOfArray;  x++){
        SendData(Cmd, round(Array1[x]),round(Array2[x]));
    }
    SendData(Cmd+2, 1,1);
}

このソースコードの中で、実際にシリアル通信で送出するメッセージを送っている部分は、中盤にある”Serial.write()”関数が並んでいる部分です。

Serial.write(byte(0));          // 先頭のビット
Serial.write(byte(Command));      // どのグラフを描画するのかを指定するコマンド

Serial.write(byte(yMSB));        // Yの値の前半8bit(1Byte)を送信
Serial.write(byte(yLSB));        // Yの値の後半8bit(1Byte)を送信
Serial.write(byte(xMSB));        // Xの値の前半8bit(1Byte)を送信
Serial.write(byte(xLSB));        // Xの値の後半8bit(1Byte)を送信

Serial.write(byte(zeroByte));    // どの値に0があるのかを送信
Serial.write(byte(Checksum));    // チェック用バイト

ここで「Yの値」となっているのがToucheで検出された電圧、「Xの値」となっているのがその値を検出した時間に相当します。このメッセージがひと塊となってProcessingに送られていきます。図示すると以下のようなイメージになるでしょう。それぞれのブロックが、8bit(00000000〜11111111)つまり1Byteの値を格納しています。

image

ここで気になるのが、何故、電圧(Yの値)と時間(Xの値)をそれぞれ前半・後半の2つの値に分割しているのか、という部分ではないでしょうか。

実はこれは Serial.wirte() の関数で書き出せるデータの大きさに由来します。Seria.write() では一度に1Byte(= 8bit)の値までしか送出できないという制限があります。これを10進数数値に換算すると 0〜255 の256段階ということを意味します。

しかし今回のToucheはより繊細な電圧の変化とそれに対応する時間の変化を送りたいので、256段階では目盛が粗すぎます。そこで、送信するデータを前半8bit(11111111)と後半8bit(11111111)に分割して送信し、それを受信するProcessing側で再度合成するという手法をとっています。これによって使用できる値は16bit(0000000000000000〜1111111111111111)、10進数に直すと0〜1023の1024段階の数値を表現できるのです。

データを分割してArduinoからProcessingへ送信する様子をイメージにすると以下のようになるでしょう。

image

また、この連携プログラムでは、一連のメッセージの2番目のバイトを「コマンド」と呼んでいて、データ送信の開始から終了までを検知できるようにしています。コマンドは以下のように定義されています。

  • コマンド 1: データを送信
  • コマンド 2: 送信開始(初期化)
  • コマンド 3: 送信終了

このコマンドによって条件分岐させて、Processing側で正しくデータを受信できるようにしています。

では次にこのデータを受信するProcessing側のプログラムをみてみましょう。Processing側でシリアル通信でデータを受信するパートは、”SerialLink.pde”が相当します。

Processing側 – SerialLink.pde

import processing.serial.*;
int SerialPortNumber=2;
int PortSelected=2;

//グローバル変数
int xValue, yValue, Command;
boolean Error=true;
boolean UpdateGraph=true;
int lineGraph;
int ErrorCounter=0;
int TotalRecieved=0;

//ローカル変数
boolean DataRecieved1=false, DataRecieved2=false, DataRecieved3=false;

float[] DynamicArrayTime1, DynamicArrayTime2, DynamicArrayTime3;
float[] Time1, Time2, Time3;
float[] Voltage1, Voltage2, Voltage3;
float[] current;
float[] DynamicArray1, DynamicArray2, DynamicArray3;

float[] PowerArray= new float[0]; // Dynamic arrays that will use the append()
float[] DynamicArrayPower = new float[0]; // function to add values
float[] DynamicArrayTime= new float[0];

String portName;
String[] ArrayOfPorts=new String[SerialPortNumber];

boolean DataRecieved=false, Data1Recieved=false, Data2Recieved=false;
int incrament=0;

int NumOfSerialBytes=8; // The size of the buffer array
int[] serialInArray = new int[NumOfSerialBytes]; // Buffer array
int serialCount = 0; // A count of how many bytes received
int xMSB, xLSB, yMSB, yLSB; // Bytes of data

Serial myPort; // The serial port object

//========================================================================
// シリアル通信の初期化関数
// スピードを115200bpsで接続し、シリアルポートのバッファをクリアした後、
// シリアルのバッファ値を20Byteに設定
//========================================================================
void SerialPortSetup() {
  //text(Serial.list().length,200,200);
  portName= Serial.list()[PortSelected];
  //println( Serial.list());
  ArrayOfPorts=Serial.list();
  println(ArrayOfPorts);
  myPort = new Serial(this, portName, 115200);
  delay(50);
  myPort.clear();
  myPort.buffer(20);
}

//========================================================================
// シリアルイベント:
// シリアルポートから何か情報を受けとると呼びだされる
//========================================================================
void serialEvent(Serial myPort) {
  while (myPort.available ()>0) {
    // シリアルのバッファーから次のバイト列を読込み
    int inByte = myPort.read();
    if (inByte==0) {
      serialCount=0;
    }
    if (inByte>255) {
      println(" inByte = "+inByte);
      exit();
    }
    // シリアルポートから取得された最新のバイトを、配列に追加
    serialInArray[serialCount] = inByte;
    serialCount++;

    Error=true;
    if (serialCount >= NumOfSerialBytes ) {
      serialCount = 0;
      TotalRecieved++;
      int Checksum=0;

      for (int x=0; x<serialInArray.length-1; x++) {
        Checksum=Checksum+serialInArray[x];
      }

      Checksum=Checksum%255;

      if (Checksum==serialInArray[serialInArray.length-1]) {
        Error = false;
        DataRecieved=true;
      } 
      else {
        Error = true;
        DataRecieved=false;
        ErrorCounter++;
        println("Error:  "+ ErrorCounter +" / "+ TotalRecieved+" : "+float(ErrorCounter/TotalRecieved)*100+"%");
      }
    }

    if (!Error) {
      int zeroByte = serialInArray[6];
      // println (zeroByte & 2);
      xLSB = serialInArray[3];
      if ( (zeroByte & 1) == 1) xLSB=0;
      xMSB = serialInArray[2];
      if ( (zeroByte & 2) == 2) xMSB=0;
      yLSB = serialInArray[5];
      if ( (zeroByte & 4) == 4) yLSB=0;
      yMSB = serialInArray[4];
      if ( (zeroByte & 8) == 8) yMSB=0;

      //データ内容確認用
      //println( "0\tCommand\tyMSB\tyLSB\txMSB\txLSB\tzeroByte\tsChecksum");
      //println(serialInArray[0]+"\t"+Command +"\t"+ yMSB +"\t"+ yLSB +"\t"+ xMSB +"\t"+ xLSB+"\t" +zeroByte+"\t"+ serialInArray[7]);

      //========================================================================
      // バイト(8bit)単位に分割されたデータを合成して、16bit(0〜1024)の値を復元する
      //========================================================================
      Command  = serialInArray[1];
      xValue   = xMSB << 8 | xLSB; // xMSBとxLSBの値から、xValueを合成
      yValue   = yMSB << 8 | yLSB; // yMSBとyLSBの値から、yValueを合成

      //データ内容確認用
      //println(Command+ "  "+xValue+"  "+ yValue+" " );

      //========================================================================
      // Command, xValue, yValueの3つの値がArduinoから取得完了
      // 様々なケースごとに、グラフ描画用の配列に格納していく
      //========================================================================

      switch(Command) { //Commandの値によって、動作をふりわけ

        //配列1と配列2がArduinoから受信された状態、グラフを更新する

      case 1: // 配列にデータを追加
        DynamicArrayTime3=append( DynamicArrayTime3, (xValue) ); //xValueを時間の配列に1つ追加
        DynamicArray3=append( DynamicArray3, (yValue) ); //yValueをの電圧の配列に1つ追加
        break;

      case 2: // 想定外のサイズの配列を受けとった場合、配列を空に初期化
        DynamicArrayTime3= new float[0];
        DynamicArray3= new float[0];
        break;

      case 3:  // 配列の受信完了、値を追加した配列をグラフ描画用の配列にコピーして、グラフを描画する
        Time3=DynamicArrayTime3;
        Voltage3=DynamicArray3;
        DataRecieved3=true;
        break;
      }
    }
  }
  redraw();
}

このProcessingのコードの中で、先程8bitずつ分割したX(時間)とY(電圧)の時間を合成しているのは、下記の部分です。

xValue   = xMSB << 8 | xLSB; // xMSBとxLSBの値から、xValueを合成
yValue   = yMSB << 8 | yLSB; // yMSBとyLSBの値から、yValueを合成

合成された値は、xValueとyValueに格納されているのがわかります。この値は、データの受信中(コマンド1の際には)以下の命令でそれぞれ、DynamicArrayTime3とDynamicArray3という配列に追加されています。

DynamicArrayTime3=append( DynamicArrayTime3, (xValue) ); //xValueを時間の配列に1つ追加
DynamicArray3=append( DynamicArray3, (yValue) ); //yValueをの電圧の配列に1つ追加

最終的にデータの送信が完了(コマンド3)したら、格納した配列を別の描画用の配列にコピーしています。

Time3=DynamicArrayTime3;
Voltage3=DynamicArray3;

つまり、最終的にビジュアライズや何かしら表現する際に参照すべきデータはそれぞれ以下の配列ということになります。

  • 時間: Time3
  • 電圧: Voltage3

では、以下のパートで、この値を活用してみましょう。

Processingでビジュアライズ 1 – テキストで出力

では、さっそく取得したTime3とVoltage3を利用して、Processingで視覚的に表現してみましょう。Processingのパッケージの中にある「GraphClass.pde」と「SerialLink.pde」はそのまま使用します。メインのプログラム「Processing_graph.pde」のみ改造していきます。

ジェスチャーの特徴は、電圧(Voltage3)のピーク(最大値)の値とそのときの時間(Time3)の値の組がそのジェスチャーの特徴をあらわしています。まずは単純に電圧の最大値と、最大値が記録されたときの時間を文字でモニターしてみましょう。以下のようなシンプルなプログラムで確認可能です。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 00
 *
 */

float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);
  fill(255);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }

    //時間と電圧の範囲(最小値と最大値)を表示
    text("Time range: " +  min(Time3) + " - " + max(Time3), 20, 20);
    text("Voltage range: " +  min(Voltage3) + " - " + max(Voltage3), 20, 40);

    //電圧の最大値と、その時の時間を表示
    text("Time: " + timeMax, 20, 80);
    text("Voltage: " + voltageMax, 20, 100);
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

このプログラムを実行すると、以下のように時間と電圧の範囲と、電圧の最大値とそのときの時間が表示されます。

image

Processingでビジュアライズ 2 – ピークの位置を座標で表示

では次に電圧のピークの際の時間をX座標に、電圧をY座標にして画面に表示してみましょう。ちょうど画面いっぱいに表示されるようにmap()関数をつかって値を変化するところがポイントです。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 02
 *
 */

float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, graphMargin, width-graphMargin);
    float y = map(voltageMax, yMin, yMax, height-graphMargin, graphMargin); 

    //枠線を描く
    noFill();
    stroke(127);
    rect(graphMargin, graphMargin, width - graphMargin * 2, height - graphMargin * 2);

    //現在の最大値の座標を交差する線で描く
    line(x, graphMargin, x, height-graphMargin);
    line(graphMargin, y, width-graphMargin, y);

    //現在のそれぞれの最大値を文字で表示
    fill(#3399ff);
    noStroke();
    text(timeMax, x+2, height-graphMargin-2);
    text(voltageMax, graphMargin, y-10);

    //現在の最大値を円で描く
    ellipse(x, y, 20, 20);
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

このプログラムを実行すると、以下のように画面上の2次元座標の位置として現在の状態が表示されます。
image

Processingでビジュアライズ 3 – タッチしていないときとの差分(距離)を表示

Toucheのジェスチャーは、じつはこのピークの際の登録したジェスチャーとの距離によって判別しています。では先程のプログラムを改良して、タッチしていないときの位置を記録しておき、現在の最大値と比較するようにしてみたいと思います。

このプログラムでは、まず画面をクリックするとその時点での最大値を記録します。記録した最大値と現在の最大値との間に線を描き、さらにその距離を表示しています。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 03
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();

  //記録した最大値の値を初期化
  recVoltageMax = recTimeMax = 0;
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, graphMargin, width-graphMargin);
    float y = map(voltageMax, yMin, yMax, height-graphMargin, graphMargin); 
    float rx = map(recTimeMax, 0, 159, graphMargin, width-graphMargin);
    float ry = map(recVoltageMax, yMin, yMax, height-graphMargin, graphMargin);

    //枠線を描く
    noFill();
    stroke(127);
    rect(graphMargin, graphMargin, width - graphMargin * 2, height - graphMargin * 2);

    //現在の最大値の座標を交差する線で描く
    stroke(80);
    line(x, graphMargin, x, height-graphMargin);
    line(graphMargin, y, width-graphMargin, y);

    //現在のそれぞれの最大値を文字で表示
    fill(#3399ff);
    noStroke();
    text(timeMax, x+2, height-graphMargin-2);
    text(voltageMax, graphMargin, y-10);

    //現在の最大値と、記録した最大値の間に線を描く
    stroke(255);
    line(x, y, rx, ry);
    //記録しておいた最大値の場所に円を描く
    ellipse(rx, ry, 20, 20);
    //現在の最大値の場所に円を描く
    ellipse(x, y, 20, 20);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    float dist = dist(x, y, rx, ry);
    text("dist = "+dist, rx + 12, ry);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

void stop() {
  myPort.stop();
  super.stop();
}

image

Processingでビジュアライズ 4 – 円の色と大きさで、差分を表現

もう少し表示を工夫してみましょう。

マウスをクリックして登録した際の位置を、常に画面の中央になるように補正します。そして、現在のピークの位置と記録したピークの位置の差分を円の半径と透明度に、また、X座標(時間)の差分を角度に対応させて画面に描いてみます。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 04
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float diffX = x - rx;
    float diffY = y - ry;
    float dist = dist(x, y, rx, ry);
    float rot = map(diffX, 0, width, 0, PI*2);
    float col = map(dist, 0, width, 0, 255);

    pushMatrix();
    translate(width/2, height/2);
    rotate(rot);

    //現在の最大値と、記録した最大値の距離で円を描く
    fill(255, 0, 0, col);
    stroke(127);
    ellipse(0, 0, dist, dist);

    //現在の最大値と、記録した最大値の間に線を描く
    stroke(255);
    line(0, 0, dist/2, 0);

    //記録しておいた最大値の場所に円を描く
    noStroke();
    fill(#3399ff);
    ellipse(0, 0, 10, 10);

    //現在の最大値の場所に円を描く
    ellipse(dist/2, 0, 10, 10);
    popMatrix();

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

void stop() {
  myPort.stop();
  super.stop();
}

image

登録したジェスチャーと現在との違いが、より直感的に把握できるようになりました。

Processingでビジュアライズ 4 – 3Dで表現

最後に少し派手なビジュアライズのサンプルを紹介します。

角度をすこしずつ変更しながら大量の立方体を描くことで、複雑な形態のアニメーションを生成することが可能です。この例ではその立方体の大きさをピークの差分に。また角度もそれぞれの座標の差分で変化させることで、ジェスチャーから複雑に変化する3Dの形態を生みだしています。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 05
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

float angle = 0;   
int NUM = 100;   
float offset = 360.0/float(NUM);          
color[] colors = new color[NUM];

void setup() {
  //画面サイズ
  size(800, 600, OPENGL); 
  noStroke();
  colorMode(HSB, 360, 100, 100, 100);
  frameRate(60);
  for (int i=0; i<NUM; i++) {
    colors[i] = color(offset*i, 70, 100, 31);
  }
  //noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(0);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0);
    //記録した座標を変換
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    //それぞれの差分
    float diffX = x - rx;
    float diffY = y - ry;
    //距離を算出
    float dist = dist(x, y, rx, ry);

    //回転する立方体を角度をずらしながら大量に描く
    //立方体の大きさを差分に対応させている
    ambientLight(0, 0, 50);
    directionalLight(0, 0, 100, -1, 0, 0);
    pushMatrix();
    translate(width/2, height/2, -20); 
    for (int i=0; i<NUM; i++) {
      pushMatrix();
      fill(colors[i]);
      rotateY(radians(angle / diffY * 3.0 + offset*i));
      rotateX(radians(angle / diffY * 2.0 + offset*i));
      rotateZ(radians(angle / 10.0 + offset*i));
      box(dist);
      popMatrix();
    }
    angle += 1.0;
    popMatrix();
    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(0, 0, 100);
    text("dist = "+dist, 20, 20);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

void stop() {
  myPort.stop();
  super.stop();
}

image


Tumblrを使う 4 – 静的ページの作成、サイトの設計

Tumblrを使用したWebサイト制作の解説の4回目は、Tumblrのサイトを単純に投稿が時系列に表示される「ミニブログ」としての使い方からステップアップして、TumblrでWebサイトを構築する方法について考えていきます。その際に必要となるのが、投稿ページとは別の静的なページを作成する方法です。今回はTumblrでの静的ページの作成と、サイトの設計について考えていきます。