楽器をつくる:シーケンサ編

最初に

このページではプログラミング初学者の人が簡単なミュージックシーケンサを作成するまでをまとめたウェブサイトになります。自作デバイス上でArduinoを動かすことを想定していますが、教材用にProcessingで動作するソフトウェアとして紹介します。配列の宣言の仕方など、Arduinoの場合と少し異なりますので、適時読み替えてください。

設計

まず、今回作成するシーケンサですが、4×4のマトリクス上にスイッチを配したものを想定します。簡単にいうとPocoPocoみたいなものです。このようなシーケンサのベース部分を作るために、まずモジュールベースで作成していきます。

重要なのは想定する一つ一つの部品や機能をソフトウェア上でclassとして作成していってください。いわゆるオブジェクト指向で考えていきます。シーケンサの機能としては、レイヤー階層になっており、各レイヤーごとに4×4のボタンが用意されています。ボタンを押すごとにそのボタンのonとoffを切り替えることができます。onになったボタンはタイムラインに従って自動で発音します。

実際に完成するソフトウェアは下記のようなものです。では一緒にやってみましょう。

最初のコード

まず最初のコードですが、単純な画面上に16個のボタンが表示されるプログラムを作成したいと思います。

sq.pde
void setup()
{
  size(500, 500);
}
 
void draw()
{
}

窓サイズ500×500の画面を作成するだけのものです。ここからはじめていきます。まずはボタンを16個描画する必要があります。ただし、このボタンは一つのオブジェクトになっているので、classとして表現します。ここで、16個のボタンを描画したいわけですから、Buttonというクラスを作り、その中のdraw()を呼びだすことでそれぞれ正しい位置にボタンが描画されればいいわけです。ということは、Button classにはそれぞれの場所を示す座標情報や円の大きさ等の情報を持っている必要があります。 以上のことから記述追加したプログラムは次のとおりです。

sq.pde
class Button {
  Button() { 
  }
  void setup(int _x, int _y, boolean _status)
  {
    x = _x;
    y = _y;
    size = 80;
    status = _status;
  }
  void draw()
  {
    ellipse(x,y,size,size);
  }
  int x;
  int y;
  boolean status;
  int size;
}
 
Button[] button = new Button[16];
 
 
void setup()
{
  size(500, 500);
 
  for ( int i = 0; i < 16; i++ ) {
    button[i] = new Button();
    button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);        
  }
}
 
void draw()
{
  background(0);
  for( int i = 0; i < 16; i++ ){
    button[i].draw();
  }
}

ボタンのON/OFF切り替え

では次はボタンを押すことでON/OFFの切り替えを行ってみます。ボタンに関してはButton Class内に記述します。現時点ではボタンクラスはdraw関数にて描画するだけですが、ボタンのステータスを更新する為にupdate()メソッドを追加します。サンプルではmousePressedのみで判定を行っているので、マウスを押しっぱなしにすると連続してonとoffが切り替わってしまうので、previous_mousePressed というメンバー変数を作成しておき、マウスクリックを「今マウスボタンが押されていて、且つ一つ前のときにはマウスが押されていなかった場合」としてif分にて条件を記述しています。

sq.pde
class Button{
  Button() {
  }
  void setup(int _x, int _y, boolean _status)
  {
    x = _x;
    y = _y;
    size = 80;
    status = _status;
    c = color(50,50,50); // 初期の色はグレー
  }
  void draw()
  {
    fill(c);
    ellipse(x, y, size, size);
  }
  void update()
  {   
    // マウスがボタン上にあるとき(ボタン押下処理)
    if ( dist(x, y, mouseX, mouseY) < size/2 ) {
 
      // マウスボタンが押されている、且つ一つ前の状態では押されていなかったとき
      if ( mousePressed == true && previous_mousePressed == false) {
        status = !status; // falseならtrue, trueならfalseにする
 
        // trueなら明るめのグレーにする
        if( status == true ){
          c = color(200,200,200);
        }
        // falseならグレー
        else{
          c = color(50,50,50);
        }
      }
    }
 
    previous_mousePressed = mousePressed;
  }
 
  int x; // x座標
  int y; // y座標
  color c; // 色
  boolean status; // スイッチの状況(false/true)
  int size; // 円の直径
  boolean previous_mousePressed; // 一つ前のマウスボタン状態を保存しておく
}
 
Button[] button = new Button[16];
 
void setup()
{ 
  size(500, 500); 
  for ( int i = 0; i < 16; i++ ) {
    button[i] = new Button();
    button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
  }
}
void draw()
{
  background(0);
  for ( int i = 0; i < 16; i++ ) {
    button[i].update();
    button[i].draw();
  }
}

