Arduino UNO R4でBGM

Arduino UNO R4

※この記事は、Arduino UNO R4のみに対応しています

タイマー割り込みで音楽を演奏する

今回は『Arduinoで音楽演奏する』と『Arduino UNO R4でタイマー割り込みを使う』を組み合わせて、loop()関数で実行されるメインの処理とは独立に、BGMとして音楽演奏をしてみます。

プログラムの技術的なことは各過去記事を参照してください。

製作&実行

スケッチ

C++
#include "FspTimer.h"
#define SPPIN 2

FspTimer _timer;

int freq[]={262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047};

struct NOTE {
  int tone;
  float length;
};

NOTE sheet[]={
    {0, 1}, {0, 1}, {7, 1}, {7, 1}, {9, 1}, {9, 1}, {7, 2}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 1}, {2, 1}, {0, 2},
    {7, 1}, {7, 1}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 2}, {7, 1}, {7, 1}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 2}, 
    {0, 1}, {0, 1}, {7, 1}, {7, 1}, {9, 1}, {9, 1}, {7, 2}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 1}, {2, 1}, {0, 2}
};

void play(timer_callback_args_t *arg){
  static int nc=0;
  static int toneNo=-1;
  static int timer=0;
  static int length=0;
  static int count=sizeof(sheet)/sizeof(NOTE);

  if(timer<=0){
    if(nc>=count){
      nc=0;
      timer=5000;
    }else{
      toneNo = sheet[nc].tone;
      length = sheet[nc].length*500;
      if(tone>=0){ 
        tone(SPPIN, freq[toneNo]);
      }
      timer=length;
      nc++;
    }
  }else if(toneNo>=0 && timer<=length*0.05){
    noTone(SPPIN);
    toneNo=-1;
  }
  timer--;
}

void setup() {
  Serial.begin(9600);
  pinMode(SPPIN, OUTPUT);

  uint8_t type;
  int8_t ch = FspTimer::get_available_timer(type);
  if (ch < 0) {
    return;
  }
  _timer.begin(TIMER_MODE_PERIODIC, type, ch, 1000.0f, 50.0f, play, nullptr);
  _timer.setup_overflow_irq();
  _timer.open();
  _timer.start();
}

void loop() {
  static int i=0;
  Serial.println(i++);
  delay(1000);
}

プログラムの解説

タイマー割り込みに関する部分
C++
#include "FspTimer.h"
#define SPPIN 2

FspTimer _timer;
C++
void setup() {
  Serial.begin(9600);
  pinMode(SPPIN, OUTPUT);

  uint8_t type;
  int8_t ch = FspTimer::get_available_timer(type);
  if (ch < 0) {
    return;
  }
  _timer.begin(TIMER_MODE_PERIODIC, type, ch, 1000.0f, 50.0f, play, nullptr);
  _timer.setup_overflow_irq();
  _timer.open();
  _timer.start();
}

まず、タイマー割り込みに関する部分です。R4用のタイマー割り込みライブラリFspTimerを使用して、『1msに1回、play()という関数を実行する』ように設定しています。個別の関数の書式などは『Arduino UNO R4でタイマー割り込みを使う』の記事を参照してください。

演奏データに関する部分
C++
int freq[]={262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047};

struct NOTE {
  int tone;
  float length;
};

NOTE sheet[]={
    {0, 1}, {0, 1}, {7, 1}, {7, 1}, {9, 1}, {9, 1}, {7, 2}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 1}, {2, 1}, {0, 2},
    {7, 1}, {7, 1}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 2}, {7, 1}, {7, 1}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 2}, 
    {0, 1}, {0, 1}, {7, 1}, {7, 1}, {9, 1}, {9, 1}, {7, 2}, {5, 1}, {5, 1}, {4, 1}, {4, 1}, {2, 1}, {2, 1}, {0, 2}
};

演奏データに関する部分です。『Arduinoで音楽演奏する』のきらきら星のデータをそのまま流用しています。

