うつろぐ

文章

Processingとffmpegで簡易BGA作成 その2(BPMに同期したモーション、fps指定書き出し)

前回 - Processingとffmpegで簡易BGA作成 その1(レンダリングとプレビューを分けるまで) - うつろぐの続きになります。前回の記事をまだ読んでいない方は是非そちらからお願いします。
 

前回は取り敢えずProcessingとffmpegで音声つきの動画を書き出すところと、レンダリングとプレビューを分けるところをやってきました。
これに今回BPMに同期したモーションの作成やプレビュー、fpsを指定した書き出し(レンダリング)を実現すれば、一通りProcessingでBGAを作成する基盤が完成することになります。
 

前回までのコードの構造を改めて見てみる

前回の最終的なコードは以下のようなものでした。

import ddf.minim.*;
//Minimのsignal等を使っていないのでこの一行だけでよかった

boolean render=true;
//フレーム書き出しと動画への変換を行うかどうか。
//falseにするとプレビューのみを行う

int frameSum=1000;

float size=0;

Minim minim;
AudioPlayer player;

void setup() {
  size(600, 600);
  minim=new Minim(this);
  player=minim.loadFile("music.wav");

  if (render) {
    //以前のフレームデータを消去
    launch("C:\\xxxx\\Desktop\\PBGA_step1\\clearFrames");
    
    //フレーム書き出し
    for (int i=0; i<frameSum; i++) {
      display(i);
      if (i%100==0) {
        println("Now I am dumping frame"+i);
      }
      save("frames/"+nf(i, 4)+".png");
    }
    
    //動画に変換
    launch("C:\\xxxx\\Desktop\\PBGA_step1\\render");
  }
  player.play();
}

void draw() {
  display(frameCount-1);

  fill(0);
  noStroke();
  rect(0, 0, 100, 100);
  fill(-1);
  text(frameCount-1, 50, 50);
}

void stop() {
  player.close();
  minim.stop();
  super.stop();
}

//--------------------------------------

void display(int t) {
  background(0);
  size=400+300*sin(radians(t*10));

  fill(255, 0, 0);
  noStroke();
  ellipse(width/2.0, height/2.0, size, size);
}

構造として、このコードではsetup内でフレームの書き出し、draw内でプレビューを行っており、両方で同じ描画を行うためにdisplay()でタイミングを引数にとって描画内容を返しています。今回はこのdisplay()にBPMに沿ったタイミングを渡してあげることで、BPMに同期したモーションを実現します。

更に、これを現実時間に依存したタイミングを用いると、プレビューもBPMに同期したものになります。前回はProcessingの処理速度に依存したタイミング(frameCount)を用いてプレビューを描画していたのでプレビューが速くなったり遅くなったりしてしまいましたが、現実時間に依存したタイミング(millis()など)を用いることによって、常に一定の速さのモーションになるわけです。

BPM同期の機構をつくる

現実時間に依存したタイミングを用いたBPM同期の機構をつくっていきます。

時間管理をしてくれるclassをつくる

Processingではmillis()で「プログラム起動からの経過ミリ秒」が取得できるので、これを用いてストップウォッチのようなclassを作り、時間管理をさせます。

class MGTime {
  int pMillis, runMillis;
  boolean working;

  MGTime() {
    pMillis=millis();
    runMillis=0;
    working=false;
  }

  void timeStep() {
    if (working) {
      runMillis+=millis()-pMillis;
      pMillis=millis();
    }
  }

  void stop() {
    working=false;
  }
  
  void kill(){
    working=false;
    runMillis=0;
  }

  void start() {
    working=true;
    pMillis=millis();
  }

  int time_normal() {
    return runMillis;
  }
}

start()した後にdraw等のループ内でtimeStep()し、time_normal()でストップウォッチの現在のミリ秒を返します。stop()で一時停止、kill()で停止してリセットします。

BPM換算の機能を追加する

time_normal()で返るミリ秒は、1000ミリ秒(1秒)がBPM60の四分音符と同じ長さになります。よってこの時点でBPM60の楽曲に同期したモーションは簡単に作れますね。ここから発展させて、各BPMでの四分音符を1000としたものをtime_synched()という関数で返すことで、任意のBPMに同期したモーションを作成することが容易になります。
ちなみにtime_synched()の内部のtoSynchedで、ミリ秒を指定したBPMでの換算数値を返しています。

