BGM 再生といっても tone() のことです、悪しからず。

あまり実用的な使い道は思いつかないですが、折角作ったので PR しちゃいます。興味のある方は「Lチカしながら音楽再生・Arduino UNO R4のタイマと割り込みとクロック事情」に貼った動画を見てくれれば喜びます :grin:

BackgroundMusicR4 の特徴

仕組みは至って簡単。tone() とセットで使われる delay() の代わりに 前回紹介した   CallbackTimerR4 で音符の長さだけ待機しながら、次々に音符を再生します。従来 チュートリアル に記載のスケッチ例では演奏出来なかった4分音符+16分音符3連符といった音符も再現できるので、レパートリーの幅がグッと広がります!

インストール方法

CallbackTimerR4 のインストール

ベースとしてタイマ系ライブラリ CallbackTimerR4 が必要です。コチラ を参照の上、インストールしてください。

BackgroundMusicR4 のインストール

CallbackTimerR4-main.zipのダウンロード スケッチ → ライブラリをインクルード → .ZIP形式のライブラリをインストール…

Github の BackgroundMusicR4 リポジトリ から画面右側にある Releases をたどり、移動先のページから最新版の zip ファイルSource code (zip) をダウンロードします。

続いて Arduino IDE メニューの「スケッチライブラリをインクルード.ZIP形式のライブラリをインストール…」から、先にダウンロードした .zip ファイルを読み込ませれば、ライブラリフォルダにインストールされます。

使い方

スケッチのサンプル

ライブラリのインストール後、IDE メニューの「ファイルスケッチ例カスタムライブラリのスケッチ例」から、曲を再生しながら Lチカする blink_and_music を開くことができます。添付の曲は イングランド民謡Greensleeves です。

Arduino.h に続けて BGMusic.h をインクルードすれば CBTimer.h も読み込みます。

#include "Arduino.h"
#include "BGMusic.h"

// pitch and duration pairs in the melody:
// duration: 4 = quarter note, 8 = eighth note, etc.
#include "pitches.h"
static int melody[] = {
// Special thanks for the memody data from: https://github.com/robsoncouto/arduino-songs
#include "Greensleeves.h"
};

#define BUZZER_PIN  9     // pin number connected to the buzzer
#define TEMPO       70    // change this to make the song slower or faster
#define REPEAT      true  // playback repeatedly
#define N_NOTES(s)  (sizeof(s) / sizeof((s)[0]) / 2)  // number of notes in musical score.

void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(BUZZER_PIN,  OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);

  // initialize and start the BGM player
  static BGMusic music;
  music.begin(BUZZER_PIN, melody, N_NOTES(melody), TEMPO, REPEAT);
  music.start();
}

void loop() {
  // blink LED
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}

公式の チュートリアル と少し違うのは、音符と音符長のデータを1つの int 型配列にフラットに格納しているところで、Robson Couto さん のアイディアです。音符を定義した Greensleeves.h の中身はザッとこんな感じです。

  NOTE_G4,8,
  NOTE_AS4,4, NOTE_C5,8, NOTE_D5,-8, NOTE_DS5,16, NOTE_D5,8,
  NOTE_C5,4, NOTE_A4,8, NOTE_F4,-8, NOTE_G4,16, NOTE_A4,8,
  NOTE_AS4,4, NOTE_G4,8, NOTE_G4,-8, NOTE_FS4,16, NOTE_G4,8,
  NOTE_A4,4, NOTE_FS4,8, NOTE_D4,4, NOTE_G4,8,
  ...

ヘッダファイルとしてはイレギュラーですが、Arduino IDE が .txt とか .dat とかの拡張子を許してくれず、仕方なく .h としています。当然、この楽譜データを差し替えれば色々な曲に対応できます。

メンバ関数の説明

コンストラクタ/デストラクタ

BGMusic 変数名;

コンストラクタは特に何もしていません。一方、デストラクタは 依存するライブラリ CallbackTimerR4end() を呼び出して後始末をします。

例えば上記 スケッチのサンプル で、変数 musicstatic に確保するか、setup() の外でグローバル変数化しないと音が鳴らなくなるので要注意です。

また CallbackTimerR4 の都合により、残念ながら次のように複数の変数を宣言しても一方しか動作しません。

BGMusic 変数名1, 変数名2; // 変数名1と変数名2でFspTimerのインスタンスが共通となってしまう

初期化

bool begin(int pin, const int notes[], int n_notes, int tempo, bool loop = false, bool start = false);
  • int pin
    ブザー(自励式、ピエゾタイプ)など tone() の出力ピンを指定します。

  • const int notes[]
    音符の周波数と長さを並べた int 型の配列を指定します。配列の中身は変更されることがない想定のため、const 修飾子を付けて宣言してください。

  • int n_notes
    音符の周波数と長さを1組と数えた時の総数を指定します。スケッチのサンプル にあるマクロ N_NOTES() を使うと楽チンです。

  • int tempo
    メトロノーム記号 で表される曲の速さを指定します。例えばテンポ=155 は「4分音符を1分間に 155 回刻む速さ」を表します。

  • bool loop
    true を指定すると曲の終了後、直ちに再生を繰り返します。false が省略時のデフォルトです。

  • bool start
    true を指定すると初期化後、直ちに再生を開始します。省略時は false がデフォルトなので、別途 start() で再生を開始する必要があります。

開始、停止、終了のメソッド

bool start();
bool stop();
void end();

start() は曲の再生を開始、stop() は停止です。end() は確保していたリソースを解放します。

音符長計算関数の登録

