第十六項 エンベロープ

第十六項 エンベロープ

電子工作創作表現(2019/10/24)

スライドPDF

エンベロープを扱う

  • 前回はオシレータと、簡単な波形の合成
     
  • エンベロープを使ってもう少し楽器に近づける
先週に引きつづき、Mozziの解説をやっていきます。
今回もう少し楽器として実用可能な落とし込みを見せたいので、エンベロープについてやっていきます。

エンベロープの基本形・ADSR

  • 音の始まり方と終わり方を定義する
     
  • 音量のコントロールでニュアンスを変化させる
シンセサイザーを使っている人は良くご存じとは思いますが、エンベロープとは音が鳴り始めてからの音量の推移のことを表します。
例えばピアノは音が鳴ってから鍵盤を離すとすぐ音が途切れますが、ペダルを踏むと鍵盤を話しても音が持続したままになります。このような時間による音の変化を定量的に表すための「ADSR」というモデルがあります。

ADSRは何の略?

  • アタック(Attack)
     
  • ディケイ(Decay)
     
  • サステイン(Sustain)
     
  • リリース(Release)
ADSRはそれぞれの略で、上のような意味があります。

変化のグラフ

ADSR

グラフに表すとこのようになります。(出展:https://www.perfectcircuit.com/signal/learning-synthesis-envelopes-1)
アタックで最初に勢いよく音量が上がった後にディケイで少し音量が下がります。その後Sustainで延ばせる時間まで伸びた後リリースの時間かけて徐々に音が小さくなります。
情報としては、ADSR四項目それぞれの最大の長さと、Attack後、Decay後のボリュームが定義されています。

ADSRクラス

  • オシレータにボリュームの変化を与える
     
  • noteOnを呼べばその通りに鳴らしてくれる
概念としてはそんなに難しくないですが、これをプログラムで実装しようとするとやや煩雑です。MozziではADSRクラスが用意されているので、それを使うことで前回やったオシレーターに対して簡単にエンベロープをかけることができるようになっています。まずはこちらのスケッチを参考にしてみましょう。

オシレーターを用意するところまでは前回同様です。setup内のsetADLevelsでアタックとディケイの音量を定義して、setTimesでADSRそれぞれの時間を入れます。スイッチを11ピン~13ピンに繋ぎ、押された時にnoteOnを呼び出すことで簡単な鍵盤を実現することができます。
#include <MozziGuts.h>
#include <mozzi_midi.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h>
#include <tables/saw8192_int8.h>

Oscil <8192, AUDIO_RATE> aOscil(SAW8192_DATA);//元となる音色
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数

void setup() {
  startMozzi(64);
  aOscil.setFreq(440);
  Atk = 10;
  Dec = 10;
  Sus = 100;
  Rel = 500;
  envelope.setADLevels(255, 128);
  envelope.setTimes(Atk, Dec, Sus, Rel);

  pinMode(11, INPUT_PULLUP);
  pinMode(12, INPUT_PULLUP);
  pinMode(13, INPUT_PULLUP);
}

void updateControl()
{
  //ボタンが押されたらエンベロープのnoteOnを呼ぶ
  if (digitalRead(11) == LOW)
  {
    aOscil.setFreq(mtof(60));
    envelope.noteOn();
  }
  if (digitalRead(12) == LOW)
  {
    aOscil.setFreq(mtof(62));
    envelope.noteOn();
  }
  if (digitalRead(13) == LOW)
  {
    aOscil.setFreq(mtof(64));
    envelope.noteOn();
  }
}

int updateAudio()
{
  envelope.update();
  return (int)(envelope.next() * aOscil.next())>>8;
  //エンベロープのボリュームは0~256なので、かけてからビットシフトをする
}

void loop() {
  audioHook();
}

mtof関数

  • MIDIノートを周波数に変更してくれる(Midi to Freqの略)
     
  • 64が国際式表記のA4で440Hz
     
  • 88鍵の音域は21~108だが、レンジ自体は0~127まで存在する
押されるボタンでオシレータの周波数を変えていますが、ここではmtofというmozziの関数を使っています。
MIDIのノート番号を周波数に変換する関数なので、音階を使いたい時にはこの関数を使うのが便利です。

mtofで音階を制御

  • ノートで指定できるので、スケールなども作りやすい
     
  • ランダムにペンタトニックを鳴らすスケッチ
配列変数としてノート番号を書いておけば、メロディやスケールを自由に定義して鳴らすことも可能です。
以下のコードは、あらかじめ変数pentaにペンタトニックのスケールを保存しておき、間を開けてランダムにnoteOnを呼び出すスケッチです。
#include <MozziGuts.h>
#include <mozzi_midi.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h>
#include <tables/saw8192_int8.h>

Oscil <8192, AUDIO_RATE> aOscil(SAW8192_DATA);//元となる音色
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数

EventDelay timer;
unsigned int penta[] = {0, 3, 5, 7, 10};

void setup() {
  startMozzi(64);
  aOscil.setFreq(440);
  envelope.setADLevels(255, 128);
  Atk = 10;
  Dec = 10;
  Sus = 100;
  Rel = 500;
  envelope.setTimes(Atk, Dec, Sus, Rel);

  pinMode(11, INPUT_PULLUP);
  pinMode(12, INPUT_PULLUP);
  pinMode(13, INPUT_PULLUP);

  timer.set(300);
  timer.start();
}

void updateControl()
{
  if (timer.ready())
  {
    //note変数に60(C4)から始まるペンタスケールをランダムに定義する
    int note = 60 + penta[int(random(100) % 5)];

    //オシレータの周波数を変更
    aOscil.setFreq(mtof(note));
    envelope.noteOn();

    //次に音を出す時間を決める(noteの音程で4分か8分の2パターン)
    if (note % 2 == 0) timer.set(300);
    else timer.set(150);
    timer.start();
  }
}

int updateAudio()
{
  envelope.update();
  return (int)(envelope.next() * aOscil.next())>>8;
  //エンベロープのボリュームは0~256なので、かけてからビットシフトをする
}

void loop() {
  audioHook();
}

EventDelay

  • Mozziのために用意されたディレイ関数
     
  • delay関数を使うと音が停まってしまう
ここで新しくEventDelayというクラスが出てきました。これもMozziを使う上でかなり便利なクラスです。ArduinoデフォルトのDelay関数では全ての処理が停まるため、オーディオの出力もストップします。それを回避しながら一定時間経過後に処理を走らせることができます。

EventDelayを使った繰り返し処理

  • setTimeを使って時間を設定し、startで計測開始
     
  • if (timer.ready())で経過したかどうか判定
setTimeで時間を設定し、startを呼び出すとタイマーが動き始めます。経過したかどうかはreadyメソッドで確認できるので、readyをif文に入れて、時間経過で呼び出したい処理を書くようにします。
readyになったらまたstartを呼び出すようにすれば繰り返し処理が行われますし、都度setTimeを呼ぶことでタイミングを変化させることもできます。

複数のEventDelay

  • アルゴリズミックな演奏をプログラムで表現することができる
     
  • ライヒの「Piano Phase」を再現してみる
これによってアルゴリズミックなメロディをArduino内にプログラムすることができるようになります。
複数のEventDelayを平行して走らせることもできるので、独立した演奏プログラムを走らせたりすることもできます。下のスケッチでは、スティーヴ・ライヒの「Piano Phase」の序盤を再現してみました。譜面のパターンを配列変数に格納して、EventDelayが発火したタイミングで順番に鳴らしていっています。1ミリ秒ごとにずれていく様子を聞くことができます。本家では途中から違う譜面になり終わり15分ほどで終了しますが、プログラムなので電源を供給する限り永遠に演奏が続きます。
#include <MozziGuts.h>
#include <mozzi_midi.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h>
#include <tables/sin8192_int8.h>

Oscil <8192, AUDIO_RATE> aOscil(SIN8192_DATA);//元となる音色
Oscil <8192, AUDIO_RATE> aOscil2(SIN8192_DATA);//元となる音色
ADSR <AUDIO_RATE, AUDIO_RATE> envelope_A;//エンベロープをかけるためのクラス
ADSR <AUDIO_RATE, AUDIO_RATE> envelope_B;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数

//piano phaseのパターン
unsigned int pattern[] = {64, 66, 71, 73, 74, 66, 64, 73, 71, 66, 74, 73};

//二つのタイマーを用意
int phase_A = 0;
int phase_B = 0;
EventDelay timer_B;
EventDelay timer_A;

void setup() {
  startMozzi(1024);
  Atk = 10;
  Dec = 10;
  Sus = 50;
  Rel = 100;
  envelope_A.setTimes(Atk, Dec, Sus, Rel);
  envelope_A.setADLevels(255, 128);
  envelope_B.setTimes(Atk, Dec, Sus, Rel);
  envelope_B.setADLevels(255, 128);

  timer_A.set(150);
  timer_B.set(151);
  timer_B.start();
  timer_A.start();
}

void updateControl()
{  
  if (timer_A.ready())
  {
    int note = pattern[phase_A];
    phase_A = (phase_A + 1) % 12;
    aOscil.setFreq(mtof(note));
    envelope_A.noteOn();
    timer_A.start();
  }

  if (timer_B.ready())
  {
    int note = pattern[phase_B];
    phase_B = (phase_B + 1) % 12;
    aOscil2.setFreq(mtof(note));
    envelope_B.noteOn();
    timer_B.start();
  }
}

int updateAudio()
{
  envelope_A.update();
  envelope_B.update();
  int A = (envelope_A.next() * aOscil.next()) >> 8;
  int B = (envelope_B.next() * aOscil2.next()) >> 8;

  return (A + B) / 2;
}

void loop() {
  audioHook();
}