(これがMGTimeクラスの最終版になります)

class MGTime {
  int pMillis, runMillis, BPM;
  boolean working;

  MGTime() {
    pMillis=millis();
    runMillis=0;
    BPM=60;
    working=false;
  }

  void setBPM(int _BPM) {
    BPM=_BPM;
  }

  void timeStep() {
    if (working) {
      runMillis+=millis()-pMillis;
      pMillis=millis();
    }
  }

  void stop() {
    working=false;
  }
  
  void kill(){
    working=false;
    runMillis=0;
  }

  void start() {
    working=true;
    pMillis=millis();
  }

  int time_normal() {
    return runMillis;
  }

  float time_synched() {
    return this.toSynched(runMillis);
  }
  
  float toNormal(float t){
    return t/(BPM/60.0);
  }
  
  float toSynched(float t){
    return t*(BPM/60.0);
  }
}

setBPMでBPMを指定し、time_synched()でBPM換算した数値が返ります。

このclassを用いたBPM同期モーションの例

使用例です。

import ddf.minim.*;

Minim minim;
AudioPlayer player;
MGTime mgt;

void setup() {
  size(600, 600);
  minim=new Minim(this);
  player=minim.loadFile("music.wav");

  mgt=new MGTime();
  mgt.setBPM(170);

  mgt.start();
  player.play();
}

void draw() {
  mgt.timeStep();
  display((int)mgt.time_synched());
}

void stop() {
  player.close();
  minim.stop();
  super.stop();
}

//--------------------------------------

void display(int t) {
  background(0);

  int turn=1000;
  float size=400*(pow(turn-(t%turn), 2)/pow(turn, 2));

  noStroke();
  fill(255, 0, 0);
  ellipse(width/2.0, height/2.0, size, size);
}


こんな感じになります。
www.youtube.com

fpsを指定して書き出す

前述のストップウォッチのclass(MGTime)にtoSynchedという関数を用意していました。これがfps指定書き出しの際にも使えます。レンダリングfpsに応じた一定の間隔で書き出すことになります。まずは1000.0/fpsでその間隔をミリ秒で算出し、それをBPM換算すると、そのBPMでの書き出し間隔が算出されます。

float interval=mgt.toSynched(1000.0/fps);

for (int i=0; i<frameSum; i++) {
  display((int)(i*interval));
  save("frames/"+nf(i, 4)+".png");
}

サンプル

先ほどのMGTimeクラス最終版を利用した場合のサンプルです。

import ddf.minim.*;

boolean render=true;
//フレーム書き出しと動画への変換を行うかどうか。
//falseにするとプレビューのみを行う

int frameSum=1000;
int fps=60;

Minim minim;
AudioPlayer player;
MGTime mgt;

void setup() {
  size(600, 600);
  minim=new Minim(this);
  player=minim.loadFile("music.wav");

  mgt=new MGTime();
  mgt.setBPM(170);

  if (render) {
    //以前のフレームデータを消去
    launch("C:\\xxxx\\Desktop\\PBGA_step2\\clearFrames");

    //フレーム書き出し
    float interval=mgt.toSynched(1000.0/fps);

    for (int i=0; i<frameSum; i++) {
      display((int)(i*interval));
      if (i%100==0) {
        println("Now I am dumping frame"+i);
      }
      save("frames/"+nf(i, 4)+".png");
    }

    //動画に変換。このbatは命令数が多いのでlaunch()から呼ぶと動作が不安定なので手動でbatを実行する必要があるかも
    launch("C:\\xxxx\\Desktop\\PBGA_step2\\render");
  }
  mgt.start();
  player.play();
}

void draw() {
  mgt.timeStep();
  display((int)mgt.time_synched());
}

void stop() {
  player.close();
  minim.stop();
  super.stop();
}

//--------------------------------------

void display(int t) {
  background(0);

  int turn=1000;
  float size=400*(pow(turn-(t%turn), 2)/pow(turn, 2));

  noStroke();
  fill(255, 0, 0);
  ellipse(width/2.0, height/2.0, size, size);
}

displayでの描画内容は「このclassを用いたBPM同期モーションの例」のものと同じです。ここまで出来たらあとはProcessingでいかにかっこいい描画が出来るかに懸かっています。がんばり太郎