pp.396ff.
著者の用意した2ビットDPCMデータを復調し、PWMによるDDSで鳴らしてみる。2ビットDPCMデータはPROGMEMに保存しておく。
タイマー0をfast PWMモードにしてDPCM値に応じてデューティを順次変化させる、という処理をタイマー2の割り込みのタイミングで実行する。タイマー2は、CTCモードにして8kHzの頻度で割り込みが発生するようにする。8kHzは、音声データの元々のサンプリング周波数に合わせた値である。
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, ...};