Step04 Tunesのサンプルソース


#1

#include <Arduboy.h>

// 曲データは中略

Arduboy ab;

void setup()
{
    ab.beginNoLogo();
    ab.setFrameRate(30);

    ab.clear();
    ab.setTextSize(4);
    ab.setCursor(0,0);
    ab.print("Music\nDemo");

    ab.display();
}

void loop()
{
    if(!(ab.nextFrame()))
    {
        return;
    }

    if(!ab.tunes.playing())
    {
        ab.tunes.playScore(score);
    }
}

■ソースコードの場所

ファイル→スケッチの例→Arduboy→Tunes

曲データは省いています。コンパイルする場合はコピペしてください。

■解説

曲データの変換元はmidiファイルです。ツールにはmiditones32.exeを使用します。
また、コマンドオプションは以下の通り。

miditones32.exe musicfile -t2

-t2は、Arduboyの同時発音数が2の為です。チャンネル2つ分を使います。
0を音楽、1を効果音と完全に分けたい場合は-t1としてください。

BIO100%のSuper Depthと呼ばれるゲームを遊んだことがある方は、
こちらの変換データに差し替えてみてください。
ちょっとノスタルジックな気分になるかもしれません。

■Tunesの原理

少々悩みましたが・・・説明にはライブラリのaudio.cppを使わないことにします。
ちょーっと読みにくいですし、解説向きではないからです。
手前味噌ですが、自作したほぼ等価なソースコードを使います。

さて、まずは重要な部分から解説します。
タイマー割り込み用の関数です。名前は固定となっています。

// TIMER1 ch1。解説用に整形しています
ISR(TIMER1_COMPA_vect)
{
    *Snd.ch[1].pPinPort ^= Snd.ch[1].pinMask;

    if(--Snd.toneCnt == 0)
    {
        SndStopTone();
    }
}

割り込み毎にこの関数が呼ばれ、スピーカーのピンをON/OFFしています。
高速に繰り返して振動させているわけです。

Snd.toneCntが0になったら停止させるわけですが、次にSndStopTone関数を見てみましょう。

void SndStopTone(void)
{
    SndStopTimer(1);

    Snd.isTonePlay = FALSE;
}

void SndStopTimer(u8 ch)
{
    if(ch == 0)
    {
        TIMSK3 &= ~(1 << OCIE3A);
        *Snd.ch[0].pPinPort &= ~Snd.ch[0].pinMask;
    }
    else
    {
        TIMSK1 &= ~(1 << OCIE1A);
        *Snd.ch[1].pPinPort &= ~Snd.ch[1].pinMask;
    }
}

タイマーを止めれば割り込みしない(音が振動しない)というわけです。普通すぎてツッコミできません(汗。
最初に戻って初期処理を見てみましょう。

void SndInit(void)
{
    _Memset(&Snd, 0x00, sizeof(ST_SND));

    pinMode(SND_PIN1, OUTPUT);
    Snd.ch[0].pPinPort = portOutputRegister(digitalPinToPort(SND_PIN1));
    Snd.ch[0].pinMask  = digitalPinToBitMask(SND_PIN1);

    pinMode(SND_PIN2, OUTPUT);
    Snd.ch[1].pPinPort = portOutputRegister(digitalPinToPort(SND_PIN2));
    Snd.ch[1].pinMask  = digitalPinToBitMask(SND_PIN2);


    TCCR3A = 0;
    TCCR3B = 0;
    TCCR1A = 0;
    TCCR1B = 0;

    bitWrite(TCCR3B, WGM32, 1);
    bitWrite(TCCR3B, CS30,  1);
    bitWrite(TCCR1B, WGM12, 1);
    bitWrite(TCCR1B, CS10,  1);

    power_timer3_enable();
    power_timer1_enable();
}

ピン番号からメモリのアドレスと、そのアドレス内のMaskビットを見つけています。
タイマーの処理は作法なので説明を省きます。そこまで踏み込みません。
というかCPUの仕様書を読みたくないからです。(ぉ

次はお待ちかねの再生用の関数です。

void SndPlayTone(u16 freq, u32 duration)
{
    if(Snd.isTonePlay == TRUE)
    {
        return;
    }
    Snd.isTonePlay = TRUE;


    u32 cnt = 2 * freq * duration / 1000;

    if(cnt == 0)
    {
        return;
    }
    Snd.toneCnt = cnt;


    SndStartTimerCh(1, F_CPU / freq / 2);
}

たとえとして、以下のような呼び出しがあったとします。

SndPlayTone(440, 1000); // freq=440hz, duration=1000ミリ秒(1秒)

関数内は、

Snd.toneCnt = 2(ONとOFFの回数分) * 440(Hz) * 1000(1秒) / 1000;

となり、次に、SndStartTimerCh関数への引数は

1(チャンネル), 16000000L(F_CPU。Arduboyの1秒のCPU Hz) / 440(Hz) / 2(ONとOFFの回数)

を割り込みタイミングとして計算します。

void SndStartTimerCh(u8 ch, u32 freq)
{
    // timer ck/1
    u32 ocr = freq;
    u8  pre = 0x01;

    if(ocr > 0xffff)
    {
        // ck/64
        ocr /= 64;
        pre  = 0x03;
    }
    ocr--;


    if(ch == 0)
    {
        TCCR3B = (TCCR3B & 0xf8) | pre;
        OCR3A  = ocr;
        bitWrite(TIMSK3, OCIE3A, 1);
    }
    else
    {
        TCCR1B = (TCCR1B & 0xf8) | pre;
        OCR1A  = ocr;
        bitWrite(TIMSK1, OCIE1A, 1);
    }
}

ここもハードウェアの作法なので特にコメントしようもないです。

今まで解説した分は1音分です。miditonesを使用した場合は配列内に音(Hz)、音の長さ、休符などが
盛り込まれています。それほど長くもないですし、軽く流し読んでみるのもいいかもしれません。