ここらで少しButtonクラスの記述が増えて来たので、下記のスクリーンショットのようにクラスの記述を別ファイルに移します。クラスの記述部分をすべてButtonというタブに移します。

タイムライン(自動演奏のための時間)機能を追加

各ボタンのON/OFFの制御に関しては上記までで記述できたので,次にタイムラインを追加します。タイムラインは決められたテンポで順番にボタンのON、OFFに従って自動演奏を行います。そこでまず決められたテンポを算出するクラスとして、Metroクラスを作ります。常時このMetroをupdate()することで、指定テンポでtrueを返す設計にします。まずは次のようなクラスを作成します。下記コードは先程のButtonクラスと同様に、新規タブからMetroとして作成してください。

Metro.pde
// _ms時間が経過したらtrueを返し、その後再度_ms時間が経過するとtrueを返すを繰り返す
class Metro {
  Metro() {
  }
  void set(int _ms) {
    ms = _ms;
    ms_stamp = millis();
  }
  boolean update() {
    if ( (millis()-ms_stamp) > ms ) {
      ms_stamp = millis();
      return true;
    }
    return false;
  }
  int ms;
  long ms_stamp; // long はintよりも桁数の多い整数を扱うときに利用します.
}

次に、このmetro.update()を利用して、シーケンスに合わせて光の移動を行ってみます。sqファイルの先頭に、Metro metro; を宣言して、少々修正を加えたプログラムを下記に示します。実際に実行して動作を確かめてください。

sq.pde
Button[] button = new Button[16];
Metro metro;
int play_pos = 0;
int play_pos_previous = 0;
 
