【Arduino】Mozzi入門3 エンベロープ(ADSR)とEventDelay

7/01/2023

Arduino

t f B! P L

今回はエンベロープ(ADSR)とEventDelayを使って、音の長さや大きさを変えてみます。

前 => 和音の出力と周波数の計算
次 => 音を連続的に変化させる

EventDelay

今更ですが、Mozziライブラリでは通常のdelay()は使えません。代わりにEventDelayを使います。

今までは、同じ音を永遠に鳴らすことしかできませんでしたが、EventDelayを使うことで「待つ」ことができるようになります。

まずはコード全体

以下は、EventDelayを使って一秒ごとに音のオンオフを切り替えるプログラムです。

このプログラムでは、EventDelayのready()関数を使って、一秒ごとにisOnの値を反転させています。

#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/sin2048_int8.h>
#include <EventDelay.h>

#define CONTROL_RATE 64

Oscil<SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);

EventDelay toggle;

int isOn = 0;

void setup() {
    startMozzi(CONTROL_RATE);
    aSin.setFreq(440);
    toggle.set(1000);
    toggle.start();
}

void updateControl() {
    if (toggle.ready()) {
        isOn = !isOn;
        toggle.start();
    }
}

int updateAudio() {
    return aSin.next() * isOn;
}

void loop() {
    audioHook();
}

コードの解説

詳しく見ていきます。まず、EventDelayを使うために、EventDelay.hをインクルードし、インスタンスを生成します。

この時に、音が出ているかどうかを判断するための変数isOnを作成しています。

#include <EventDelay.h>
EventDelay toggle;

int isOn = 0;

次に、setup()関数でEventDelayのset()関数を使って、待ち時間をmsで設定します。

toggle.set(1000);

そして、start()関数を呼びだします。するとタイマーがスタートします。

toggle.start();

設定した待ち時間が経過するとready()関数がtrueを返すようになります。

このプログラムでは、ready()関数がtrueを返したら、isOnの値を反転させて、再びstart()関数を呼び出しています。

if (toggle.ready()) {
    isOn = !isOn;
    toggle.start();
}

これで、一秒ごとにisOnの値が反転するようになるので、updateAudio()関数でisOnの値を使って音をオンオフさせることができます。

int updateAudio() {
    return aSin.next() * isOn;
}

ちなみに、EventDelayのインスタンスを生成する際に以下のように引数を指定することで、set()関数をあらかじめ呼び出すことができます。

EventDelay toggle(1000);

Metronome

MetronomeはEventDelayと似ていますが、set()で設定した時間が経過すると、自動的に再度呼び出されるようになっています。

下記は、Metronomeを使ってEventDelayでの例と同じように一秒ごとに音のオンオフを切り替えるプログラムです。

#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/sin2048_int8.h>
#include <Metronome.h>

#define CONTROL_RATE 64

Oscil<SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);

Metronome toggle(1000);

int isOn = 0;

void setup() {
    startMozzi(CONTROL_RATE);
    aSin.setFreq(440);
}

void updateControl() {
    if (toggle.ready()) {
        isOn = !isOn;
    }
}

int updateAudio() {
    return aSin.next() * isOn;
}

void loop() {
    audioHook();
}

エンベロープ(ADSR)

そもそもエンベロープとは、オシレーターが出力するサウンドに対して、音の立ち上がりや立ち下がりの速さ、音の長さ、音の大きさなどを調整するためのものです。

エンベロープのパラメータとしては、主に以下の4つがあります。

  • Attack(立ち上がり)
  • Decay(減衰)
  • Sustain(減衰後の保持)
  • Release(余韻)

AttackとDecayの間にHoldというパラメータがある場合もありますが、MozziではHoldはありませんが、現実的に困ることはないと思います。

それでは、Mozziでエンベロープを使用してみましょう。

コード全体

#include <MozziGuts.h>
#include <mozzi_midi.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h>
#include <tables/sin2048_int8.h>

#define CONTROL_RATE 64

Oscil<SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);
ADSR<AUDIO_RATE, AUDIO_RATE> envelope;
EventDelay noteDelay;

void setup() {
    startMozzi(CONTROL_RATE);

    envelope.setLevels(255, 200, 180, 0);
    envelope.setTimes(500, 500, 500, 500);
  
    aSin.setFreq(440);
  
    noteDelay.set(2500);
    noteDelay.start();
}

void updateControl() {
    if (noteDelay.ready()) {
        envelope.noteOn();
        noteDelay.start();
    }
}

int updateAudio() {
    envelope.update();
    return (int)(envelope.next() * aSin.next()) >> 8;
}

void loop() {
    audioHook();
}

コードの解説

まず、エンベロープを使うために、ADSR.hをインクルードし、インスタンスを生成します。

#include <ADSR.h>
ADSR<AUDIO_RATE, AUDIO_RATE> envelope;

そして、エンベロープのパラメータを設定します。

setLevels()関数で、エンベロープの各段階の最終的な音量を設定します。ここでは、Attackの音量を255、Decayの音量を200、Sustainの音量を180、Releaseの音量を0としています。

envelope.setLevels(255, 200, 180, 0);

setTimes()関数で、エンベロープの各段階の時間を設定します。ここでは、Attackの時間を500ms、Decayの時間を500ms、Sustainの時間を500ms、Releaseの時間を500msとしています。

envelope.setTimes(500, 500, 500, 500);

updateControl()関数で、エンベロープのnoteOn()関数を呼び出しています。noteOn()関数を呼び出すと、ADSRの各段階が始まります。

今回はEventDelayを使って、一定時間ごとにnoteOn()関数を呼び出しています。