BGMとして演奏する
C++
void play(timer_callback_args_t *arg){
  static int nc=0;
  static int toneNo=-1;
  static int timer=0;
  static int length=0;
  static int count=sizeof(sheet)/sizeof(NOTE);

  if(timer<=0){
    if(nc>=count){
      nc=0;
      timer=5000;
    }else{
      toneNo = sheet[nc].tone;
      length = sheet[nc].length*500;
      if(tone>=0){ 
        tone(SPPIN, freq[toneNo]);
      }
      timer=length;
      nc++;
    }
  }else if(toneNo>=0 && timer<=length*0.05){
    noTone(SPPIN);
    toneNo=-1;
  }
  timer--;
}

このplay()関数がBGMとして演奏するための処理の本体です。この関数はタイマー割り込みによって1ms毎に呼び出されるので、関数が呼び出されるたびに変数をカウントダウンすることで次の音に切り替えるタイミングを判断しています。

ローカル変数
C++
void play(timer_callback_args_t *arg){
  static int nc=0;
  static int toneNo=-1;
  static int timer=0;
  static int length=0;
  static int count=sizeof(sheet)/sizeof(NOTE);

以下のローカル変数はstaticで定義されているので、次の関数呼び出し時にも値を保持します。

名前意味
ncint次に発音すべき音符が楽譜の何番目か(sheet[]の要素番号)
toneNoint現在の音符の音の高さ(freq[]の要素番号)
timerint現在の音符が終了するまでの残り時間をmsで表す
lengthint現在の音符の長さをmsで表す
countintsheet[]に含まれる要素(音符)の総数
次の音符に移るタイミングかの判定
C++
  if(timer<=0){
    if(nc>=count){
      nc=0;
      timer=5000;
    }else{
      toneNo = sheet[nc].tone;
      length = sheet[nc].length*500;
      if(tone>=0){ 
        tone(SPPIN, freq[toneNo]);
      }
      timer=length;
      nc++;
    }
  }else if(toneNo>=0 && timer<=length*0.05){
    noTone(SPPIN);
    toneNo=-1;
  }

変数timerが、現在の音符が終了するまでの残り時間を表しています。timer>0のときは現在の音符を継続中なのですから、なにもしません。

timer≦0のときは、現在の音符は終了するタイミングなので、次の音符に進む処理をします。

曲が終了している場合
C++
  if(timer<=0){
    if(nc>=count){
      nc=0;
      timer=5000;
    }else{
      toneNo = sheet[nc].tone;
      length = sheet[nc].length*500;
      if(tone>=0){ 
        tone(SPPIN, freq[toneNo]);
      }
      timer=length;
      nc++;
    }
  }else if(toneNo>=0 && timer<=length*0.05){
    noTone(SPPIN);
    toneNo=-1;
  }

変数ncは『次に発音するべき音符が楽譜の何番目か』を配列sheet[]の要素番号で表し、変数countは配列sheet[]の要素数を表しています。つまり『次の音符に進む』タイミングで nc≧count となったということは、sheet[]に記述されているすべての音符の演奏が終わった(曲の最後まで演奏した)ということです。

このとき、以下の2つのことを行います。

  • 変数ncを0にセットします。これは、また曲の最初から演奏をすることを意味します。
  • 変数timerを5000にセットします。これは、これからplay()関数が5000回呼ばれるまで(5000ms=5秒が経過するまで)次の音を出さないことを意味します。

つまり、最後まで演奏が終わったら、5秒待ってから再び曲の最初から演奏することになります。

次の音を出力
C++
  if(timer<=0){
    if(nc>=count){
      nc=0;
      timer=5000;
    }else{
      toneNo = sheet[nc].tone;
      length = sheet[nc].length*500;
      if(tone>=0){ 
        tone(SPPIN, freq[toneNo]);
      }
      timer=length;
      nc++;
    }
  }else if(toneNo>=0 && timer<=length*0.05){
    noTone(SPPIN);
    toneNo=-1;
  }

まだ演奏すべき音符が残っている場合は、次の音符を発音します。手順は以下の通りです。

  • 変数toneNoに、いまから発音すべき音符の高さ(配列freq[]の要素番号で表す)をセットします
  • 変数lengthに、いまから発音すべき音符の長さをms単位で計算して代入します。
  • 変数toneNo≧のとき(toneNo<0は休符を表します)、tone()関数で周波数freq[toneNo]の音を発生します。
  • 変数timer(現在の音符の残り時間)に変数length(現在の音符の長さ)の値をセットします。
  • 変数ncの値を1増やします(次に発音すべき音符を1つ進めます)。

以前のサンプル『Arduinoで音楽演奏する』とは異なり、tone()関数の第3引数で音符の長さを指定していません。

sheet[nc]の音をlengthの95%の長さだけ保つように指定して音声を発します。音符の長さはlengthで表されますが、実際に音を発する時間をlengthの95%にすることによって、同じ高さの音が連続した場合にもちゃんと区切って聞こえるようにしています。

音符の長さの95%で発音を停止します
C++
  if(timer<=0){
    if(nc>=count){
      nc=0;
      timer=5000;
    }else{
      toneNo = sheet[nc].tone;
      length = sheet[nc].length*500;
      if(tone>=0){ 
        tone(SPPIN, freq[toneNo]);
      }
      timer=length;
      nc++;
    }
  }else if(toneNo>=0 && timer<=length*0.05){
    noTone(SPPIN);
    toneNo=-1;
  }

toneNo≧0(toneNoは休符または発音停止中なら-1なので、『現在発音中』の意味になる)かつ
timer≦length*0.05(現在の音符の残り時間が音符の長さの5%以下)
であったとき、現在の音符の発音を停止します。既に発音が止まったことを表すため、変数toneNoに休符を表す-1をセットします。

なぜ残り5%で発音を止めてしまうかというと、長さいっぱい発音してしまうと同じ高さの音符が連続したときにつながった1つの長い音に聞こえてしまうためです。5%分の無音区間を作ることで、別の音符がちゃんと区切れて聞こえるようにしています。

残り時間を減らす
C++
  timer--;
}