void setup()
{
  size(500, 500);
  for ( int i = 0; i < 16; i++ ) {
    button[i] = new Button();
    button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
  }
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
}
 
 
void draw()
{
  background(0);
  for ( int i = 0; i < 16; i++ ) {
    button[i].update();
    button[i].draw();
  }
  if( metro.update() ){
 
    // 一つ前の該当ボタンは 50 の色に戻す
    button[play_pos_previous].c = color(50);
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は 150 の白色にする
    button[play_pos].c = color(150);
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
}

ここまでで、sq.pde, Button.pde, Metro.pdeにて3つのファイルによって動作するプロジェクトになりました。

発音

それでは、ボタンを押した箇所だけ発音するプログラムまで進めてみましょう。ここまでですでにどのボタンがONになっているか、シーケンスの位置はどこにあるのかがわかっているので、あとはそのタイミングで適切な音を鳴らせばOKです。まずは音を鳴らす代わりにprintで動作をチェックします。どの鳴らすべき音のタイミングの箇所に“Make some sounds”とprintするように修正したほか、play_pos_previousの色設定にも、スイッチのON,OFFによる条件分岐を加えました。

sq.pde
Button[] button = new Button[16];
Metro metro;
int play_pos = 0;
int play_pos_previous = 0;
 
void setup()
{
  size(500, 500);
  for ( int i = 0; i < 16; i++ ) {
    button[i] = new Button();
    button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
  }
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
}
 
 
void draw()
{
  background(0);
  for ( int i = 0; i < 16; i++ ) {
    button[i].update();
    button[i].draw();
  }
  if( metro.update() ){
 
    // ボタンが押されているなら
    if( button[play_pos_previous].status == true ){
      button[play_pos_previous].c = color(200);
    }
    // 一つ前の該当ボタンは 50 の色に戻す
    else{
      button[play_pos_previous].c = color(50);
    }
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は 150 の白色にする
    button[play_pos].c = color(250);
    if( button[play_pos].status == true ){
      println("Make some sounds");
    }
 
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
}

実行してみると、ボタンを押した箇所にシーケンスが来たときのみ、Make some soundsと文字列がデバッグ画面に表示されたかと思います。では次にこのprint文の位置に対して実際に音がなるコードを追加します。MIDIを利用すると広がりがありますが、このページでは学習目的でなるべく簡素にしておきたいので、音声ファイルを再生するだけにしておきます。利用する音声ファイルはMinimライブラリのサンプルにあるDrumMachineの BD.wav(バスドラ), CHH.wav(シンバルハイハット), SD.wav(スネアドラム)ファイルをそのまま利用します。下記においておきます。

ではsound libraryを利用して(Minim Libraryでもよいですが,Version3以上からはsound libraryが公式の音を扱うライブラリになった為)、それらを読み込み、シーケンスのタイミングに合わせて発話するようにします。今回はCHH.wavを利用します。

sq.pde
import processing.sound.*;
SoundFile sound;
 
Button[] button = new Button[16];
Metro metro;
int play_pos = 0;
int play_pos_previous = 0;
 
void setup()
{
  size(500, 500);
  for ( int i = 0; i < 16; i++ ) {
    button[i] = new Button();
    button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
  }
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
  // 再生音声ファイルの読み込み
  sound = new SoundFile(this, "CHH.wav");
}
 
 
void draw()
{
  background(0);
  for ( int i = 0; i < 16; i++ ) {
    button[i].update();
    button[i].draw();
  }
  if( metro.update() ){
 
    // ボタンが押されているなら
    if( button[play_pos_previous].status == true ){
      button[play_pos_previous].c = color(200);
    }
    // 一つ前の該当ボタンは 50 の色に戻す
    else{
      button[play_pos_previous].c = color(50);
    }
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は 150 の白色にする
    button[play_pos].c = color(250);
    if( button[play_pos].status == true ){
      println("Make some sounds");
      sound.play(); // 音を鳴らす
    }
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
}

Layerクラスを考える

さあ、ハイハットのチチチでなんとなくリズム刻めてきました。先程ダウンロードした音声ファイルにはこの他バスドラ、ハイハットがあるので、これらもシーケンス内で再生したいと考えます。しかしこの場合、ハイハットの音に対してどのように他の音を割り当てるかを考えなければいけません。シーケンサでは通常このようなものに対してレイヤー機能を利用します。つまりこれまで作成してきたものはハイハットレイヤーであり、この他に、バスドラ、スネアレイヤーを追加します。レイヤーの概念としてはボタンの上位層にあたるので、ここまでの sq.pde 内の処理をLayerクラスに移築し、Layerクラス内でButtonクラスを扱うようにします。というわけで、Layer.pdeを作成します。これまで通り、新規タブからLayerファイルを作成し、sq.pdeのsetup()での内容をLayerに移行してみます。まずはlayerを配列にせず、一枚だけのレイヤーでbuttonを包括するように記述し直します。新規に作成するLayer.pdeの他、sq.pdeも修正します。修正したコードは下記に示します。sq.pdeがスッキリしたのがわかると思います。

sq.pde
Metro metro;
Layer layer;
 
int play_pos = 0;
int play_pos_previous = 0;
 
void setup()
{
  size(500, 500);
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
  layer = new Layer();
  layer.setup();
}
 
 
void draw()
{
  background(0);
  for ( int i = 0; i < 16; i++ ) {
    layer.button[i].update();
    layer.button[i].draw();
  }
  if( metro.update() ){    
    layer.update();    
  }
}
Layer.pde
import processing.sound.*;
 
class Layer {
 
  Layer() {
  }
 
  void setup()
  {
    button  = new Button[16];
    for ( int i = 0; i < 16; i++ ) {
      button[i] = new Button();
      button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
    }
    // 再生音声ファイルの読み込み
    sound = new SoundFile(sq.this, "CHH.wav");
  }
  void update()
  {
    // ボタンが押されているなら
    if( button[play_pos_previous].status == true ){
      button[play_pos_previous].c = color(200);
    }
    // 一つ前の該当ボタンは 50 の色に戻す
    else{
      button[play_pos_previous].c = color(50);
    }
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は 150 の白色にする
    button[play_pos].c = color(250);
    if( button[play_pos].status == true ){
      println("Make some sounds");
      sound.play();// 音を鳴らす
    }
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
 
  Button[] button;
  SoundFile sound;
  int play_pos;
  int play_pos_previous;
}

上記をもう少し、クラスとしてしっかりまとめてみます。修正したい箇所は以下の2点です。

  1. metroのtrueなタイミングはlayer.update()で勝手に判断してほしい
  2. button[i].draw() を呼び出さず、layer.draw()で一括して呼び出したい。
  3. buttonのstatusのupdate()は別途更新できるようにしたい(Layerが配列になった場合、任意のlayerのbutton statusだけ変更したいため)

ただし、layer.update()は現在Metroのtrueなタイミングに合わせて更新しているため、素早い更新が必要なbutton.update()との整合が取れていません。なので、ここで、layer.update()の仕様を少し変更します。MetroのtrueタイミングをLayer.update()内で判断できるようにします。以下が修正後の sq.pde, Layer.pde になります。

sq.pde
Metro metro;
Layer layer;
 
int play_pos = 0;
int play_pos_previous = 0;
 
void setup()
{
  size(500, 500);
  frameRate(1000); // Metroを正確に刻むために目一杯FPSを上げる
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
  layer = new Layer();
  layer.setup();
}
 
 
void draw()
{
  background(0);
 
  layer.updateButtonStatus();
  layer.update(metro.update());
  layer.draw();
}
Layer.pde
import processing.sound.*;
 
class Layer {
 
  Layer() {
  }
 
  void setup()
  {
    button  = new Button[16];
    for ( int i = 0; i < 16; i++ ) {
      button[i] = new Button();
      button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
    }
    // 再生音声ファイルの読み込み
    sound = new SoundFile(sq2.this, "CHH.wav");
  }
  void updateButtonStatus()
  {
    for ( int i = 0; i < 16; i++ ) {
      button[i].update();
    }
  }
  void update(boolean _metro)
  {
    if ( _metro == false ) {
 
      return;
    }
 
    // ボタンが押されているなら
    if ( button[play_pos_previous].status == true ) {
      button[play_pos_previous].c = color(200);
    }
    // 一つ前の該当ボタンは 50 の色に戻す
    else {
      button[play_pos_previous].c = color(50);
    }
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if ( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は 150 の白色にする
    button[play_pos].c = color(250);
    if ( button[play_pos].status == true ) {
      sound.play();// 音を鳴らす
    }
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
 
  void draw()
  {
    for ( int i =0; i < 16; i++ ) {
      button[i].draw();
    }
  }
  Button[] button;
  SoundFile sound;
  int play_pos;
  int play_pos_previous;
}

layer.update()関数にmetro.update()を渡すだけですべての更新ができるようになったほか、レイヤー描画もdraw()関数で一度にできるようになりました。結果として sq.pdeのdraw()関数が非常にスッキリしました。

Layerの配列化

さて、ここまで来たところで、現在のlayerを配列で宣言し直すことで、複数の楽器を割り当てることができます。ただし現状のままではどの配列のレイヤーにしても必ずハイハットの音になってしますので、layer.setup()の際に引数として読み込むファイル名を指定できるようにします。そしてlayerを配列として、それぞれ別の音を読み込みむように設定します。またキーボードの1,2,3にそれぞれのレイヤー切り替えを割り当てて置きます。修正するのは sq.pdeとLayer.pdeです。

sq.pde
Metro metro;
Layer[] layer;
 
int play_pos = 0;
int play_pos_previous = 0;
int selected_layer = 0;
 
void setup()
{
  size(500, 500); 
  frameRate(1000); // シーケンスを正確に動かすために目一杯早くする
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
  layer = new Layer[3];
 
  for ( int i = 0; i < 3; i++ ) {
    layer[i] = new Layer();
  }
  layer[0].setup("CHH.wav");
  layer[1].setup("SD.wav");
  layer[2].setup("BD.wav");
}
 
 
void draw()
{
  background(0);
 
  layer[selected_layer].updateButtonStatus();
  boolean flg = metro.update();
  for( int i = 0; i < 3; i++ ){
    layer[i].update(flg);
  }
  layer[selected_layer].draw();
 
  fill(255);
  text("Layer: " + str(selected_layer+1), 20, 20);
 
}
 
void keyPressed()
{
  switch(key)
  {
  case '1':
    selected_layer = 0;
    break;
  case '2':
    selected_layer = 1;
    break;
  case '3':
    selected_layer = 2;
    break;
  }
 
  // Screenshot用
  if ( key == 's' ) {
    String name = year()+"-"+month()+"-"+day()+"-"+hour()+"-"+minute()+"-"+second() + ".png";
    save(name);
  }
}
Layer.pde
import processing.sound.*;
 
class Layer {
 
  Layer() {
  }
 
  void setup(String filename)
  {
    button  = new Button[16];
    for ( int i = 0; i < 16; i++ ) {
      button[i] = new Button();
      button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
    }
    // 再生音声ファイルの読み込み
    sound = new SoundFile(sq.this, filename);
  }
  void updateButtonStatus()
  {
    for ( int i = 0; i < 16; i++ ) {
      button[i].update();
    }
  }
  void update(boolean _metro)
  {
    if ( _metro == false ) {
 
      return;
    }
 
    // ボタンが押されているなら
    if ( button[play_pos_previous].status == true ) {
      button[play_pos_previous].c = color(200);
    }
    // 一つ前の該当ボタンは 50 の色に戻す
    else {
      button[play_pos_previous].c = color(50);
    }
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if ( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は 150 の白色にする
    button[play_pos].c = color(250);
    if ( button[play_pos].status == true ) {
      sound.play();// 音を鳴らす
    }
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
 
  void draw()
  {
    for ( int i =0; i < 16; i++ ) {
      button[i].draw();
    }
  }
  Button[] button;
  SoundFile sound;
  int play_pos;
  int play_pos_previous;
}

最後に少しだけカスタマイズ

以上で基本的な機能を実装することができました。最後に少しだけカスタマイズを行ってみます。

  1. いまどのレイヤーを選択しているか分かりづらいので、レイヤーごとにシーケンスの色を変更する(1:赤、2:緑、3:青)

この機能を実装するには Layerクラス内にcolorを保持しておく必要があります。特に今回の場合はシーケンス色はデフォルトの白ではなく、 カスタムできるようにしておく必要があるわけです。下記のように sq.pde と Layer.pde を修正すれば良いです。

sq.pde
Metro metro;
Layer[] layer;
 
int play_pos = 0;
int play_pos_previous = 0;
int selected_layer = 0;
 
void setup()
{
  size(500, 500); 
  frameRate(1000); // シーケンスを正確に動かすために目一杯早くする
 
  // Metroの初期化
  metro = new Metro();
  metro.set(100); // 100[ms]おきにtrueを返す
 
  layer = new Layer[3];
 
  for ( int i = 0; i < 3; i++ ) {
    layer[i] = new Layer();
  }
  layer[0].setup("CHH.wav");
  layer[0].setSqColor(color(250,0,0));
 
  layer[1].setup("SD.wav");
  layer[1].setSqColor(color(0,250,0));
 
  layer[2].setup("BD.wav");
  layer[2].setSqColor(color(0,0,250));
}
 
 
void draw()
{
  background(0);
 
  layer[selected_layer].updateButtonStatus();
  boolean flg = metro.update();
  for( int i = 0; i < 3; i++ ){
    layer[i].update(flg);
  }
  layer[selected_layer].draw();
 
  fill(255);
  text("Layer: " + str(selected_layer+1), 20, 20);
 
}
 
void keyPressed()
{
  switch(key)
  {
  case '1':
    selected_layer = 0;
    break;
  case '2':
    selected_layer = 1;
    break;
  case '3':
    selected_layer = 2;
    break;
  }
 
  // Screenshot用
  if ( key == 's' ) {
    String name = year()+"-"+month()+"-"+day()+"-"+hour()+"-"+minute()+"-"+second() + ".png";
    save(name);
  }
}
Layer.pde
import processing.sound.*;
 
class Layer {
 
  Layer() {
  }
 
  void setup(String filename)
  {
    button  = new Button[16];
    for ( int i = 0; i < 16; i++ ) {
      button[i] = new Button();
      button[i].setup(100+(i%4)*100, 100+(i/4)*100, false);
    }
    // 再生音声ファイルの読み込み
    sound = new SoundFile(sq.this, filename);
 
    color_sq = color(250);
  }
  void updateButtonStatus()
  {
    for ( int i = 0; i < 16; i++ ) {
      button[i].update();
    }
  }
  void update(boolean _metro)
  {
    if ( _metro == false ) {
 
      return;
    }
 
    // ボタンが押されているなら
    if ( button[play_pos_previous].status == true ) {
      button[play_pos_previous].c = color(200);
    }
    // 一つ前の該当ボタンは 50 の色に戻す
    else {
      button[play_pos_previous].c = color(50);
    }
 
    // シーケンス箇所を一つ進める
    play_pos++;
    if ( play_pos > 15 )play_pos = 0;
 
    // そのシーケンス箇所は color_sq の白色にする
    button[play_pos].c = color(color_sq);
    if ( button[play_pos].status == true ) {
      sound.play();// 音を鳴らす
    }
 
    // 古いシーケンス場所として保存しておく。
    play_pos_previous = play_pos;
  }
 
  void draw()
  {
    for ( int i =0; i < 16; i++ ) {
      button[i].draw();
    }
  }
 
  void setSqColor(color _color){
    color_sq = _color;
  }
  Button[] button;
  SoundFile sound;
  int play_pos;
  int play_pos_previous;
  color color_sq;
}

最後に出来上がったファイル一式を置いておきます。

  • 完成したProcessingファイルの一式:sq.zip
  • /home/users/2/lolipop.jp-4404d470cd64c603/web/ws/data/pages/楽器をつくる/シーケンサ編.txt
  • 最終更新: 2020/06/20 11:28
  • by baba