DDS (direct-digital synthesis) / エンベロープジェネレーター

pp.282-287
その他: 音楽・動画・ゲームに活用! ソフトシンセ 音作り大全, p.22

いわゆるADSR方式でエンベロープカーブが設定できるようにしてみる。
f:id:ti-nspire:20191106085153p:plain:h250

lookupPitch()で返しているC1C2はscale.hに定義してある。

#include <avr/io.h>
#include "scale.h"
extern "C"{
    #include "USART.h"
}

#define FULL_VOL       31    // 最大音量。5ビット値(0~31)。
#define ATTACK_RATE    50    // 小さいほど鋭く最大音量まで立ち上がる。
#define DECAY_RATE     300   // 小さいほど鋭くsustainレベルまで減衰する。
#define SUSTAIN_LEVEL  10
#define SUSTAIN_TIME   10000
#define RELEASE_RATE   500   //小さいほど鋭く音量ゼロまで減衰する。

#define ATTACK_TIME  (ATTACK_RATE * FULL_VOL)
#define DECAY_TIME   (ATTACK_TIME + (FULL_VOL - SUSTAIN_LEVEL) * DECAY_RATE)
#define RELEASE_TIME (DECAY_TIME + SUSTAIN_TIME)

void createSaw(int8_t* tbl){
    for(int i=0; i<256; i++){
        tbl[i] = (int8_t)i;
    }
}

void initTimer0(){
    TCCR0A |= (1 << WGM00) | (1 << WGM01); // Fast PWMモード
    TCCR0A |= (1 << COM0A1);               // コンペアマッチ時にOC0A (PD6)をクリア、ボトム時にOC0A (PD6)をセット。
    TCCR0B |= (1 << CS00);                 // 1分周。8 MHzをそのままカウントする。
}

uint16_t lookupPitch(char ch){
    switch(ch){
        case 'a': return(G1);
        case 's': return(A1);
        case 'd': return(B1);
        case 'f': return(C2);
        case 'g': return(D2);
        case 'h': return(E2);
        case 'j': return(F2);
        case 'k': return(G2);
        case 'l': return(A2);
        case ';': return(B2);
        case ':': return(C3);
        default : return(C1);
    }
}

int main(void) {

    int8_t Saw[256]; createSaw(Saw); // のこぎり波を1周期分作る。

    uint16_t accumulator = 0;
    uint8_t  vol         = 0;
    uint16_t noteClock   = 0;
    uint16_t tuningWord  = C1;

    uint8_t waveStep;
    int16_t mixer = 0;
    char    serialInput;

    char isInAttack, isInDecay, isInRelease, AdsrComplete;

    initTimer0();
    initUSART();

    DDRD |= (1 << PD6); // PD6 (OC0A)にスピーカーをつないでそこからDDS (pwm)を出力することにする。

    while(1){

        accumulator += tuningWord;
        waveStep     = accumulator >> 8;
        mixer        = Saw[waveStep] * vol;
        mixer      >>= 5;

        if(bit_is_set(UCSR0A, RXC0)){
            serialInput = UDR0;
            printWord(serialInput);
            //transmitByte(serialInput);
            tuningWord  = lookupPitch(serialInput);
            noteClock   = 1;
        }

        if(noteClock){
            (noteClock < ATTACK_TIME) && (noteClock > ATTACK_RATE * vol) && (vol < FULL_VOL)?
                isInAttack = 1:
                isInAttack = 0;
            (noteClock < DECAY_TIME) && ((noteClock - ATTACK_TIME) > (FULL_VOL - vol) * DECAY_RATE) && (vol > SUSTAIN_LEVEL)?
                isInDecay = 1:
                isInDecay = 0;
            (noteClock > RELEASE_TIME) && ((noteClock - RELEASE_TIME) > (SUSTAIN_LEVEL - vol) * RELEASE_RATE) && (vol > 0)?
                isInRelease = 1:
                isInRelease = 0;
            (noteClock > RELEASE_TIME) && ((noteClock - RELEASE_TIME) > (SUSTAIN_LEVEL - vol) * RELEASE_RATE) && (vol <= 0)?
                AdsrComplete = 1:
                AdsrComplete = 0;

            noteClock++;                          // ADSRの4フェーズが終了するまでインクリメントし続ける。
            if     (isInAttack)  {vol++;}         // attackフェーズが終わるまで音量を上げ続ける。
            else if(isInDecay)   {vol--;}         // decayフェーズが終わるまで音量を下げ続ける。
            else if(isInRelease) {vol--;}         // releaseフェーズが終わるまで音量を下げ続ける。
            else if(AdsrComplete){noteClock = 0;} // ADSRの4フェーズが終わったら、
        }

        loop_until_bit_is_set(TIFR0, TOV0); // タイマーの1周期が終わるのを待ってから、
        OCR0A  = 128 + (uint8_t)mixer;      // 次のパルス幅をセットして、
        TIFR0 |= (1 << TOV0);               // オーバーフローフラグを手動でクリアする。

    }
     
    return 0;
}

f:id:ti-nspire:20191106084806p:plain
これでChapter 13: Advanced PWM Tricksが終わり。