しゃべる何か / 2ビットDPCMデータを鳴らす

pp.396ff.
著者の用意した2ビットDPCMデータを復調し、PWMによるDDSで鳴らしてみる。2ビットDPCMデータはPROGMEMに保存しておく。

タイマー0をfast PWMモードにしてDPCM値に応じてデューティを順次変化させる、という処理をタイマー2の割り込みのタイミングで実行する。タイマー2は、CTCモードにして8kHzの頻度で割り込みが発生するようにする。8kHzは、音声データの元々のサンプリング周波数に合わせた値である。
f:id:ti-nspire:20200227074310p:plain:h250
https://github.com/ti-nspire/AVR/tree/master/talker

#define F_CPU 8000000UL

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "USART.h"
#include "allDigits.h"


const uint16_t tableLengths[] PROGMEM = {
    sizeof(ZERO_TABLE), sizeof(ONE_TABLE), sizeof(TWO_TABLE)  , sizeof(THREE_TABLE), sizeof(FOUR_TABLE),
    sizeof(FIVE_TABLE), sizeof(SIX_TABLE), sizeof(SEVEN_TABLE), sizeof(EIGHT_TABLE), sizeof(NINE_TABLE),
};
const uint8_t* const tablePointers[] PROGMEM = {
    ZERO_TABLE, ONE_TABLE, TWO_TABLE  , THREE_TABLE, FOUR_TABLE,
    FIVE_TABLE, SIX_TABLE, SEVEN_TABLE, EIGHT_TABLE, NINE_TABLE,
};

volatile uint8_t  *thisTableP;                    // 目的の音声排列を指し示すポインタ変数。
volatile uint16_t thisTableLength;                // 目的の音声排列の要素数。
volatile uint16_t sampleNumber;                   // 現在どのサンプルが再生されているか(2ビット単位で何番目のサンプルが再生されているか)。
volatile int8_t   out, lastout;                   // 現在のPWM値, 1つ前のPWM値。
volatile uint8_t  differentials[4] = {};          // 1バイトに含まれる4つの2ビットDPCMを一時的に保存しておくための変数。
const    int8_t   dpcmWeights[4] = {-12,-3,3,12}; // 復調時の差分。えいやで決めた値。

void initTimer0(){
    TCCR0A |= (1 << WGM00) | (1 << WGM01);
    TCCR0A |= (1 << COM0A0) | (1 << COM0A1);
    TCCR0B = (1 << CS00);
    OCR0A = 128;
    DDRD |= (1 << PD6);
}
void initTimer2(){
    TCCR2A = (1 << WGM21);
    TIMSK2 = (1 << OCIE2A);
    OCR2A = 125;
}
void startSampleTimer(){
    sampleNumber = 0;
    TCCR2B = (1 << CS21); // 8分周。これでタイマーへのクロック供給が始まってタイマーがスタートする。
}
void stopSampleTimer(){
    TCCR2B = 0;  // これでタイマーへのクロックの供給が遮断されてタイマーが止まる。
    OCR0A = 128; // 無音時はデューティを50%にしておく。
    lastout = 0; // 無音時はデューティを50%にしておく。
}
void updatePWMAudio(){
    OCR0A = out + 128; // 音圧値(-128~127)をコンペア値0~255に変換し、
    lastout = out;     // 新旧の音圧値を入れ換えて、
    sampleNumber++;    // 次の2ビットサンプルへ進む。
}
void unpackByte(uint8_t dataByte){             // 1サンプルが2ビットで構成されているので、取り出した1バイトの
    differentials[0] = (dataByte >> 6) & 0x03; // 最上位2ビット(4つの2ビットの中で一番若い)を取り出し、
    differentials[1] = (dataByte >> 4) & 0x03; // ...
    differentials[2] = (dataByte >> 2) & 0x03; // ...
    differentials[3] =  dataByte       & 0x03; // 最下位2ビットを取り出す。
}
void selectTable(uint8_t whichTable){
    thisTableLength = pgm_read_word(&tableLengths[whichTable]);                  // 目的の排列の要素数を取得する。
    thisTableP = (uint8_t *)pgm_read_word((uint16_t)&tablePointers[whichTable]); // 目的の排列の先頭アドレス(ポインタ)を取得する。
}
void talk(uint8_t whichTable){
    selectTable(whichTable);               // 目的の排列を選択して、
    startSampleTimer();                    // 8 kHzの頻度で割り込みを発生するタイマーをスタートさせ、
    loop_until_bit_is_clear(TCCR2B, CS21); // stopSampelTimer()によってクロックが遮断されるまで続ける。
}

ISR(TIMER2_COMPA_vect){
    uint8_t  cycle = sampleNumber & 0x03; // 1バイトに含まれる4つのサンプルの何番目が再生されているか。
    uint16_t tableEntry;
    uint8_t  packedData;
    if(cycle==0){
        tableEntry = sampleNumber >> 2; // 音声排列の何番目の要素にいるか(8ビット単位で数えたときの何番目にいるか)。
        if(tableEntry < thisTableLength){                        // 音声排列の末尾に達していなかったら、
            packedData = pgm_read_byte(&thisTableP[tableEntry]); // 目的の音声排列をPROGMEMから取り出して、
            unpackByte(packedData);                              // 取り出した1バイトを4つの2ビットデータに分けて、
            }else{
            stopSampleTimer();
        }
    }
    out = lastout + dpcmWeights[differentials[cycle]] - (lastout >> 4); // 前回の音圧値を増減する。歪み防止として(lastout >> 4)のぶんだけ減らしておく。
    updatePWMAudio();
}

int main(){

    initUSART();
    initTimer0();
    initTimer2();
    sei();

    while(1){
        talk(getNumber());
    }
    
    return 0;
}

DPCMを格納するヘッダーファイルは下のような形で用意する。これは"one"の音声を2ビットDPCM化した排列である。排列の各要素のサイズは8ビットであり、一つ一つが4つの2ビット値で構成されている。

const uint8_t ONE_TABLE[] PROGMEM = {153,90,106,150,105,86, ...};