変数timer(現在の音符の残り時間)の値を1だけ減らします。play()関数はタイマー割り込みによって1msごとに呼び出されるので、timerの値は1msに1ずつ減っていきます。

BGM演奏とは無関係な部分
C++
void setup() {
  Serial.begin(9600);
  pinMode(SPPIN, OUTPUT);

  uint8_t type;
  int8_t ch = FspTimer::get_available_timer(type);
  if (ch < 0) {
    return;
  }
  _timer.begin(TIMER_MODE_PERIODIC, type, ch, 1000.0f, 50.0f, play, nullptr);
  _timer.setup_overflow_irq();
  _timer.open();
  _timer.start();
}

void loop() {
  static int i=0;
  Serial.println(i++);
  delay(1000);
}

setup()関数内のSerial.begin()関数及びloop()関数により、1000msごとにカウントアップする数値をシリアル送信するようになっています。これはBGM演奏とはまったく関係ない処理です。loop()関数の内容とBGM演奏が独立・並行して処理されることを確認するために実装したものです。

実行結果

シリアルモニタのカウントと演奏が独立に実行されていることが判ります。演奏のテンポがちょうど2音符で1秒なのでタイミングが合っているように見えてしまいますが。

Arduino UNO R4以外での実行

Arduino UNO R3以前にもMsTimer2というタイマー割り込みライブラリがあるのですが、どうもMsTimer2とtone()が同じ割り込みを共有しているようで、単純にFspTimerとMsTimer2を置き換えれば動く、というわけにはいかないようです。

まとめ

・割り込みタイマーを応用すると、loop()関数内のメインの処理とは独立して音楽演奏することができる

コメント

タイトルとURLをコピーしました