aa develop

開発と成長

Processingでリズムマシンをつくる

今回は、ProcessingのMinimとControlP5を使ってリズムマシンを作りたいと思います。 リズムマシンは、ドラムパターンを自動で演奏する楽器です。

メトロノームをつくる

まずは、これから作るリズムマシンの基本を理解するためにメトロノームを作ってみたいと思います。 次のプログラムは4/4拍子のリズムを刻むメトロノームです。 「BPM」ノブでテンポを変更することができます。

f:id:aa_debdeb:20161120091840j:plain

/*
* sample01
*/

import ddf.minim.spi.*;
import ddf.minim.signals.*;
import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.ugens.*;
import ddf.minim.effects.*;

import controlP5.*;

Minim minim;
AudioOutput out;
int bpm;
int beat = 0;

ControlP5 cp5;

void setup(){
  size(300, 200);
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("bpm")
     .setRange(60, 240)
     .setValue(120)
     .setPosition(width / 2 - knobRadius, height / 2 - knobRadius)
     .setRadius(knobRadius);
  minim = new Minim(this);
  out = minim.getLineOut();
  out.setTempo(bpm);
  out.playNote(0, 1.0, new Metronome());
}

void draw(){

}

class Metronome implements Instrument {
  
  void noteOn(float duration){
    if(beat == 0){
      out.playNote(0.0, 0.5, "A5");
    } else {
      out.playNote(0.0, 0.5, "A4");
    }
  }
  
  void noteOff(){
    beat++;
    if(beat == 4) beat = 0;
    out.setTempo(bpm);
    out.playNote(0.0, 1.0, this);
  }
}

このプログラムでは、Instrumentインターフェースを継承したMetronomeクラスを作成しています。このMetronomeクラスのインスタンスをsetup関数のplayNoteメソッドで作成しています。 作成したMetronomeインスタンスのnoteOnメソッドがまず呼ばれて、音を鳴らします。 その後、指定した継続期間が終了後に、noteOffメソッドが呼ばれます。 このnoteOffメソッドの中で再び自分自身を呼び出すことにより、自動で繰り返し音が鳴るようにしています。

テンポはAudioOutputクラスのsetTempoメソッドで変更します。 ここで設定した値はplayNoteメソッド内での時間指定に利用されます。 デフォルトでは、BPMが60に設定されているので、playNoteメソッドの引数は単位を1秒とみなして処理されます。 もしBPMを30に設定すると、playNoteの引数は単位が2秒刻みになり、BPMを120にすると単位が0.5秒刻みになります。

このプログラムでは、変数beatにより拍子を管理し、小節の頭に音程が変わるようにしています。

16ビートを刻む

次のプログラムは先程のメトロノームプログラムを16分音符刻みで音がでるようにしたものです。

f:id:aa_debdeb:20161120092554j:plain

/*
* sample02
*/

import ddf.minim.spi.*;
import ddf.minim.signals.*;
import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.ugens.*;
import ddf.minim.effects.*;

import controlP5.*;

Minim minim;
AudioOutput out;
int bpm;
int beat = 0;

ControlP5 cp5;

void setup(){
  size(300, 200);
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("bpm")
     .setRange(60, 240)
     .setValue(120)
     .setPosition(width / 2 - knobRadius, height / 2 - knobRadius)
     .setRadius(knobRadius);
  minim = new Minim(this);
  out = minim.getLineOut();
  out.setTempo(bpm);
  out.playNote(0, 0.25, new Metronome());
}

void draw(){

}

class Metronome implements Instrument {
  
  void noteOn(float duration){
    if(beat == 0){
      out.playNote(0.0, 0.3, "A5");
    } else if(beat % 4 == 0){
      out.playNote(0.0, 0.3, "A4");
    } else {
      out.playNote(0.0, 0.3, "A3");    
    }
  }
  
  void noteOff(){
    beat++;
    if(beat == 16) beat = 0;
    out.setTempo(bpm);
    out.playNote(0.0, 0.25, this);
  }
}

BPMで指定した1ビートのさらに4分の1の間隔で音を鳴らせば、16ビートを刻むことができます。 そのために、playNoteメソッドの第2引数に「1」ではなく「0.25」を指定しています。

16ステップ・シーケンサーをつくる

ここまででリズムを刻むことはできるようになりました。 このリズムに合わせて、鳴らす音を変えることにより、シーケンサー作ることができます。

次のプログラムは、ControlP5のGUIを使って各ビートについて音を鳴らすかと鳴らす音の音程を選択できるようにしたものです。

f:id:aa_debdeb:20161120093015j:plain

/*
* sample03
*/

import ddf.minim.spi.*;
import ddf.minim.signals.*;
import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.ugens.*;
import ddf.minim.effects.*;

import controlP5.*;

Minim minim;
AudioOutput out;
int bpm;
int beat = 0;

ControlP5 cp5;
Knob[] notes;
Toggle[] toggles;

String[] noteNames = {"C4", "D4", "E4", "F4", "G4", "A4", "B4"};

int[] defaultNotes = {0, 1, 2, 0, 0, 1, 2, 0, 4, 2, 1, 0, 1, 2, 1, 0};
boolean[] defaultToggles = {true, true, true, false, true, true, true, false, true, true, true, true, true, true, true, false};

void setup(){
  size(800, 500);
  cp5 = new ControlP5(this);
  notes = new Knob[16];
  toggles = new Toggle[16];
  float knobRadius = 25;
  PVector toggleSize = new PVector(30, 20);
  cp5.addKnob("bpm")
     .setRange(60, 240)
     .setValue(120)
     .setPosition(width / 2 - knobRadius, height / 4 * 1 - knobRadius)
     .setRadius(knobRadius);
  for(int i = 0; i < 16; i++){
    notes[i] = cp5.addKnob("tone" + i)
                  .setLabel("")
                  .setRange(0, 6)
                  .setValue(defaultNotes[i])
                  .setPosition(width / (8 + 1) * (1 + (i < 8? i: i % 8)) - knobRadius, height / 4 * (i < 8? 2: 3) - knobRadius)
                  .setRadius(knobRadius)
                  .setNumberOfTickMarks(6)
                  .setTickMarkLength(5)
                  .snapToTickMarks(true);        
    toggles[i] = cp5.addToggle("toggle" + i)
                  .setLabel("")
                  .setValue(defaultToggles[i])
                  .setPosition(width / (8 + 1) * (1 + (i < 8? i: i % 8)) - toggleSize.x / 2, height / 4 * (i < 8? 2.5: 3.5) - toggleSize.y / 2)
                  .setSize(int(toggleSize.x), int(toggleSize.y));
  }
  minim = new Minim(this);
  out = minim.getLineOut();
  out.setTempo(bpm);
  out.playNote(0, 0.25, new Metronome());
}

void draw(){

}

class Metronome implements Instrument {
  
  void noteOn(float duration){
    if(toggles[beat].getBooleanValue()){
      out.playNote(0.0, 0.3, noteNames[int(notes[beat].getValue())]);
    }
  }
  
  void noteOff(){
    beat++;
    if(beat == 16) beat = 0;
    out.setTempo(bpm);
    out.playNote(0.0, 0.25, this);
  }
}

配列で各ビートに対応したToggleインスタンスとKnobインスタンスを管理しています。 noteOnメソッド内でその値を参照して鳴らす音程を設定しています。

リズムマシンをつくる

このシーケンサーの鳴らす音をドラム音にすると、リズムマシンにすることができます。 次のプログラムでは、キック、スネア、ハイハットについて、それぞれ各ビートで鳴らすかを選択できます。

f:id:aa_debdeb:20161120094618j:plain

/*
* sample04
*/

import ddf.minim.spi.*;
import ddf.minim.signals.*;
import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.ugens.*;
import ddf.minim.effects.*;

import controlP5.*;

Minim minim;
AudioOutput out;
Sampler kick, snare, hihat;
int bpm;
int beat = 0;

ControlP5 cp5;
Toggle[] kickToggles;
Toggle[] snareToggles;
Toggle[] hihatToggles;

void setup(){
  size(800, 500);
  cp5 = new ControlP5(this);
  kickToggles = new Toggle[16];
  snareToggles = new Toggle[16];
  hihatToggles = new Toggle[16];
  float knobRadius = 25;
  PVector toggleSize = new PVector(30, 20);
  cp5.addKnob("bpm")
     .setRange(60, 240)
     .setValue(120)
     .setPosition(width / 2 - knobRadius, height / 5 * 1 - knobRadius)
     .setRadius(knobRadius);
  for(int i = 0; i < 16; i++){
    kickToggles[i] = cp5.addToggle("kick" + (i + 1))
                        .setValue(false)
                        .setPosition(width / (16 + 1) * (i + 1) - toggleSize.x / 2, height / 5 * 2 - toggleSize.y / 2)
                        .setSize(int(toggleSize.x), int(toggleSize.y));       
    snareToggles[i] = cp5.addToggle("snare" + (i + 1))
                        .setValue(false)
                        .setPosition(width / (16 + 1) * (i + 1) - toggleSize.x / 2, height / 5 * 3 - toggleSize.y / 2)
                        .setSize(int(toggleSize.x), int(toggleSize.y));       
    hihatToggles[i] = cp5.addToggle("hihat" + (i + 1))
                        .setValue(false)
                        .setPosition(width / (16 + 1) * (i + 1) - toggleSize.x / 2, height / 5 * 4 - toggleSize.y / 2)
                        .setSize(int(toggleSize.x), int(toggleSize.y));       
  }
  minim = new Minim(this);
  out = minim.getLineOut();
  kick = new Sampler("kick.wav", 8, minim);
  kick.patch(out);
  snare = new Sampler("snare.wav", 8, minim);
  snare.patch(out);
  hihat = new Sampler("hihat.wav", 8, minim);
  hihat.patch(out);
  out.setTempo(bpm);
  out.playNote(0, 0.25, new RhythmMachine());
}

void draw(){

}

class RhythmMachine implements Instrument {
  
  void noteOn(float duration){
    if(kickToggles[beat].getBooleanValue()){
      kick.trigger();
    }
    if(snareToggles[beat].getBooleanValue()){
      snare.trigger();
    }
    if(hihatToggles[beat].getBooleanValue()){
      hihat.trigger();
    }
  }
  
  void noteOff(){
    beat++;
    if(beat == 16) beat = 0;
    out.setTempo(bpm);
    out.playNote(0.0, 0.25, this);
  }
}

サウンドファイルを再生するのには、Samplerクラスを利用しています。 Samplerクラスについては、第4回目の記事「サンプラーをつくる」を確認してください。

サンプラー付きリズムマシンをつくる

最後に、第4回で作ったサンプラーを組み合わせて、自分でサンプリングした音でリズムを作れるようにしてみました。 次のプログラムでは、最大3つの音をサンプリングしてリズムを作ることができます。 各サンプルについて、「RECORD」ボタンを押すと録音を開始し、もう一度「RECORD」ボタンを押すと録音を終了します。 「RATE」ノブで、サンプルの再生スピードを変更することができます。

f:id:aa_debdeb:20161120094816p:plain

/*
* sample05
*/

import ddf.minim.spi.*;
import ddf.minim.signals.*;
import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.ugens.*;
import ddf.minim.effects.*;

import controlP5.*;

Minim minim;
AudioInput in;
AudioOutput out;
AudioRecorder[] recorders;
Sampler[] samplers;

int bpm;
int beat = 0;

ControlP5 cp5;
Knob[] sampleRates;
Toggle[][] toggles;

void setup(){
  size(800, 500);
  cp5 = new ControlP5(this);
  sampleRates = new Knob[3];
  toggles = new Toggle[3][16];
  float knobRadius = 25;
  PVector toggleSize = new PVector(30, 20);
  cp5.addKnob("bpm")
     .setRange(60, 240)
     .setValue(120)
     .setPosition(width / 5.0 * 1 - knobRadius, height / 6 * 1.5 - knobRadius)
     .setRadius(knobRadius);
  for(int si = 0; si < 3; si++){
    cp5.addBang("bangRecord" + (si + 1))
       .setLabel("record" + si)
       .setPosition(width / 5.0 * (2 + si) - toggleSize.x / 2, height / 6 * 1 - toggleSize.y / 2)
       .setSize(int(toggleSize.x), int(toggleSize.y));  
    sampleRates[si] = cp5.addKnob("rate" + (si + 1))
       .setRange(-1.0, 1.0)
       .setValue(0)
       .setPosition(width / 5.0 * (2 + si) - knobRadius, height / 6 * 2 - knobRadius)
       .setRadius(knobRadius);       
  }
  for(int si = 0; si < 3; si++){
    for(int bi = 0; bi < 16; bi++){
      toggles[si][bi] = cp5.addToggle( + (si + 1) + "-" + (bi + 1))
                         .setValue(false)
                         .setPosition(width / (16 + 1) * (bi + 1) - toggleSize.x / 2, height / 6 * (si + 3) - toggleSize.y / 2)
                         .setSize(int(toggleSize.x), int(toggleSize.y));            
    }
  }
  minim = new Minim(this);
  in = minim.getLineIn();
  out = minim.getLineOut();
  recorders = new AudioRecorder[3];
  samplers = new Sampler[3];
  out.setTempo(bpm);
  out.playNote(0, 0.25, new RhythmMachine());
}

void controlEvent(ControlEvent theEvent) {
  for (int si = 0; si < 3; si++) {
    if (theEvent.getController().getName().equals("bangRecord" + (si + 1))) {
      if(recorders[si] != null && recorders[si].isRecording()){
        recorders[si].endRecord();
        recorders[si].save();
        samplers[si] = new Sampler("sample" + si + ".wav", 8, minim);
        samplers[si].patch(out);
      } else {
        recorders[si] = minim.createRecorder(in, "sample" + si + ".wav");
        recorders[si].beginRecord();
      }
    }
  }
}

void draw(){
  background(200);
  for(int si = 0; si < 3; si++){
    if(recorders[si] != null && recorders[si].isRecording()){
      fill(255, 50, 50);
      noStroke();
      ellipse(width / 5.0 * (2 + si), height / 6 * 0.5, 15, 15);
    }
  }
}

class RhythmMachine implements Instrument {
  
  void noteOn(float duration){
    for(int si = 0; si < 3; si++){
      if(toggles[si][beat].getBooleanValue()){
        Sampler sampler = samplers[si];
        if(sampler != null){
          sampler.rate.setLastValue(pow(10.0, sampleRates[si].getValue()));
          sampler.trigger();
        }
      }
    }
  }
  
  void noteOff(){
    beat++;
    if(beat == 16) beat = 0;
    out.setTempo(bpm);
    out.playNote(0.0, 0.25, this);
  }
}

この記事はProcessingで楽器をつくろうの第6回目です。