aa develop

開発と成長

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