void updateControl() {
    if (noteDelay.ready()) {
        envelope.noteOn();
        noteDelay.start();
    }
}

updateAudio()関数内でエンベロープのupdate()関数を呼び出して、エンベロープを更新します。

また、エンベロープのnext()関数を呼び出して、エンベロープの現在の値を取得しています。その際、next()関数の戻り値の最大値は255なので、音の大きさを調整するために、8ビット右シフト(256で割る)しています。

int updateAudio() {
    envelope.update();
    return (int)(envelope.next() * aSin.next()) >> 8;
}

プログラム例

今までのことを踏まえて、プログラム例としてカノンの最初の部分を作ってみました。

せっかく作ったのでcalcFreqを使いましたが、処理が重くなってしまうので、この程度であればcalcFreqを使わずに、直接周波数を設定したほうがいいかもしれません。

波形は最初に作成した"piano2048_int8.h"を使っています。sin2048_int8.hを使ってもいいですが、正弦波では少し物足りないので、ピアノの音を使ってみました。

#include <MozziGuts.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h>
#include "piano2048_int8.h"

#define CONTROL_RATE 64

Oscil<PIANO2048_NUM_CELLS, AUDIO_RATE> aBase(PIANO2048_DATA);
Oscil<PIANO2048_NUM_CELLS, AUDIO_RATE> bBase(PIANO2048_DATA);
Oscil<PIANO2048_NUM_CELLS, AUDIO_RATE> cBase(PIANO2048_DATA);
Oscil<PIANO2048_NUM_CELLS, AUDIO_RATE> aMelody(PIANO2048_DATA);

ADSR<AUDIO_RATE, AUDIO_RATE> envelope;

EventDelay noteDelay;

int c = -1;

struct ToneMap {
    String note;
    int toneIndex;
};
    
float calcFreq(String note, int octave) {
    ToneMap toneMap[] = {
        { "A", 0 },
        { "B", 2 },
        { "C", -9 },
        { "D", -7 },
        { "E", -5 },
        { "F", -4 },
        { "G", -2 }
    };
    
    int toneIndex = 0;
    
    for (int i = 0; i < sizeof(toneMap) / sizeof(toneMap[0]); i++) {
        if (note.substring(0, 1) == toneMap[i].note) {
        toneIndex = toneMap[i].toneIndex;
        break;
        }
    }
    
    if (note.substring(1, 2) == "#") {
        toneIndex++;
    } else if (note.substring(1, 2) == "b") {
        toneIndex--;
    }
    
    return (float) 440.0 * pow(2.0, (octave - 4) + (toneIndex / 12.0));
}


void setup() {
    startMozzi(CONTROL_RATE);
    noteDelay.set(1500);
    noteDelay.start();

    envelope.setADLevels(255, 210);
    envelope.setTimes(10, 1200, 10, 500);
}

int baseA = 0;
int baseB = 0;
int baseC = 0;
int melodyA = 0;

void updateControl() {
    if (noteDelay.ready()) {
    c++;
    noteDelay.start();
    envelope.noteOn();
    }

    switch (c % 8) {
    case 0:
        baseA = calcFreq("C", 5);
        baseB = calcFreq("E", 5);
        baseC = calcFreq("G", 5);
        break;
    case 1:
        baseA = calcFreq("G", 4);
        baseB = calcFreq("B", 4);
        baseC = calcFreq("D", 5);
        break;
    case 2:
        baseA = calcFreq("A", 4);
        baseB = calcFreq("C", 5);
        baseC = calcFreq("E", 5);
        break;
    case 3:
        baseA = calcFreq("E", 4);
        baseB = calcFreq("G", 4);
        baseC = calcFreq("B", 4);
        break;
    case 4:
        baseA = calcFreq("F", 4);
        baseB = calcFreq("A", 4);
        baseC = calcFreq("C", 5);
        break;
    case 5:
        baseA = calcFreq("C", 4);
        baseB = calcFreq("E", 4);
        baseC = calcFreq("G", 4);
        break;
    case 6:
        baseA = calcFreq("F", 4);
        baseB = calcFreq("A", 4);
        baseC = calcFreq("C", 5);
        break;
    case 7:
        baseA = calcFreq("G", 4);
        baseB = calcFreq("B", 4);
        baseC = calcFreq("D", 5);
        break;
    }

    switch (c / 8) {
    case 0:
        melodyA = 0;
        break;
    default:
        switch (c % 8) {
        case 0:
            melodyA = calcFreq("E", 6);
            break;
        case 1:
            melodyA = calcFreq("D", 6);
            break;
        case 2:
            melodyA = calcFreq("C", 6);
            break;
        case 3:
            melodyA = calcFreq("B", 5);
            break;
        case 4:
            melodyA = calcFreq("A", 5);
            break;
        case 5:
            melodyA = calcFreq("G", 5);
            break;
        case 6:
            melodyA = calcFreq("A", 5);
            break;
        case 7:
            melodyA = calcFreq("B", 5);
            break;
        }
    }

    aBase.setFreq(baseA);
    bBase.setFreq(baseB);
    cBase.setFreq(baseC);
    aMelody.setFreq(melodyA);
}

int updateAudio() {
    envelope.update();
    return (int)(envelope.next() * ((aBase.next() + bBase.next() + cBase.next() + aMelody.next()) >> 2)) >> 8;
}

void loop() {
    audioHook();
}

まとめ

今回はエンベロープ(ADSR)とEventDelayを使って、音の長さや大きさを変えてみました。

次回は、バイオリンのように音を連続的に変化させてみます。

前 => 和音の出力と周波数の計算
次 => 音を連続的に変化させる