void set_duration_function(int (*calc_duration_function)(int duration))

音符長をミリ秒単位で返す関数を登録する関数ですが、少々ややこしいので 後述 することにします :sweat_smile:

曲のテンポと音符長の関係

このページ を参考にまとめると、テンポと音符長の関係は次の様になります。

  • 曲のテンポは、1分間に刻むことができる4分音符の拍数で表す
  • 例えば 4分音符= 60 は、1分間に4分音符を60回を数える速さを表す

このテンポと4分音符一拍分の長さ(時間)を式で表すと以下となります。

4分音符一拍分の長さ(時間)

これを tone()チュートリアル に当てはめると、4分音符の長さを数値 4 で表し、その時間を 1000 ÷ 4 = 250[msec] としているので、1分間のテンポ数に換算すると 60000 ÷ 250 = 240、つまり4分音符= 240 ということになります。

そこで音の長さを表す数値をn、テンポを4分音符=tempo とすると、音符の長さ(時間)を求める式は4分音符一拍分の長さ(時間)となります。

BackgroundMusicR4 では互換性を考慮し、この式を元に音符長を決めています。

音符長計算関数について

一方、上記方式の問題点は、「・」のついた付点音符(元の音符の1.5倍の長さ)や、4分音符+16分音符を表す4分音符+16分音符などの音符長が整数で表せないことです。

float を使えばデータサイズこそ int と同じになりますが、浮動小数点演算が必要になります。UNO R4 のマイコンは FPU を持っているので実際にはそれほど問題にならないかも知れませんが、できれば整数演算で済ませたいところです。

ということで「音符長計算関数」の登場です。

音符長の計算式

まずは チュートリアル の音符長表現のおさらいです。

N分音符 16分音符 8分音符 4分音符 2分音符 全音符
音符長 16 8 4 2 1

直感的な分かり易さを優先させたのだと思いますが、短い音符ほど音符長を表す数値が大きくなってます。

これに対し、4分音符一拍分の長さ 4 を基準に(ここまでは(チュートリアル と同じ)、各音符の長さの比と、それを表すシンボルを割り当ててみます。直感的な分かり易さはそのままに、数値ではなく N4 などのシンボルを使う様にするワケです。

N分音符 16分音符 8分音符 4分音符 2分音符 全音符
音符長比 n 1 2 4 8 16
シンボル N16 N8 N4 N2 N1

ここで N 分音符の音符長比を表す数値をn、曲の速さをテンポとすれば、音の長さ(時間)を求める式は4分音符一拍分の長さ(時間)となります。

この方式であれば、4分音符と16分音符を タイ で1つに繋げた4分音符+16分音符の音符長は整数演算が可能な N4 + N16、即ち 5 で表せることになります。

また4分音符一拍中に3音を鳴らす3連符(Triplet)3連符も怖くありません。音符長比をそれぞれ3倍したシンボルを定義すれば良いだけです。

N分音符 16分音符 3連符 8分音符 4分音符 2分音符 全音符
音符長比n 1 × 3 = 3 4 2 × 3 = 6 4 × 3 = 12 8 × 3 = 24 16 × 3 = 48
シンボル N16 NT N8 N4 N2 N1

これで再現できる曲がグッと増えること間違いなしです!

音符長計算関数の登録

ということで、前述のメンバ関数 set_duration_function() は以下の様に使います。

#define BUZZER_PIN    9     // pin number connected to the buzzer
#define TEMPO         155   // change this to make the song slower or faster
#define REPEAT        true  // playback repeatedly
#define N_NOTES(s)    (sizeof(s) / sizeof((s)[0]) / 2)  // number of notes in musical score.
#define QUARTER_NOTE  ((60000 / N4) / TEMPO)  // ratio of the duration for one quarter note per beat

// Calculate note length from note length symbol
int calc_duration(int duration) {
  return QUARTER_NOTE * duration;
}

void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(BUZZER_PIN,  OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);

  // initialize and start the BGM player
  static BGMusic music;
  music.set_duration_function(calc_duration);
  music.begin(BUZZER_PIN, score, N_NOTES(score), TEMPO, REPEAT);
  music.start();
}

QUARTER_NOTE は「4分音符一拍分当たりの長さの比率」です。calc_duration() に渡される int durationN4 とか N8 とか N4 + N16 などの値となります。

上記のコードは background_music からの抜粋です。大好きな T-SQUARE の TRUTH を収録してあるので、よかったら聞いてみてください。

あとがき

バックグラウンドで音を鳴らすだけで、tone() で1個、CallbackTimerR4 ライブラリで1個、合計2個のタイマを使っちゃてるんですよね。

今回はかつて Cortex-M3 の LPC1343 ボードで作った機能(ご参考:play.c)の移植を目指してきましたが、移植に際し当初2つの案がありました。

  • Tone.cpp を丸っとコピーし、そこにミュージックシーケンサ的機能を組み込む
  • tone() はそのまま活かし、ミュージックシーケンサ的機能でラップする

前者であればタイマは1個で済んだハズですが、勉強も兼ねて 後者を選びました。

その上での感想ですが、まず第1に Cortex-M3 のレジスタに比べ Cortex-M4 のそれは何倍も難しくなっていること、そして第2に Arduino のプログラミングはライブラリのお陰で楽な反面、うまく折り合いを付けなきゃならない(コメントの少ないコードをリーディングするという)苦労があり、なかなかに手強いぞ!と感じてます。

ともあれ、豊富な情報と関連モジュール(H/W、S/W)との組み合わせが Arduino を楽しむコツと分かってきた今日この頃です :man_shrugging: