aa develop

開発と成長

Processingでシンセサイザーをつくる

Processingで楽器をつくろう」の最後の記事です。

最後は、倍音を含んだ音を加工して音をつくる減算式シンセサイザーをつくろうと思います。

シンセサイザーの構成

次の図は、今回作成するシンセサイザーの構成を表しています。

f:id:aa_debdeb:20161123132642j:plain

太い矢印が音声信号、細い矢印が制御信号の流れを表しています。 各部分の役割は次のとおりです。

  • キーボード:発振周波数を入力する
  • VCO:基本波形をつくる
  • VCF:波形を加工する
  • VCA:音量を制御する
  • EG:音量に時間的な変化を与える
  • LFO:音量や音程に周期的な変化を与える
  • スピーカー:音を出力する

各部分について、順番に作っていきたいと思います。

キーボードとVCOをつくる

まずは、図の赤枠で示したキーボードとVCOをつくって、それをもとにスピーカーから音を出したいと思います。

f:id:aa_debdeb:20161123132713j:plain

「VCO WAVE」で波形を矩形波とノコギリ波から選択することができます。 矩形波とノコギリ波は倍音を多く含んでおり、VCFで加工すると音色が変化しやすいので、減算方式のシンセサイザーではよく用いられます。

OCTAVE」で鳴らす音程を変更することができます。 キーボードの対応した部分を押すと、対応した音を鳴らすことができます。

f:id:aa_debdeb:20161123132858j: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;
ControlP5 cp5;

// variance for VCO
Oscil oscil;
int octave;
// variance for keyboard
boolean isKeyLocked;
char lockedKey;

void setup(){
  size(800, 500);
  textAlign(CENTER);
  fill(255);
  
  minim = new Minim(this);
  out = minim.getLineOut();
  oscil = new Oscil(440, 1.0, Waves.SQUARE);

  isKeyLocked = false;
  
  cp5 = new ControlP5(this);
  float knobRadius = 25;
  PVector toggleSize = new PVector(40, 20);
  // GUI for VCO
  text("VCO", width / 4.0 * 1.0, height / 4.0 * 0.5);
  cp5.addToggle("setVcoWave")
     .setLabel("VCO WAVE")
     .setValue(true)
     .setPosition(width / 4.0 * 1.0 - toggleSize.x / 2.0, height / 4.0 * 1.0 - toggleSize.y / 2.0)
     .setSize(int(toggleSize.x), int(toggleSize.y))
     .setMode(ControlP5.SWITCH);
  cp5.addKnob("octave")
     .setLabel("OCTAVE")
     .setRange(2, 6)
     .setValue(4)
     .setPosition(width / 4.0 * 1.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius)
     .setNumberOfTickMarks(4)
     .setTickMarkLength(5)
     .snapToTickMarks(true); 

}

// functions for VCO
void setVcoWave(boolean value){
  if(value){
    oscil.setWaveform(Waves.SQUARE);
  } else {
    oscil.setWaveform(Waves.SAW);  
  }
}

void keyPressed(){
  if(!isKeyLocked){
    String tone = "";
    switch(key){
    case 'z':
      tone += "C"; break;
    case 's':
      tone += "C#"; break;
    case 'x':
      tone += "D"; break;
    case 'd':
      tone += "D#"; break;
    case 'c':
      tone += "E"; break;
    case 'v':
      tone += "F"; break;
    case 'g':
      tone += "F#"; break;
    case 'b':
      tone += "G"; break;
    case 'h':
      tone += "G#"; break;
    case 'n':
      tone += "A"; break;
    case 'j':
      tone += "A#"; break;
    case 'm':
      tone += "B"; break;
    }
    if(tone != ""){
      oscil.setFrequency(Frequency.ofPitch(tone + octave).asHz());
      oscil.patch(out);
      isKeyLocked = true;
      lockedKey = key;
    }
  }
}

void keyReleased(){
  if(isKeyLocked && key == lockedKey){
    oscil.unpatch(out);
    isKeyLocked = false;
  }
}

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);
//  }  
}

プログラムの基本は、第4回の「Processingでキーボードを鍵盤にする」で作成したものです。 そのときはキーボードの代わりにInstrumentインターフェースを利用しましたが、今回は利用しませんでした。 その代わりに、keyPressed()とkeyReleased()を使いました。 これにより、キーを押している間は音を鳴らし続け、キーを離すと音が止まるようにしました。

VCFをつくる

次に赤枠で示したVCFをつくります。すでに作成した箇所は黒の太枠で示しています。

f:id:aa_debdeb:20161123132729j:plain

VCFでは、VCOで作成した波形をフィルターにより加工します。 これにより、出力する音の音色を変えることができます。 VCFで最もよく用いられるフィルターは、ローパス・フィルターです。

次のプログラムは、前に作成したVCOプログラムにローパス・フィルターのVCFを追加したものです。 「CUTOFF FREQUENCY」ノブでフィルターのカットオフ周波数を変更することができます。 「RESONANCE」ノブでフィルターのレゾナンスを変更することができます。

f:id:aa_debdeb:20161123132913j: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;
ControlP5 cp5;

// variance for VCO
Oscil oscil;
int octave;
// variance for VCF
MoogFilter filter;
// variance for keyboard
boolean isKeyLocked;
char lockedKey;

void setup(){
  size(800, 500);
  textAlign(CENTER);
  fill(255);
  
  minim = new Minim(this);
  out = minim.getLineOut();
  oscil = new Oscil(440, 1.0, Waves.SQUARE);
  filter = new MoogFilter(10000, 0.0, MoogFilter.Type.LP);
  oscil.patch(filter);

  isKeyLocked = false;
  
  cp5 = new ControlP5(this);
  float knobRadius = 25;
  PVector toggleSize = new PVector(40, 20);
  // GUI for VCO
  text("VCO", width / 4.0 * 1.0, height / 4.0 * 0.5);
  cp5.addToggle("setVcoWave")
     .setLabel("VCO WAVE")
     .setValue(true)
     .setPosition(width / 4.0 * 1.0 - toggleSize.x / 2.0, height / 4.0 * 1.0 - toggleSize.y / 2.0)
     .setSize(int(toggleSize.x), int(toggleSize.y))
     .setMode(ControlP5.SWITCH);
  cp5.addKnob("octave")
     .setLabel("OCTAVE")
     .setRange(2, 6)
     .setValue(4)
     .setPosition(width / 4.0 * 1.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius)
     .setNumberOfTickMarks(4)
     .setTickMarkLength(5)
     .snapToTickMarks(true); 
  // GUI for VCF
  text("VCF", width / 4.0 * 2.0, height / 4.0 * 0.5);
  cp5.addKnob("setCutoffFrequency")
     .setLabel("CUTOFF FREQUENCY")
     .setRange(0, 10000)
     .setValue(10000)
     .setPosition(width / 4.0 * 2.0 - knobRadius, height / 4.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("setResonance")
     .setLabel("RESONANCE")
     .setRange(0, 1)
     .setValue(0)
     .setPosition(width / 4.0 * 2.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
}

// functions for VCO
void setVcoWave(boolean value){
  if(value){
    oscil.setWaveform(Waves.SQUARE);
  } else {
    oscil.setWaveform(Waves.SAW);  
  }
}
// functions for VCF
void setCutoffFrequency(float value){
  filter.frequency.setLastValue(value);
}
void setResonance(float value){
  filter.resonance.setLastValue(value);
}

void keyPressed(){
  if(!isKeyLocked){
    String tone = "";
    switch(key){
    case 'z':
      tone += "C"; break;
    case 's':
      tone += "C#"; break;
    case 'x':
      tone += "D"; break;
    case 'd':
      tone += "D#"; break;
    case 'c':
      tone += "E"; break;
    case 'v':
      tone += "F"; break;
    case 'g':
      tone += "F#"; break;
    case 'b':
      tone += "G"; break;
    case 'h':
      tone += "G#"; break;
    case 'n':
      tone += "A"; break;
    case 'j':
      tone += "A#"; break;
    case 'm':
      tone += "B"; break;
    }
    if(tone != ""){
      oscil.setFrequency(Frequency.ofPitch(tone + octave).asHz());
      filter.patch(out);
      isKeyLocked = true;
      lockedKey = key;
    }
  }
}

void keyReleased(){
  if(isKeyLocked && key == lockedKey){
    filter.unpatch(out);
    isKeyLocked = false;
  }
}

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);
//  }  
}

ローパス・フィルターには、第5回「Processingでエフェクターをつくる」でも利用したMoogFilterクラスを利用しました。 MoogFilterの使い方はそちらを参考にしてください。

VCAとEGをつくる

次にVCAとEGを作りたいと思います。

f:id:aa_debdeb:20161123132749j:plain

VCAで音量を、EGで音量の変化を設定します。 この部分により、シンセサイザーに楽器のような音量の変化を加えることができます。 シンセサイザーの一般的なEGは、ADSRを設定します。 今回作成しているシンセサイザーもADSRを使いたいと思います。

各ノブで次の値を制御できます。

  • AMPLITUDE:音量
  • ATTACK:attackの長さ
  • SUSTAIN:sustain時の音量
  • DECAY / RELEAE: decayとreleaseの長さ

f:id:aa_debdeb:20161123132940j: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;
ControlP5 cp5;

// variance for VCO
Oscil oscil;
int octave;
// variance for VCF
MoogFilter filter;
// variance for VCA
ADSR adsr;
Multiplier multiplier;
Knob amplitudeKnob, attackKnob, sustainKnob, decayReleaseKnob;
// variance for keyboard
boolean isKeyLocked;
char lockedKey;

void setup(){
  size(800, 500);
  textAlign(CENTER);
  fill(255);
  
  minim = new Minim(this);
  out = minim.getLineOut();
  oscil = new Oscil(440, 1.0, Waves.SQUARE);
  filter = new MoogFilter(10000, 0.0, MoogFilter.Type.LP);
  multiplier = new Multiplier(1.0);
  adsr = new ADSR(1.0, 1.0, 1.0, 0.5, 1.0);
  oscil.patch(filter).patch(multiplier).patch(adsr);

  isKeyLocked = false;
  
  cp5 = new ControlP5(this);
  float knobRadius = 25;
  PVector toggleSize = new PVector(40, 20);
  // GUI for VCO
  text("VCO", width / 4.0 * 1.0, height / 4.0 * 0.5);
  cp5.addToggle("setVcoWave")
     .setLabel("VCO WAVE")
     .setValue(true)
     .setPosition(width / 4.0 * 1.0 - toggleSize.x / 2.0, height / 4.0 * 1.0 - toggleSize.y / 2.0)
     .setSize(int(toggleSize.x), int(toggleSize.y))
     .setMode(ControlP5.SWITCH);
  cp5.addKnob("octave")
     .setLabel("OCTAVE")
     .setRange(2, 6)
     .setValue(4)
     .setPosition(width / 4.0 * 1.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius)
     .setNumberOfTickMarks(4)
     .setTickMarkLength(5)
     .snapToTickMarks(true); 
  // GUI for VCF
  text("VCF", width / 4.0 * 2.0, height / 4.0 * 0.5);
  cp5.addKnob("setCutoffFrequency")
     .setLabel("CUTOFF FREQUENCY")
     .setRange(0, 10000)
     .setValue(10000)
     .setPosition(width / 4.0 * 2.0 - knobRadius, height / 4.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("setResonance")
     .setLabel("RESONANCE")
     .setRange(0, 1)
     .setValue(0)
     .setPosition(width / 4.0 * 2.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
  // GUI for VCA + EG
  text("VCA + EG", width / 4.0 * 3.0, height / 7.0 * 0.5);
  amplitudeKnob = cp5.addKnob("setAmplitude")
     .setLabel("AMPLITUDE")
     .setRange(0, 1)
     .setValue(0.5)
     .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  attackKnob = cp5.addKnob("setAttack")
                  .setLabel("ATTACK")
                  .setRange(0.01, 5.0)
                  .setValue(1.0)
                  .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 2.0 - knobRadius)
                  .setRadius(knobRadius);
  sustainKnob = cp5.addKnob("setSustain")
                   .setLabel("SUSTAIN")
                   .setRange(0, 1.0)
                   .setValue(0.5)
                   .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 3.0 - knobRadius)
                   .setRadius(knobRadius); 
  decayReleaseKnob = cp5.addKnob("setDecayRelease")
                        .setLabel("DECAY / RELEASE")
                        .setRange(0.01, 5.0)
                        .setValue(1.0)
                        .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 4.0 - knobRadius)
                        .setRadius(knobRadius);  
}

// functions for VCO
void setVcoWave(boolean value){
  if(value){
    oscil.setWaveform(Waves.SQUARE);
  } else {
    oscil.setWaveform(Waves.SAW);  
  }
}
// functions for VCF
void setCutoffFrequency(float value){
  filter.frequency.setLastValue(value);
}
void setResonance(float value){
  filter.resonance.setLastValue(value);
}
// function for VCA + EG
void setAmplitude(float value){
  multiplier.amplitude.setLastValue(value);
}
void setParameters(){
  adsr.setParameters(1.0, attackKnob.getValue(), decayReleaseKnob.getValue(), sustainKnob.getValue(), decayReleaseKnob.getValue(), 0.0, 0.0);
}
void setAttack(){
  setParameters();
}
void setSustain(){
  setParameters();
}
void setDecayRelease(){
  setParameters();
}

void keyPressed(){
  if(!isKeyLocked){
    String tone = "";
    switch(key){
    case 'z':
      tone += "C"; break;
    case 's':
      tone += "C#"; break;
    case 'x':
      tone += "D"; break;
    case 'd':
      tone += "D#"; break;
    case 'c':
      tone += "E"; break;
    case 'v':
      tone += "F"; break;
    case 'g':
      tone += "F#"; break;
    case 'b':
      tone += "G"; break;
    case 'h':
      tone += "G#"; break;
    case 'n':
      tone += "A"; break;
    case 'j':
      tone += "A#"; break;
    case 'm':
      tone += "B"; break;
    }
    if(tone != ""){
      oscil.setFrequency(Frequency.ofPitch(tone + octave).asHz());
      adsr.unpatch(out);
      adsr.noteOn();
      adsr.patch(out);
      isKeyLocked = true;
      lockedKey = key;
    }
  }
}

void keyReleased(){
  if(isKeyLocked && key == lockedKey){
    adsr.unpatchAfterRelease(out);
    adsr.noteOff();
    isKeyLocked = false;
  }
}

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);
//  }  
}

VCAにはMultiplierクラスを、EGにはADSRクラスを利用しました。 それぞれ、第5回「Processingでエフェクターをつくる」、第3回「Processingでキーボードを鍵盤にする」で解説しています。 使い方はそちらを参考にしてください。

LFOをつくる

最後に、残ったLFOを作りたいと思います。

f:id:aa_debdeb:20161123132802j:plain

LFOは「Low Frequency Oscilator」の略で、低い周波数で音程や音量を変調するために用いられます。 LFOでVCOの音程を変調するとビブラートに、VCFのカットオフ周波数を変調するとワウに、VCAを音量を変調するとトレモロになります。 今回作成するシンセサイザーでは、一つのLFOでVCO、VCF、VCAをそれぞれ変調させます。 LFOの波形は「LFO WAVE」で正弦波と矩形波から選択でき、「RATE」ノブで発振周波数を変更することができます。 VCO、VCF、VCAに対する変調の強さは、それぞれ「VCA INTENSITY」、「VCF INTENSITY」、「VCA INTENSITY」で制御することができます。

f:id:aa_debdeb:20161123132956j: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;
ControlP5 cp5;

// variance for VCO
Oscil oscil;
int octave;
// variance for VCF
MoogFilter filter;
Knob cutoffFrequencyKnob;
// variance for VCA
ADSR adsr;
Multiplier multiplier;
Knob amplitudeKnob, attackKnob, sustainKnob, decayReleaseKnob;
// variance for LFO
Oscil vcoLfo, vcfLfo, vcaLfo;
float vcoLfoIntensity;
Knob vcfLfoIntensityKnob, vcaLfoIntensityKnob;
// variance for keyboard
boolean isKeyLocked;
char lockedKey;

void setup(){
  size(800, 500);
  textAlign(CENTER);
  fill(255);
  
  minim = new Minim(this);
  out = minim.getLineOut();
  oscil = new Oscil(440, 1.0, Waves.SQUARE);
  filter = new MoogFilter(10000, 0.0, MoogFilter.Type.LP);
  multiplier = new Multiplier(1.0);
  adsr = new ADSR(1.0, 1.0, 1.0, 0.5, 1.0);
  oscil.patch(filter).patch(multiplier).patch(adsr);
  vcoLfo = new Oscil(10, 0.0, Waves.SINE);
  vcoLfo.patch(oscil.frequency);
  vcfLfo = new Oscil(10, 0.0, Waves.SINE);
  vcfLfo.amplitude.setLastValue(0.0);
  vcfLfo.offset.setLastValue(10000);
  vcfLfo.patch(filter.frequency);
  vcaLfo = new Oscil(10, 0.0, Waves.SINE);
  vcaLfo.amplitude.setLastValue(0.0);
  vcaLfo.offset.setLastValue(1.0);
  vcaLfo.patch(multiplier.amplitude);
  
  isKeyLocked = false;
  
  cp5 = new ControlP5(this);
  float knobRadius = 25;
  PVector toggleSize = new PVector(40, 20);
  // GUI for VCO
  text("VCO", width / 4.0 * 1.0, height / 4.0 * 0.5);
  cp5.addToggle("setVcoWave")
     .setLabel("VCO WAVE")
     .setValue(true)
     .setPosition(width / 4.0 * 1.0 - toggleSize.x / 2.0, height / 4.0 * 1.0 - toggleSize.y / 2.0)
     .setSize(int(toggleSize.x), int(toggleSize.y))
     .setMode(ControlP5.SWITCH);
  cp5.addKnob("octave")
     .setLabel("OCTAVE")
     .setRange(2, 6)
     .setValue(4)
     .setPosition(width / 4.0 * 1.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius)
     .setNumberOfTickMarks(4)
     .setTickMarkLength(5)
     .snapToTickMarks(true); 
  // GUI for VCF
  text("VCF", width / 4.0 * 2.0, height / 4.0 * 0.5);
  cutoffFrequencyKnob = cp5.addKnob("setCutoffFrequency")
                           .setLabel("CUTOFF FREQUENCY")
                            .setRange(0, 10000)
                           .setValue(10000)
                           .setPosition(width / 4.0 * 2.0 - knobRadius, height / 4.0 * 1.0 - knobRadius)
                           .setRadius(knobRadius);
  cp5.addKnob("setResonance")
     .setLabel("RESONANCE")
     .setRange(0, 1)
     .setValue(0)
     .setPosition(width / 4.0 * 2.0 - knobRadius, height / 4.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
  // GUI for VCA + EG
  text("VCA + EG", width / 4.0 * 3.0, height / 7.0 * 0.5);
  amplitudeKnob = cp5.addKnob("setAmplitude")
                     .setLabel("AMPLITUDE")
                     .setRange(0, 1)
                     .setValue(0.5)
                     .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 1.0 - knobRadius)
                     .setRadius(knobRadius);
  attackKnob = cp5.addKnob("setAttack")
                  .setLabel("ATTACK")
                  .setRange(0.01, 5.0)
                  .setValue(1.0)
                  .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 2.0 - knobRadius)
                  .setRadius(knobRadius);
  sustainKnob = cp5.addKnob("setSustain")
                   .setLabel("SUSTAIN")
                   .setRange(0, 1.0)
                   .setValue(0.5)
                   .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 3.0 - knobRadius)
                   .setRadius(knobRadius); 
  decayReleaseKnob = cp5.addKnob("setDecayRelease")
                        .setLabel("DECAY / RELEASE")
                        .setRange(0.01, 5.0)
                        .setValue(1.0)
                        .setPosition(width / 4.0 * 3.0 - knobRadius, height / 7.0 * 4.0 - knobRadius)
                        .setRadius(knobRadius);  
  // GUI for LFO
  text("LFO", width / 2.0, height / 7.0 * 5.0);
  cp5.addToggle("setLfoWave")
     .setLabel("LFO WAVE")
     .setValue(true)
     .setPosition(width / 6.0 * 1.0 - toggleSize.x / 2.0, height / 7.0 * 6.0 - toggleSize.y / 2.0)
     .setSize(int(toggleSize.x), int(toggleSize.y))
     .setMode(ControlP5.SWITCH);
  cp5.addKnob("setLfoRate")
     .setLabel("RATE")
     .setRange(0, 100)
     .setValue(10)
     .setPosition(width / 6.0 * 2.0 - knobRadius, height / 7.0 * 6.0 - knobRadius)
     .setRadius(knobRadius);  
   cp5.addKnob("vcoLfoIntensity")
     .setLabel("VCO INTENSITY")
     .setRange(0.0, 1.0)
     .setValue(0.0)
     .setPosition(width / 6.0 * 3.0 - knobRadius, height / 7.0 * 6.0 - knobRadius)
     .setRadius(knobRadius);  
  vcfLfoIntensityKnob = cp5.addKnob("setVcfIntensity")
                           .setLabel("VCF INTENSiTY")
                           .setRange(0.0, 1.0)
                           .setValue(0.0)
                           .setPosition(width / 6.0 * 4.0 - knobRadius, height / 7.0 * 6.0 - knobRadius)
                           .setRadius(knobRadius);  
   vcaLfoIntensityKnob = cp5.addKnob("setVcaIntensity")
                            .setLabel("VCA INTENSITY")
                            .setRange(0.0, 1.0)
                            .setValue(0.0)
                            .setPosition(width / 6.0 * 5.0 - knobRadius, height / 7.0 * 6.0 - knobRadius)
                            .setRadius(knobRadius); 
}

// functions for VCO
void setVcoWave(boolean value){
  if(value){
    oscil.setWaveform(Waves.SQUARE);
  } else {
    oscil.setWaveform(Waves.SAW);  
  }
}
// functions for VCF
void setCutoffFrequency(float value){
  vcfLfo.amplitude.setLastValue(value / 2.0 * vcfLfoIntensityKnob.getValue());
  vcfLfo.offset.setLastValue(value);
}
void setResonance(float value){
  filter.resonance.setLastValue(value);
}
// function for VCA + EG
void setAmplitude(float value){
  vcaLfo.amplitude.setLastValue(value / 2.0 * vcaLfoIntensityKnob.getValue());
  vcaLfo.offset.setLastValue(value);
}
void setParameters(){
  adsr.setParameters(1.0, attackKnob.getValue(), decayReleaseKnob.getValue(), sustainKnob.getValue(), decayReleaseKnob.getValue(), 0.0, 0.0);
}
void setAttack(){
  setParameters();
}
void setSustain(){
  setParameters();
}
void setDecayRelease(){
  setParameters();
}
// functions for LFO
void setLfoWave(boolean value){
   if(value){
     vcoLfo.setWaveform(Waves.SINE);
     vcfLfo.setWaveform(Waves.SINE);
     vcaLfo.setWaveform(Waves.SINE);
   } else {
     vcoLfo.setWaveform(Waves.SQUARE); 
     vcfLfo.setWaveform(Waves.SQUARE); 
     vcaLfo.setWaveform(Waves.SQUARE); 
   }
}
void setLfoRate(float value){
  vcoLfo.setFrequency(value);
  vcfLfo.setFrequency(value);
  vcaLfo.setFrequency(value);
}
void setVcfIntensity(float value){
  vcfLfo.amplitude.setLastValue(cutoffFrequencyKnob.getValue() / 2.0 * value);
}
void setVcaIntensity(float value){
  vcaLfo.amplitude.setLastValue(amplitudeKnob.getValue() / 2.0 * value);
}

void keyPressed(){
  if(!isKeyLocked){
    String tone = "";
    switch(key){
    case 'z':
      tone += "C"; break;
    case 's':
      tone += "C#"; break;
    case 'x':
      tone += "D"; break;
    case 'd':
      tone += "D#"; break;
    case 'c':
      tone += "E"; break;
    case 'v':
      tone += "F"; break;
    case 'g':
      tone += "F#"; break;
    case 'b':
      tone += "G"; break;
    case 'h':
      tone += "G#"; break;
    case 'n':
      tone += "A"; break;
    case 'j':
      tone += "A#"; break;
    case 'm':
      tone += "B"; break;
    }
    if(tone != ""){
      vcoLfo.amplitude.setLastValue(Frequency.ofPitch(tone + octave).asHz() / 2.0 * vcoLfoIntensity);
      vcoLfo.offset.setLastValue(Frequency.ofPitch(tone + octave).asHz());
      adsr.unpatch(out);
      adsr.noteOn();
      adsr.patch(out);
      isKeyLocked = true;
      lockedKey = key;
    }
  }
}

void keyReleased(){
  if(isKeyLocked && key == lockedKey){
    adsr.unpatchAfterRelease(out);
    adsr.noteOff();
    isKeyLocked = false;
  }
}

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);
//  }  
}

変調の方法は、第5回「Processingでエフェクターをつくる」でトレモロを作ったときと同じです。 今回は、VCOでは音程を変調するためにoscil変数のfrequencyプロパティに、VCFではカットオフ周波数を変調するためにfilter変数のfrequencyプロパティに、VCOで音量を変調するためにmultiplier変数のamplitudeプロパティに、それぞれLFOをパッチしています。

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

Processingでイージング

少し前に、fladdictさんの乱数にコクを出すという話がネットで盛り上がっていました。

乱数にコクを出す方法について - Togetterまとめ

私もかれこれ1年以上、Processingで作品を作ってきましたが、自分の作品のクオリティをもっと上げていくには、こういう細かい動きや配色にこだわっていかないのかなーと思っています。

www.openprocessing.org

そうぼんやりと思っているときに、jQueryを使う機会があり、アニメーションの動き方を設定するイージングというものを知りました。

Easing Function 早見表

Processingでも、今までだったらオブジェクトを線形に動かしていたところを、イージングで動きに変化をつけると面白いのではないかと思い、とりあえずProcessingに上の早見表に載っている30のイージングを移植してみました。

f:id:aa_debdeb:20161119165627j:plain

easing test - OpenProcessing

/*
* easing test
*/

float startX = 150;
float endX = 480;
int duration = 200;
float margin = 30;
float ellipseSize = 20;

void setup(){
  size(1000, 480);
}

void draw(){
  background(255);
  fill(60);
  stroke(60);
  text("1. easeInSine", 20, margin * 1);
  text("2. easeOutSine", 20, margin * 2);
  text("3. easeInOutSine", 20, margin * 3);
  text("4. easeInQuad", 20, margin * 4);
  text("5. easeOutQuad", 20, margin * 5);
  text("6. easeInOutQuad", 20, margin * 6);
  text("7. easeInCubic", 20, margin * 7);
  text("8. easeOutCubic", 20, margin * 8);
  text("9. easeInOutCubic", 20, margin * 9);
  text("10. easeinQuart", 20, margin * 10);
  text("11. easeOutQuart", 20, margin * 11);
  text("12. easeInOutQuart", 20, margin * 12);
  text("13. easeinQuint", 20, margin * 13);
  text("14. iaseOutQuint", 20, margin * 14);
  text("15. easeInOutQuint", 20, margin * 15);
  line(startX, 0, startX, height);
  line(endX, 0, endX, height);
  fill(128, 0, 0);
  noStroke();
  ellipse(easeInSine(frameCount % duration, startX, endX - startX, duration), margin * 1, ellipseSize, ellipseSize);
  ellipse(easeOutSine(frameCount % duration, startX, endX - startX, duration), margin * 2, ellipseSize, ellipseSize);
  ellipse(easeInOutSine(frameCount % duration, startX, endX - startX, duration), margin * 3, ellipseSize, ellipseSize);
  ellipse(easeInQuad(frameCount % duration, startX, endX - startX, duration), margin * 4, ellipseSize, ellipseSize);
  ellipse(easeOutQuad(frameCount % duration, startX, endX - startX, duration), margin * 5, ellipseSize, ellipseSize);
  ellipse(easeInOutQuad(frameCount % duration, startX, endX - startX, duration), margin * 6, ellipseSize, ellipseSize);
  ellipse(easeInCubic(frameCount % duration, startX, endX - startX, duration), margin * 7, ellipseSize, ellipseSize);
  ellipse(easeOutCubic(frameCount % duration, startX, endX - startX, duration), margin * 8, ellipseSize, ellipseSize);
  ellipse(easeInOutCubic(frameCount % duration, startX, endX - startX, duration), margin * 9, ellipseSize, ellipseSize);
  ellipse(easeInQuart(frameCount % duration, startX, endX - startX, duration), margin * 10, ellipseSize, ellipseSize);
  ellipse(easeOutQuart(frameCount % duration, startX, endX - startX, duration), margin * 11, ellipseSize, ellipseSize);
  ellipse(easeInOutQuart(frameCount % duration, startX, endX - startX, duration), margin * 12, ellipseSize, ellipseSize);
  ellipse(easeInQuint(frameCount % duration, startX, endX - startX, duration), margin * 13, ellipseSize, ellipseSize);
  ellipse(easeOutQuint(frameCount % duration, startX, endX - startX, duration), margin * 14, ellipseSize, ellipseSize);
  ellipse(easeInOutQuint(frameCount % duration, startX, endX - startX, duration), margin * 15, ellipseSize, ellipseSize);

  translate(width / 2, 0);
  fill(60);
  stroke(60);
  text("16. easeInExpo", 20, margin * 1);
  text("17. easeOutExpo", 20, margin * 2);
  text("18. easeInOutExpo", 20, margin * 3);
  text("19. easeinCirc", 20, margin * 4);
  text("20. easeOutCirc", 20, margin * 5);
  text("21. easeInOutCirc", 20, margin * 6);
  text("22. easeInBack", 20, margin * 7);
  text("23. easeOutBack", 20, margin * 8);
  text("24. easeInOutBack", 20, margin * 9);
  text("25. easeInElastic", 20, margin * 10);
  text("26. easeOutElastic", 20, margin * 11);
  text("27. easeInOutElastic", 20, margin * 12);
  text("28. easeInBounce", 20, margin * 13);
  text("29. easeOutBounce", 20, margin * 14);
  text("30. easeInOutBounce", 20, margin * 15);
  line(startX, 0, startX, height);
  line(endX, 0, endX, height);
  fill(128, 0, 0);
  noStroke();
  ellipse(easeInExpo(frameCount % duration, startX, endX - startX, duration), margin * 1, ellipseSize, ellipseSize);
  ellipse(easeOutExpo(frameCount % duration, startX, endX - startX, duration), margin * 2, ellipseSize, ellipseSize);
  ellipse(easeInOutExpo(frameCount % duration, startX, endX - startX, duration), margin * 3, ellipseSize, ellipseSize);
  ellipse(easeInCirc(frameCount % duration, startX, endX - startX, duration), margin * 4, ellipseSize, ellipseSize);
  ellipse(easeOutCirc(frameCount % duration, startX, endX - startX, duration), margin * 5, ellipseSize, ellipseSize);
  ellipse(easeInOutCirc(frameCount % duration, startX, endX - startX, duration), margin * 6, ellipseSize, ellipseSize);
  ellipse(easeInBack(frameCount % duration, startX, endX - startX, duration, 1.0), margin * 7, ellipseSize, ellipseSize);
  ellipse(easeOutBack(frameCount % duration, startX, endX - startX, duration, 1.0), margin * 8, ellipseSize, ellipseSize);
  ellipse(easeInOutBack(frameCount % duration, startX, endX - startX, duration, 1.0), margin * 9, ellipseSize, ellipseSize);
  ellipse(easeInElastic(frameCount % duration, startX, endX - startX, duration), margin * 10, ellipseSize, ellipseSize);
  ellipse(easeOutElastic(frameCount % duration, startX, endX - startX, duration), margin * 11, ellipseSize, ellipseSize);
  ellipse(easeInOutElastic(frameCount % duration, startX, endX - startX, duration), margin * 12, ellipseSize, ellipseSize);
  ellipse(easeInBounce(frameCount % duration, startX, endX - startX, duration), margin * 13, ellipseSize, ellipseSize);
  ellipse(easeOutBounce(frameCount % duration, startX, endX - startX, duration), margin * 14, ellipseSize, ellipseSize);
  ellipse(easeInOutBounce(frameCount % duration, startX, endX - startX, duration), margin * 15, ellipseSize, ellipseSize);

}

float easeInSine(float t, float b, float c, float d){
  return -c * cos(t / d * (PI / 2)) + c + b;
}

float easeOutSine(float t, float b, float c, float d){
  return c * sin(t / d * (PI / 2)) + b;
}

float easeInOutSine(float t, float b, float c, float d){
  return -c / 2 * (cos(PI * t / d) - 1) + b;
}

float easeInQuad(float t, float b, float c, float d){
  t /= d;
  return c * t * t + b; 
}

float easeOutQuad(float t, float b, float c, float d){
  t /= d;
  return -c * t * (t - 2) + b; 
}

float easeInOutQuad(float t, float b, float c, float d){
  t /= d / 2;
  if(t < 1)
    return c / 2 * t * t + b;
  t--;
  return -c / 2 * (t * (t - 2) - 1) + b; 
}

float easeInCubic(float t, float b, float c, float d){
  t /= d;
  return c * t * t * t + b; 
}

float easeOutCubic(float t, float b, float c, float d){
  t /= d;
  t--;
  return c * (t * t * t + 1) + b; 
}

float easeInOutCubic(float t, float b, float c, float d){
  t /= d / 2;
  if(t < 1)
    return c / 2 * t * t * t + b;
  t -= 2;
  return c / 2 * (t * t * t + 2) + b; 
}

float easeInQuart(float t, float b, float c, float d){
  t /= d;
  return c * t * t * t * t + b; 
}

float easeOutQuart(float t, float b, float c, float d){
  t /= d;
  t--;
  return -c * (t * t * t * t - 1) + b; 
}

float easeInOutQuart(float t, float b, float c, float d){
  t /= d / 2;
  if(t < 1)
    return c / 2 * t * t * t * t + b;
  t -= 2;
  return -c / 2 * (t * t * t * t - 2) + b; 
}

float easeInQuint(float t, float b, float c, float d){
  t /= d;
  return c * t * t * t * t * t + b; 
}

float easeOutQuint(float t, float b, float c, float d){
  t /= d;
  t--;
  return c * (t * t * t * t  * t + 1) + b; 
}

float easeInOutQuint(float t, float b, float c, float d){
  t /= d / 2;
  if(t < 1)
    return c / 2 * t * t * t * t * t + b;
  t -= 2;
  return c / 2 * (t * t * t * t * t + 2) + b; 
}

float easeInExpo(float t, float b, float c, float d){
  return t == 0 ? b: c * pow(2, 10 * (t / d - 1)) + b;
}

float easeOutExpo(float t, float b, float c, float d){
  return t == d ? b + c: c * (-pow(2, -10 * t / d) + 1) + b;
}

float easeInOutExpo(float t, float b, float c, float d){
  if(t == 0)
    return b;
  if(t == d)
    return b + c;
  t /= d / 2;
  if(t < 1)
    return c / 2 * pow(2, 10 * (t - 1)) + b;
  t--;
  return c / 2 * (-pow(2, -10 * t) + 2) + b;
}

float easeInCirc(float t, float b, float c, float d){
  t /= d;
  return -c * (sqrt(1 - t * t) - 1) + b;
}

float easeOutCirc(float t, float b, float c, float d){
  t /= d;
  t--;
  return c * sqrt(1 - t * t) + b;
}

float easeInOutCirc(float t, float b, float c, float d){
  t /= d / 2;
  if(t < 1)
    return -c / 2 * (sqrt(1 - t * t) - 1) + b;
  t -= 2;
  return c / 2 * (sqrt(1 - t * t) + 1) + b;
}

float easeInBack(float t, float b, float c, float d, float s){
  t /= d;
  return c * t * t * ((s + 1) * t - s) + b;
}

float easeOutBack(float t, float b, float c, float d, float s){
  t /= d;
  t--;
  return c * (t * t * ((s + 1) * t + s) + 1) + b;
}

float easeInOutBack(float t, float b, float c, float d, float s){
  t /= d / 2;
  s *= 1.525;
  if(t < 1) 
    return c / 2 * (t * t * ((s + 1) * t - s)) + b;
  t -= 2;
   return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b;
}

float easeInElastic(float t, float b, float c, float d){
  float s = 1.70158;
  float a = c;
  if(t == 0)
    return b;
  t /= d;
  if(t == 1)
    return b + c;
  float p = d * 0.3;
  if(a < abs(c)){
    a = c;
    s = p / 4;
  } else {
    s = p / (2 * PI) * asin(c / a);
  }
  t--;
  return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * PI) / p)) + b;
}

float easeOutElastic(float t, float b, float c, float d){
  float s = 1.70158;
  float a = c;
  if(t == 0)
    return b;
  t /= d;
  if(t == 1)
    return b + c;
  float p = d * 0.3;
  if(a < abs(c)){
    a = c;
    s = p / 4;
  } else {
    s = p / (2 * PI) * asin(c / a);
  }
  return a * pow(2, -10 * t) * sin((t * d - s) * (2 * PI) / p) + c + b;
}

float easeInOutElastic(float t, float b, float c, float d){
  float s = 1.70158;
  float a = c;
  if(t == 0)
    return b;
  t /= d / 2; 
  if(t == 2)
    return b + c;
  float p = d * (0.3 * 1.5);
  if(a < abs(c)){
    a = c;
    s = p / 4;
  } else {
    s = p / (2 * PI) * asin(c / a);
  }
  if(t < 1){
    t--;
    return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * PI) / p)) + b;
  }
  t--;
  return a * pow(2, -10 * t) * sin((t * d - s) * (2 * PI) / p) * 0.5 + c + b;
}

float easeInBounce(float t, float b, float c, float d){
  return c - easeOutBounce(d - t, 0, c, d) + b;
}

float easeOutBounce(float t, float b, float c, float d){
  t /= d;
  if(t < 1.0 / 2.75){
    return c * (7.5265 * t * t) + b;
  } else if(t < 2.0 / 2.75){
    t -= 1.5 / 2.75;
    return c * (7.5265 * t * t + 0.75) + b;
  } else if(t < 2.5 / 2.75){
    t -= 2.25 / 2.75;
    return c * (7.5265 * t * t + 0.9375) + b;
  }else{
    t -= 2.625 / 2.75;
    return c * (7.5265 * t * t + 0.984375) + b;
  }
}

float easeInOutBounce(float t, float b, float c, float d){
  if(t < d / 2)
    return easeInBounce(t * 2, 0, c, d) * 0.5 + b;
  return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
}

イージングを使うと、動きがこんな感じになります。

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回目です。

Processingでエフェクターをつくる

今回は、Processingでエフェクターを作ろうと思います。 エフェクターは入力した音に音響効果を加えて出力するもので、特にエレキギターエレキベースといった楽器で音を加工するために使われています。 エフェクターの種類は多々ありますが、そのうちのいくつかをProcessingのMinimとControlP5で作ってみたいと思います。

ギター エフェクターについて -【エレキギター博士】

入力した音をそのまま出力する

まずは、第1回で紹介した入力した音をそのままスピーカーから出力する方法を確認しましょう。

/*
* sample01
*/

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

Minim minim;
AudioOutput out;
LiveInput in;

void setup(){
  size(512, 350);
  minim = new Minim(this);
  out = minim.getLineOut();
  AudioStream inputStream = minim.getInputStream(
                            out.getFormat().getChannels(),
                            out.bufferSize(),
                            out.sampleRate(),
                            out.getFormat().getSampleSizeInBits());
  in = new LiveInput(inputStream);
  in.patch(out);
}


void draw(){
  background(128);
  stroke(255);
  for(int i = 0; i < out.bufferSize() - 1; i++){
    line(i, 50 + out.left.get(i) * 50, i + 1,  50 + out.left.get(i + 1) * 50);
    line(i, 150 + out.right.get(i) * 50, i + 1,  150 + out.right.get(i + 1) * 50);
  }
}

Minimではマイクからの入力を利用するためにLiveInputを使います。 このプログラムの入力と出力にPCのマイクとスピーカーで利用するとハウリングが生じる場合があります。 そのときは、イヤフォンやヘッドフォンを使ってください。

ディストーション

まずは、ディストーションを作ってみましょう。 ディストーションは、音を歪ませるエフェクターです。 入力した音を増幅してから、過大部分をクリッピングすることにより、音を歪ませています。 オーバードライブやファズも基本的には同じ処理を行うエフェクターです。

MinimとControlP5で作ったディストーションのプログラムは次のようになります。 「DISTORTION」ノブで値を大きくするほど、歪みが大きくなります。 音を入力すると、上端と下端が平らになっている(クリッピングされている)のが確認できると思います。

f:id:aa_debdeb:20161119153045p: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;
LiveInput in;
WaveShaper shaper;

ControlP5 cp5;

void setup(){
  size(512, 350);
  minim = new Minim(this);
  out = minim.getLineOut();
  AudioStream inputStream = minim.getInputStream(
                            out.getFormat().getChannels(),
                            out.bufferSize(),
                            out.sampleRate(),
                            out.getFormat().getSampleSizeInBits());
  in = new LiveInput(inputStream);
  float[] sawVal= {-1.0, 1.0};
  Wavetable saw = new Wavetable(sawVal);
  shaper = new WaveShaper(0.7, 1.0, saw);
  in.patch(shaper);
  shaper.patch(out);
  
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("setDistortion")
     .setLabel("distortion")
     .setRange(1.0, 100.0)
     .setValue(1.0)
     .setPosition(width / 2 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius);
}

void setDistortion(float value){
  shaper.mapAmplitude.setLastValue(value);
}

void draw(){
  background(128);
  stroke(255);
  for(int i = 0; i < out.bufferSize() - 1; i++){
    line(i, 50 + out.left.get(i) * 50, i + 1,  50 + out.left.get(i + 1) * 50);
    line(i, 150 + out.right.get(i) * 50, i + 1,  150 + out.right.get(i + 1) * 50);
  }
}

今回はディストーション処理を行うために、WaveShaperクラスを利用しています。 WaveShaperクラスはWaveShaperという音響合成のための加工をするクラスです。 Wave Shaperは、あらかじめ入力に対する出力の対応関係と入力の増幅度を決めておき、それもとに入力音を増幅してから出力に変換する処理を行います。

偏ったDTM用語辞典 - ウェーブシェイパー:Wave Shaperとは - DTM / MIDI 用語の意味・解説 | g200kg Music & Software

WaveShaperクラスのコンストラクタの引数は、それぞれ次の値を指定しています。

  • 第1引数:出力音量
  • 第2引数:入力音の増幅度
  • 第3引数:入力音と出力音の対応関係 (Wavetableインスタンス

ディストーションプログラムでは、第3引数として与えた対応関係は「入力音 = 出力音」となるようにして、増幅して限界値を超えた値は限界値となるというWaveShaperクラスの特性を利用してクリッピングを行っています。

WaveShaperクラスはもともとクリッピングではなく音響合成のためのものなので、工夫次第でこれをもとに面白い音を作り出すことができます。

Minim : : WaveShaper

Waveshaper - Wikipedia

ディレイ

ディレイは、入力した音を遅れて出力するエフェクターです。

単発ディレイ

次のプログラムを入力した音を一度だけ遅れて鳴らすディレイプログラムです。 「DELAY TIME」で遅れ時間を指定します。 「DELAY AMP」で遅れた音の音量を指定します。

f:id:aa_debdeb:20161119153346p: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;
LiveInput in;
Delay delay;

ControlP5 cp5;

void setup(){
  size(512, 350);
  minim = new Minim(this);
  out = minim.getLineOut();
  AudioStream inputStream = minim.getInputStream(
                            out.getFormat().getChannels(),
                            out.bufferSize(),
                            out.sampleRate(),
                            out.getFormat().getSampleSizeInBits());
  in = new LiveInput(inputStream);
  delay = new Delay(1.0, 1.0);
  in.patch(delay);
  delay.patch(out);
  
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("setDelayTime")
     .setLabel("dekay time")
     .setRange(0.00, 2.0)
     .setValue(1.0)
     .setPosition(width / 3.0 * 1.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("setDelayAmp")
     .setLabel("delay amp")
     .setRange(0.00, 5.0)
     .setValue(1.0)
     .setPosition(width / 3.0 * 2.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius); 
}

void setDelayTime(float value){
  delay.setDelTime(value);
}

void setDelayAmp(float value){
  delay.setDelAmp(value);
}

void draw(){
  background(128);
  stroke(255);
  for(int i = 0; i < out.bufferSize() - 1; i++){
    line(i, 50 + out.left.get(i) * 50, i + 1,  50 + out.left.get(i + 1) * 50);
    line(i, 150 + out.right.get(i) * 50, i + 1,  150 + out.right.get(i + 1) * 50);
  }
}

ディレイには、MinimのDelayクラスを指定します。 Delayクラスのコンストラクタの第1引数が遅れ時間、第2引数が遅れた音の音量を表しています。

Minim : : Delay

フィードバック・ディレイ

フィードバックディレイは、出力した音を再びディレイに入力する(フィードバック)することにより、音を段々と減衰させていくディレイです。

f:id:aa_debdeb:20161119153408p: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;
LiveInput in;
Delay delay;

ControlP5 cp5;

void setup(){
  size(512, 350);
  minim = new Minim(this);
  out = minim.getLineOut();
  AudioStream inputStream = minim.getInputStream(
                            out.getFormat().getChannels(),
                            out.bufferSize(),
                            out.sampleRate(),
                            out.getFormat().getSampleSizeInBits());
  in = new LiveInput(inputStream);
  delay = new Delay(1.0, 1.0, true);
  in.patch(delay);
  delay.patch(out);
  
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("setDelayTime")
     .setLabel("dekay time")
     .setRange(0.0, 2.0)
     .setValue(1.0)
     .setPosition(width / 3.0 * 1.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("setDelayAmp")
     .setLabel("delay amp")
     .setRange(0.0, 1.0)
     .setValue(0.5)
     .setPosition(width / 3.0 * 2.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius); 
}

void setDelayTime(float value){
  delay.setDelTime(value);
}

void setDelayAmp(float value){
  delay.setDelAmp(value);
}

void draw(){
  background(128);
  stroke(255);
  for(int i = 0; i < out.bufferSize() - 1; i++){
    line(i, 50 + out.left.get(i) * 50, i + 1,  50 + out.left.get(i + 1) * 50);
    line(i, 150 + out.right.get(i) * 50, i + 1,  150 + out.right.get(i + 1) * 50);
  }
}

Delayクラスでフィードバックを行うには、コンストラクタの第3引数にtrueを設定します。 フィードバックをかけた状態で、遅れた音の音量を1より大きくすると、ポジティブフィードバックが発生し、音量が無限大に発散するので注意してください。

フィルター

フィルターは、入力音のうち特定の周波数帯域のみを出力音にするエフェクターです。 フィルターには、次のものがあります。

  • ローパス・フィルター(Low-Pass Filter, LP)
  • ハイパス・フィルター(High-Pass Filter, HP)
  • バンドパス・フィルター(Band-Pass Filter, BP)

ローパス・フィルターは入力音のうち特定の周波数以下の帯域のみを通します。 ハイパス・フィルターは入力音のうち特定の周波数以上の帯域のみを通します。 バンドパス・フィルターは入力音のうち特定の周波数の帯域のみを通します。 この境界値となる周波数をカットオフ周波数と呼びます。

フィルターでは、レゾナンスを設定できることがあります。 レゾナンスを設定することにより、カットオフ周波数付近の音を強調することができます。

Minimでのフィルターは次のようになります。 「FILTER TYPE」でフィルターの種類を選択できます。0がLP、1がHP、2がBPに対応しています。 「CUTOFF FREQUENCY」でカットオフ周波数を指定します。 「RESONANCE」でレゾナンスの値を指定します。 レゾナンスを1に近づけすぎると共鳴が始まり、フィルター自体が音を発してしまいます。

f:id:aa_debdeb:20161119153653p: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;
AudioOutput out;
LiveInput in;
MoogFilter filter;

ControlP5 cp5;

void setup(){
  size(512, 350);
  minim = new Minim(this);
  out = minim.getLineOut();
  AudioStream inputStream = minim.getInputStream(
                            out.getFormat().getChannels(),
                            out.bufferSize(),
                            out.sampleRate(),
                            out.getFormat().getSampleSizeInBits());
  in = new LiveInput(inputStream);
  filter = new MoogFilter(1000, 0.5, MoogFilter.Type.LP);
  in.patch(filter);
  filter.patch(out);
  
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("setFilterType")
     .setLabel("filter type")
     .setRange(0, 2)
     .setValue(0)
     .setPosition(width / 4.0 * 1.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius)
     .setNumberOfTickMarks(2)
     .setTickMarkLength(5)
     .snapToTickMarks(true);
  cp5.addKnob("setCutoffFreq")
     .setLabel("cutoff frequency")
     .setRange(0, 2000)
     .setValue(1000)
     .setPosition(width / 4.0 * 2.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("setResonance")
     .setLabel("resonance")
     .setRange(0.0, 1.0)
     .setValue(0.5)
     .setPosition(width / 4.0 * 3.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius); 
}

void setFilterType(int value){
  switch(value){
  case 0:
    filter.type = MoogFilter.Type.LP;
    break;
  case 1:
    filter.type = MoogFilter.Type.HP;
    break;
  case 2:
    filter.type = MoogFilter.Type.BP;
    break;
  }
}

void setCutoffFreq(float value){
  filter.frequency.setLastValue(value);
}

void setResonance(float value){
  filter.resonance.setLastValue(value);
}

void draw(){
  background(128);
  stroke(255);
  for(int i = 0; i < out.bufferSize() - 1; i++){
    line(i, 50 + out.left.get(i) * 50, i + 1,  50 + out.left.get(i + 1) * 50);
    line(i, 150 + out.right.get(i) * 50, i + 1,  150 + out.right.get(i + 1) * 50);
  }
}

MinimではMoogFilterクラスを利用することでフィルタリング処理を簡単に行うことができます。

Minim : : MoogFilter

トレモロ

トレモロは音量を周期的に上下させ揺れているような効果を与えるエフェクターです。

次のプログラムは、サイン波で入力音を揺らすプログラムです。 「RATE」で音を揺らす早さを指定することができます。 「DEPTH」で変化する音量の幅を指定することができます。

f:id:aa_debdeb:20161119153914p:plain

/*
* sample06
*/

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;
LiveInput in;
Oscil tremolo;
Multiplier multiplier;

ControlP5 cp5;

void setup(){
  size(512, 350);
  minim = new Minim(this);
  out = minim.getLineOut();
  AudioStream inputStream = minim.getInputStream(
                            out.getFormat().getChannels(),
                            out.bufferSize(),
                            out.sampleRate(),
                            out.getFormat().getSampleSizeInBits());
  in = new LiveInput(inputStream);
  tremolo = new Oscil(2.0, 0.5, Waves.SINE);
  tremolo.offset.setLastValue(1.0);
  multiplier = new Multiplier(1.0);
  tremolo.patch(multiplier.amplitude);
  in.patch(multiplier);
  multiplier.patch(out);
  
  cp5 = new ControlP5(this);
  float knobRadius = 40;
  cp5.addKnob("setRate")
     .setLabel("rate")
     .setRange(0, 10.0)
     .setValue(2.0)
     .setPosition(width / 3.0 * 1.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("setDepth")
     .setLabel("depth")
     .setRange(0, 1.0)
     .setValue(0.5)
     .setPosition(width / 3.0 * 2.0 - knobRadius, 250 - knobRadius)
     .setRadius(knobRadius);
}

void setRate(float value){
  tremolo.setFrequency(value);
}

void setDepth(float value){
  tremolo.setAmplitude(value);
}

void draw(){
  background(128);
  stroke(255);
  for(int i = 0; i < out.bufferSize() - 1; i++){
    line(i, 50 + out.left.get(i) * 50, i + 1,  50 + out.left.get(i + 1) * 50);
    line(i, 150 + out.right.get(i) * 50, i + 1,  150 + out.right.get(i + 1) * 50);
  }
}

このプログラムでは、OscilクラスとMultiplierクラスを利用してトレモロを実現しています。 Multiplierは入力音の音量を制御するためのクラスです。 Oscilクラスの変数であるtremoloは、音量を揺らすための波を表しています。 これをMultiplierインスタンスの音量にパッチしてから、Multiplierインスタンスに入力をパッチしています。

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

参考

Processingでサンプラーをつくる

前回はキーボードを鍵盤にしました。今回はMinimとControlP5を使って簡単なサンプラーを作りたいと思います。 サンプラーとは、いろいろな音を録音して、それを音源として利用する楽器です。 音を録音することをサンプリングと呼びます。

ファイルを再生する

サンプラーを作るにあたって、まずはMinimのSamplerクラスの使い方を確認しましょう。 次のプログラムでは、キック、スネア、ハイハットの3つのサウンドファイルをSamplerクラスを使って再生することができます。

f:id:aa_debdeb:20161112153432j: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;
Sampler kick, snare, hihat;

ControlP5 cp5;

void setup(){
  size(500, 300);
  minim = new Minim(this);
  out = minim.getLineOut();
  kick = new Sampler("kick.wav", 4, minim);
  kick.patch(out);
  snare = new Sampler("snare.wav", 4, minim);
  snare.patch(out);
  hihat = new Sampler("hihat.wav", 4, minim);
  hihat.patch(out);
  
  PVector bangSize = new PVector(40, 40);
  float knobRadius = 30;
  cp5 = new ControlP5(this);
  cp5.addBang("playKick")
     .setLabel("kick")
     .setPosition(width / 4.0 * 1.0 - bangSize.x / 2.0, height / 3.0 * 1.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addBang("playSnare")
     .setLabel("snare")
     .setPosition(width / 4.0 * 2.0 - bangSize.x / 2.0, height / 3.0 * 1.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addBang("playHihat")
     .setLabel("hihat")
     .setPosition(width / 4.0 * 3.0 - bangSize.x / 2.0, height / 3.0 * 1.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
}

void draw(){

}

void playKick(){
  kick.trigger();
}

void playSnare(){
  snare.trigger();
}

void playHihat(){
  hihat.trigger();
}

このプログラムを実行して、ボタンを押すと対応する音源を鳴らすことができます。

Minimでサウンドファイルを再生する方法として、第1回目ではAudioPlayerクラスを紹介しました。 SamplerはAudioPlayerと比べて再生時間が短いファイルに適しており、再生後に巻き戻す必要もありません。

Samplerクラスのコンストラクタには第1引数にファイル名、第2引数にボイス数、第3引数にMinim型変数を渡します。 ボイス数とは、同時にサウンドファイルを再生できる数を表しています。 今回は「4」を指定しているので、最大4つ同じ音を重ねることができます。 音を再生するにはtriggerメソッドを利用します。

今回は、BangというGUIパーツを初めて利用しました。Bangは値を持たないGUIパーツで、押すとイベントとして関数を呼び出すことができます。

再生速度を制御する

サンプラーでは、サンプリングした音を加工することもできます。 次のプログラムは、ここまでのプログラムにサウンドファイルの再生速度の変更機能を追加したものです。 再生速度を遅くすると音が低くなり、再生速度を早くすると音が高くなることが確認できると思います。

f:id:aa_debdeb:20161112153445j: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;
Sampler kick, snare, hihat;

ControlP5 cp5;

void setup(){
  size(500, 300);
  minim = new Minim(this);
  out = minim.getLineOut();
  kick = new Sampler("kick.wav", 4, minim);
  kick.patch(out);
  snare = new Sampler("snare.wav", 4, minim);
  snare.patch(out);
  hihat = new Sampler("hihat.wav", 4, minim);
  hihat.patch(out);
  
  PVector bangSize = new PVector(40, 40);
  float knobRadius = 30;
  cp5 = new ControlP5(this);
  cp5.addBang("playKick")
     .setLabel("kick")
     .setPosition(width / 4.0 * 1.0 - bangSize.x / 2.0, height / 3.0 * 1.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addBang("playSnare")
     .setLabel("snare")
     .setPosition(width / 4.0 * 2.0 - bangSize.x / 2.0, height / 3.0 * 1.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addBang("playHihat")
     .setLabel("hihat")
     .setPosition(width / 4.0 * 3.0 - bangSize.x / 2.0, height / 3.0 * 1.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addKnob("changeKickRate")
     .setLabel("kick rate")
     .setRange(-2.0, 2.0)
     .setValue(0.0)
     .setPosition(width / 4.0 * 1.0 - knobRadius, height / 3.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("changeSnareRate")
     .setLabel("snare rate")
     .setRange(-2.0, 2.0)
     .setValue(0.0)
     .setPosition(width / 4.0 * 2.0 - knobRadius, height / 3.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("changeHihatRate")
     .setLabel("hihat rate")
     .setRange(-2.0, 2.0)
     .setValue(0.0)
     .setPosition(width / 4.0 * 3.0 - knobRadius, height / 3.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
}

void draw(){

}

void playKick(){
  kick.trigger();
}

void playSnare(){
  snare.trigger();
}

void playHihat(){
  hihat.trigger();
}

void changeKickRate(float value){
  kick.rate.setLastValue(pow(10.0, value));
}

void changeSnareRate(float value){
  snare.rate.setLastValue(pow(10.0, value));
}

void changeHihatRate(float value){
  hihat.rate.setLastValue(pow(10.0, value));
}

再生速度を指定するために、各音源に対してノブを追加しました。 再生速度はSamplerインスタンスのrateプロパティにより設定されています。 このrateプロパティのsetLastValueメソッドに引数として再生速度を渡すことで、再生速度を変更することができます。 ノブの値は線形的に変化しますが、pow関数を使って再生速度が指数的に変化するようにしました。 ノブの値が-2から2までなので、0.01倍から100倍まで再生速度を変更することができます。 ノブの値が0のときが1倍速です。

マイク入力をサンプリングする

ここまでは、すでに用意してあるサウンドファイルを利用してきました。 次は音を実際にサンプリングしてみましょう。

次のプログラムではマイク入力の音を録音して、それをSamplerクラスで再生します。 Recordボタンを一度押すと録音を開始し、もう一度Recordボタンを押すと録音を終了します。 録音した音源はPlayボタンを押すと再生することができます。

f:id:aa_debdeb:20161112153520p: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;
AudioInput in;
AudioOutput out;
AudioRecorder recorder;
Sampler sampler;

ControlP5 cp5;

void setup(){
  size(500, 200);
  minim = new Minim(this);
  in = minim.getLineIn();
  out = minim.getLineOut();
  
  cp5 = new ControlP5(this);
  PVector bangSize = new PVector(40, 40);
  float knobRadius = 40;
  cp5.addBang("record")
     .setPosition(width / 4.0 * 1.0 - bangSize.x / 2.0, height / 2.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addBang("play")
     .setPosition(width / 4.0 * 2.0 - bangSize.x / 2.0, height / 2.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
}

void record(){
  if(recorder != null && recorder.isRecording()){
    recorder.endRecord();
    recorder.save();
    sampler = new Sampler("sample.wav", 4, minim);
    sampler.patch(out);
  } else {
    recorder = minim.createRecorder(in, "sample.wav");
    recorder.beginRecord();
  }
}

void play(){
  if(sampler != null){
    sampler.trigger();
  }
}

void draw(){
  if(recorder != null && recorder.isRecording()){
    background(255, 128, 128);
  } else {
    background(200);
  }
}

マイク入力音を録音するのに、AudioRecorderクラスを利用しています。 最初にRecordボタンを押すと、createRecorderメソッドでAudioRecorderインスタンスを作成し、beginRecordメソッドで録音を開始します。 もう一度、Reordボタンを押すとisRecording()で録音中だと判定され、endRecord()で録音を終了します。 その後、saveメソッドで録音した音源が「sample.wav」ファイルに保存されます。 この保存したファイルをもとにSamplerインスタンスを作成し、Playボタンが押されたら音が鳴るようにしています。

サンプリングした音の再生速度を制御する

最後にこれまでのプログラムを組み合わせて、録音した音の再生速度を変更できるようにしてみました。

f:id:aa_debdeb:20161112153530p: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;
AudioInput in;
AudioOutput out;
AudioRecorder recorder;
Sampler sampler;

ControlP5 cp5;

void setup(){
  size(500, 200);
  minim = new Minim(this);
  in = minim.getLineIn();
  out = minim.getLineOut();
  
  cp5 = new ControlP5(this);
  PVector bangSize = new PVector(40, 40);
  float knobRadius = 40;
  cp5.addBang("record")
     .setPosition(width / 4.0 * 1.0 - bangSize.x / 2.0, height / 2.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addBang("play")
     .setPosition(width / 4.0 * 2.0 - bangSize.x / 2.0, height / 2.0 - bangSize.y / 2.0)
     .setSize(int(bangSize.x), int(bangSize.y));
  cp5.addKnob("changeRate")
     .setLabel("rate")
     .setRange(-1.0, 1.0)
     .setValue(0.0)
     .setPosition(width / 4.0 * 3.0 - knobRadius, height / 2.0 - knobRadius)
     .setRadius(knobRadius);
}

void record(){
  if(recorder != null && recorder.isRecording()){
    recorder.endRecord();
    recorder.save();
    sampler = new Sampler("sample.wav", 4, minim);
    sampler.patch(out);
  } else {
    recorder = minim.createRecorder(in, "sample.wav");
    recorder.beginRecord();
  }
}

void play(){
  if(sampler != null){
    sampler.trigger();
  }
}

void changeRate(float value){
  if(sampler != null){
    sampler.rate.setLastValue(pow(10.0, value));
  }
}

void draw(){
  if(recorder != null && recorder.isRecording()){
    background(255, 128, 128);
  } else {
    background(200);
  }
}

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

Processingでキーボードを鍵盤にする

第1回目ではMinimを、第2回目ではControlP5について解説しました。 ここから、本格的に楽器づくりを始めていきたいと思います。 まずは手始めとしてPCのキーボードを楽器の鍵盤のように叩くと音がでるようにしたいと思います。

キーボードを鍵盤にする

次のコードはキーを押すと、対応するピッチの音が1秒間鳴るプログラムです。

/**
* sample01
*/

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

Minim minim;
AudioOutput out;

void setup(){
  size(600, 400);
  minim = new Minim(this);
  out = minim.getLineOut();
}

void draw(){

}

void keyPressed(){
  String pitchName = "";
  switch(key){
  case 'z':
    pitchName += "C"; break;
  case 's':
    pitchName += "C#"; break;
  case 'x':
    pitchName += "D"; break;
  case 'd':
    pitchName += "D#"; break;
  case 'c':
    pitchName += "E"; break;
  case 'v':
    pitchName += "F"; break;
  case 'g':
    pitchName += "F#"; break;
  case 'b':
    pitchName += "G"; break;
  case 'h':
    pitchName += "G#"; break;
  case 'n':
    pitchName += "A"; break;
  case 'j':
    pitchName += "A#"; break;
  case 'm':
    pitchName += "B"; break;
  }
  if(pitchName != ""){
    out.playNote(0.0, 1.0, new Keyboard(Frequency.ofPitch(pitchName + "4").asHz()));
  }
}

class Keyboard implements Instrument {
  
  Oscil osc;
  
  Keyboard(float pitch){
    osc = new Oscil(pitch, 1.0, Waves.SAW);
  }
  
  void noteOn(float duration){
    osc.patch(out);  
  }
  
  void noteOff(){
    osc.unpatch(out);
  }
  
}

キーと音の対応関係は以下のようになっています。 文字だけだとわかりずらいですが、キーボードの位置と鍵盤上のドレミの位置が一致しています。

  • z -> ド(C)
  • s -> ド#(C#)
  • x -> レ(D)
  • d -> レ#(D#)
  • c -> ミ(E)
  • v -> ファ(F)
  • f -> ファ#(F#)
  • v -> ソ(G)
  • g -> ソ#(G#)
  • b -> ラ(A)
  • h -> ラ#(A#)
  • n -> シ(B)

プログラムについて解説したいと思います。 キーボードを押したときの処理は、keyPressed()内に記述しています。 switch文で押したキーに合わせてピッチを決め、playNoteメソッドで音を鳴らしています。 playNoteメソッドの第1引数は音が鳴るまでの時間(秒)、第2引数が音の長さ(秒)です。今回は第1引数を「0」、第2引数を「1」にしているのでplayNoteメソッドを読んだ直後に1秒間、音を鳴らしています。 第3引数にはInstrumentインターフェースを継承したKeyboardクラスのインスタンスを渡します。 Keyboardクラスのコンストラクタの引数には周波数を渡します。 ここでは、FrequencyクラスのofPitch()メソッドとasHz()メソッドを使って、ピッチ名から対応した周波数を求めています。 Frequencyクラスを使うとピッチ名以外にMIDIノート番号からも周波数を求めることができます。 詳しくは、公式のリファレンスを確認してください。 Instrumentインターフェースの解説は第1回を確認してください。

このプログラムがキーボードの基本形になります。 ここから、もう少し楽器っぽくなるように改良していきます。

ADSRを設定する

まずはキーボードプログラムに音量の変化をつけたいと思います。 キーボード楽器にかかわらず、すべての楽器には音量の変化があります。 例えばピアノの場合は、キーを押すとすぐに音量が大きくなり、段々と音が減衰していきます。 また、オルガンの場合は、ピアノよりもゆっくりと音量が大きくなっていきます。

電子楽器では、この音量変化を再現するためにADSRを設定します。 ADSRは、Attack、Decay、Sustain、Releaseの頭文字で一般的にそれぞれ次のものを表しています。

  • Attack: キーが押されてから音量が最大音量に達するまでの時間
  • Decay: 音量が最大音量に達してからSustainレベルになるまでの時間
  • Sustain: Attack、Decayが終了したあとにReleaseが始まるまでの時間
  • Release: キーを離してから音量が0になるまでの時間

一般的にADSRを設定するとき、Attack、Decay、Releaseは時間を指定しますが、Sustainは音量を指定します。

「ADSR」でGoogleの画像検索をすれば、わかりやすい模式図を確認できます。

ADSR - Google 検索

次のプログラムは先程のキーボードプログラムにGUIでADSRを設定できるようにしたものです。

f:id:aa_debdeb:20161112084723j: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;
ControlP5 cp5;
float attack, decay, sustain, release;
float knobRadius = 40;

void setup(){
  size(600, 400);
  minim = new Minim(this);
  out = minim.getLineOut();
  cp5 = new ControlP5(this);
  cp5.addKnob("attack")
     .setRange(0.01, 0.5)
     .setValue(0.25)
     .setPosition(width / 5.0 * 1.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("decay")
     .setRange(0.01, 0.5)
     .setValue(0.25)
     .setPosition(width / 5.0 * 2.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("sustain")
     .setRange(0.0, 1.0)
     .setValue(0.5)
     .setPosition(width / 5.0 * 3.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("release")
     .setRange(0.01, 0.5)
     .setValue(0.25)
     .setPosition(width / 5.0 * 4.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
}

void draw(){

}

void keyPressed(){
  String pitchName = "";
  switch(key){
  case 'z':
    pitchName += "C"; break;
  case 's':
    pitchName += "C#"; break;
  case 'x':
    pitchName += "D"; break;
  case 'd':
    pitchName += "D#"; break;
  case 'c':
    pitchName += "E"; break;
  case 'v':
    pitchName += "F"; break;
  case 'g':
    pitchName += "F#"; break;
  case 'b':
    pitchName += "G"; break;
  case 'h':
    pitchName += "G#"; break;
  case 'n':
    pitchName += "A"; break;
  case 'j':
    pitchName += "A#"; break;
  case 'm':
    pitchName += "B"; break;
  }
  if(pitchName != ""){
    out.playNote(0.0, 1.0, new Keyboard(Frequency.ofPitch(pitchName + "4").asHz()));
  }
}

class Keyboard implements Instrument {
  
  Oscil osc;
  ADSR adsr;
  
  Keyboard(float pitch){
    osc = new Oscil(pitch, 1.0, Waves.SAW);
    adsr = new ADSR(1.0, attack, decay, sustain, release);
    osc.patch(adsr);
  }
  
  void noteOn(float duration){
    adsr.noteOn();
    adsr.patch(out);  
  }
  
  void noteOff(){
    adsr.unpatchAfterRelease(out);
    adsr.noteOff();
  }
  
}

setup関数内では、ControlP5でADSRの値をコントロールするためのノブの作成をしています。 ADSRを設定するため、ADSRクラスをKeyboardクラスの中で利用しています。 ADSRクラスのコンストラクタの引数はそれぞれ次のものを表しています。

  • 第1引数 (1.0): 最大音量
  • 第2引数 (attack): attackの時間(秒)
  • 第3引数 (decay): dacayの時間(秒)
  • 第4引数 (sustain): 最大音量に対するsustatin時の音量の割合
  • 第5引数 (release): releaseの時間(秒)

このように作成したADSR型変数adsrを、oscとoutの間に挟んでパッチすることで音量をADSRに沿って変化させることができます。

Keyboardメソッド内では、unpatchAfterRelease()というメソッドを利用しています。 これを利用することにより、releaseが終了してからパッチを外すことを指示することができます。

ちなみに、playNote()メソッドを呼び出すときに音の継続時間を1秒間にしています。 この1秒の中でattack、decay、sustainが行われ、1秒後にreleaseが始まります。

GUIと音程と音量を制御する

最後に、音程と音量もGUIのノブで変えられるようにしてみましょう。

f:id:aa_debdeb:20161112084738j: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;
ControlP5 cp5;
float attack, decay, sustain, release;
int octave;
float amplitude;
float knobRadius = 40;

void setup(){
  size(600, 400);
  minim = new Minim(this);
  out = minim.getLineOut();
  cp5 = new ControlP5(this);
  cp5.addKnob("attack")
     .setRange(0.01, 0.5)
     .setValue(0.25)
     .setPosition(width / 5.0 * 1.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("decay")
     .setRange(0.01, 0.5)
     .setValue(0.25)
     .setPosition(width / 5.0 * 2.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("sustain")
     .setRange(0.0, 1.0)
     .setValue(0.5)
     .setPosition(width / 5.0 * 3.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("release")
     .setRange(0.01, 0.5)
     .setValue(0.25)
     .setPosition(width / 5.0 * 4.0 - knobRadius, height / 3.0 * 1.0 - knobRadius)
     .setRadius(knobRadius);
  cp5.addKnob("octave")
     .setRange(2, 6)
     .setValue(4)
     .setPosition(width / 3.0 * 1.0 - knobRadius, height / 3.0 * 2.0 - knobRadius)
     .setRadius(knobRadius)
     .setNumberOfTickMarks(4)
     .setTickMarkLength(5)
     .snapToTickMarks(true);
  cp5.addKnob("amplitude")
     .setRange(0, 1.0)
     .setValue(1.0)
     .setPosition(width / 3.0 * 2.0 - knobRadius, height / 3.0 * 2.0 - knobRadius)
     .setRadius(knobRadius);
}

void draw(){

}

void keyPressed(){
  String pitchName = "";
  switch(key){
  case 'z':
    pitchName += "C"; break;
  case 's':
    pitchName += "C#"; break;
  case 'x':
    pitchName += "D"; break;
  case 'd':
    pitchName += "D#"; break;
  case 'c':
    pitchName += "E"; break;
  case 'v':
    pitchName += "F"; break;
  case 'g':
    pitchName += "F#"; break;
  case 'b':
    pitchName += "G"; break;
  case 'h':
    pitchName += "G#"; break;
  case 'n':
    pitchName += "A"; break;
  case 'j':
    pitchName += "A#"; break;
  case 'm':
    pitchName += "B"; break;
  }
  if(pitchName != ""){
    out.playNote(0.0, 1.0, new Keyboard(Frequency.ofPitch(pitchName + octave).asHz()));
  }
}

class Keyboard implements Instrument {
  
  Oscil osc;
  ADSR adsr;
  
  Keyboard(float pitch){
    osc = new Oscil(pitch, 1.0, Waves.SAW);
    adsr = new ADSR(amplitude, attack, decay, sustain, release);
    osc.patch(adsr);
  }
  
  void noteOn(float duration){
    adsr.noteOn();
    adsr.patch(out);  
  }
  
  void noteOff(){
    adsr.unpatchAfterRelease(out);
    adsr.noteOff();
  }
  
}

特に難しいことはしていないので解説はしませんが、これでより広い範囲の音がでるようになったと思います。

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

ControlP5の使い方

今回はProcessingでGUIを扱うためのライブラリであるControlP5の使い方を解説します。

GitHub - sojamo/controlp5: A gui library for processing.org

ControlP5のインストール

ControlP5はProcessingにデフォルトではインストールされていないので、はじめにインストールを行います。 以下のところからLibrary Managerを開き、「ControlP5」で検索してインストールしてください。

「Sketch」->「Import Library...」->「Add Library...」

インストールが完了したら、「Import Library...」にControlP5が表示されるようになります。

ノブを使う

f:id:aa_debdeb:20161104221404p:plain

まずはControlP5の基本的な使い方を説明します。 次のコードは上図のように3つのノブをスケッチ上に配置しています。 これをマウスで操作すると、それぞれ対応した四角形の大きさが変化します。

/*
* sample01
*/

import controlP5.*;

ControlP5 cp5;
float knobValue;
Knob knob;
float knobFuncValue;

void setup(){
  size(600, 350);
  rectMode(CENTER);
  cp5 = new ControlP5(this);
  
  cp5.addKnob("knobValue") // left knob
     .setLabel("left")
     .setRange(0, 100)
     .setValue(50)
     .setPosition(75, 50)
     .setRadius(50);
  
  knob = cp5.addKnob("center") // center knob
            .setRange(0, 100)
            .setValue(50)
            .setPosition(250, 50)
            .setRadius(50)
            .setDragDirection(Knob.VERTICAL);
            
  cp5.addKnob("knobFunc") // light knob
     .setLabel("right")
     .setRange(0, 100)
     .setValue(50)
     .setPosition(425, 50)
     .setRadius(50)
     .setNumberOfTickMarks(10)
     .setTickMarkLength(5)
     .snapToTickMarks(true);           
}

void knobFunc(float value){
  knobFuncValue = value;
}

void draw(){
  background(128);
  stroke(255);
  noFill();
  rect(125, 250, knobValue, knobValue); // left box
  rect(300, 250, knob.getValue(), knob.getValue()); // center box
  rect(475, 250, knobFuncValue, knobFuncValue); //right box
}

このコードについて解説していきたいと思います。

import controlP5.*;
...
ControlP5 cp5;
...
cp5 = new ControlP5(this);

まずControlP5のライブラリをインポートし、ControlP5クラスのインスタンスを作成します。

  cp5.addKnob("knobValue") // left knob
     .setLabel("left")
     .setRange(0, 100)
     .setValue(50)
     .setPosition(75, 50)
     .setRadius(50);

ノブを作成するにはaddKnobメソッドを使います。 このノブの値は、addKnobメソッドの引数と同じ名前の変数に格納されます(この場合、変数knobValueに値が格納されます)。 パラメータはメソッドチェーンの形式で設定します。 以下、各メソッドで設定できるパラメータです。

  • setLabel: ラベル名(デフォルトは、addKnobメソッドの引数の値)
  • setRange: 値の範囲
  • setValue: 初期値
  • setPosition: GUIの右上の位置
  • setRadius: ノブの半径
  knob = cp5.addKnob("center") // center knob
            .setRange(0, 100)
            .setValue(50)
            .setPosition(250, 50)
            .setRadius(50)
            .setDragDirection(Knob.VERTICAL);

真ん中のノブはKnob型変数に作成したノブを代入し、draw関数内でその値をgetValueメソッドを使って取得しています。

ここで新たに出てきたメソッドは、次のパラメータを設定しています。

  • setDragDirection: マウスを動かす方向(デフォルトはKnob.HORIZONTAL)
  cp5.addKnob("knobFunc") // light knob
     .setLabel("right")
     .setRange(0, 100)
     .setValue(50)
     .setPosition(425, 50)
     .setRadius(50)
     .setNumberOfTickMarks(10)
     .setTickMarkLength(5)
     .snapToTickMarks(true);           

3つ目のノブは操作するとaddKnobメソッドの引数と同名の関数knobFunc()を呼び出します。

ここで新たに出てきたメソッドは、次のパラメータを設定しています。

  • setNumberOfTickMarks: 目盛りの数
  • setTickMarkLength: 目盛りの長さ
  • snapToTickMarks: 目盛りの値でステップさせるか(デフォルトはfalse)

以上がノブの使い方の説明です。 他のGUIパーツも同様に「add○○」でGUIパーツを作成し、メソッドチェーンでパラメータを設定します。 GUIからの値の取得も同じように行います。

以下では、楽器作成で使うトグルスイッチとスライダーについて説明します。

トグルスイッチを使う

f:id:aa_debdeb:20161104224856p:plain

トグルスイッチは、Boolean型の値を操作するためのGUIです。 以下のコードでは、トグルスイッチで四角形の中の色を変えることができます。

/*
* sample02
*/

import controlP5.*;

ControlP5 cp5;

boolean toggleValue;
Toggle toggle;

void setup(){
  size(400, 400);
  rectMode(CENTER);
  cp5 = new ControlP5(this);
  
  cp5.addToggle("toggleValue")
     .setLabel("left")
     .setPosition(100, 100)
     .setValue(false)
     .setSize(40, 20);
  
  toggle = cp5.addToggle("right")
              .setPosition(260, 100)
              .setValue(false)
              .setSize(40, 20)
              .setMode(ControlP5.SWITCH);
}

void draw(){
  background(128);
  stroke(255);
  
  // left box
  if(toggleValue){
    fill(255);
  } else {
    noFill();
  }
  rect(120, 250, 100, 100);
  
  // right box
  if(toggle.getBooleanValue()){
    fill(255);
  } else {
    noFill();
  }
  rect(280, 250, 100, 100);
}

基本的にはノブのときと同じですが、setModeでノブの種類を変えられるのと、Toggle型変数から値を取得するのにgetBooleanValue()を使っている点に注意してください。

スライダーを使う

f:id:aa_debdeb:20161104225222p:plain

スライダーもノブと同様に連続した数値を設定するために使います。 次のコードではスライダーを操作すると対応した四角形の大きさを変えることができます。

/*
* sample03
*/

import controlP5.*;

ControlP5 cp5;
float sliderValue;
Slider slider;

void setup(){
  size(400, 400);
  rectMode(CENTER);
  cp5 = new ControlP5(this);
  
  cp5.addSlider("sliderValue")
     .setLabel("left")
     .setRange(0, 100)
     .setValue(50)
     .setPosition(50, 90)
     .setSize(100, 20);
  
  slider = cp5.addSlider("right")
              .setRange(0, 100)
              .setValue(50)
              .setPosition(290, 50)
              .setSize(20, 100)
              .setNumberOfTickMarks(11);
}

void draw(){
  background(128);
  stroke(255);
  noFill();
  rect(100, 250, sliderValue, sliderValue); // left box
  rect(300, 250, slider.getValue(), slider.getValue()); // right box
}

スライダーは、横幅が縦幅より大きいときは横向き、縦幅が横幅より大きいときは縦向きに操作できるように勝手になります。

もしこれら以外のGUIパーツを使うことがあれば、その都度解説します。

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