モールス信号シールドでC言語プログラミング入門 (3)

第3回はブザーを使ったメロディーの再生を題材に「配列」を扱います。第1回ではLチカ(LEDの点灯)のコントロールを通じて「定数」を、第2回ではその点灯パターンを変更させて「変数」と「繰り返し処理」を学びました。定数や変数は数値を記憶しておいてスケッチの任意の場所から参照するのに便利でした。

しかし、複数の定数が存在するとどうでしょう。ある曲のメロディーを定義するのに、個々の音符ごとに定数や変数を用意するとなると、その名前を考えるだけでもうんざりしてしまいます。そこで便利なのが配列です。配列は、同じ「型」の定数を順序立てて宣言・定義しておくことができます。

今回は配列を利用してブザーを鳴動するためのスマートな方法を扱います。tone()関数とnoTone()関数という、PWM制御をしてくれる便利な関数を使います。

PWM制御とは

ここで、PWMについて少しだけ説明しておきます。PWMとはPulse Width Modulationの頭文字を取ったものでパルス幅変調のことです。図のように1周期(Period)あたりのオン区間のパルス幅(Duty)を調節することで、LEDの輝度や音の音量を調整できます。パルス幅が0%だとdigitalWrite(ピン番号, LOW)と同じで、同じく100%だとdigitalWrite(ピン番号, HIGH)と同じことになります。

音の場合、周期とデューティ比を調整すると音の高低や音量を変化させることができます。Arduinoのtone()関数は指定した周波数の矩形波(50%デューティ)を生成し、この信号をブザーに印加することで様々な周波数の音を発生できます。

一方、音以外を対象にPWM制御するにはanalogWrite()関数を使ってデューティ比を指定します。その時のPWM信号の周波数は使用するデジタル出力ピンによって異なり、約490Hzまたは約980Hzの矩形波を出力します。例えば、LEDの明るさやモータの回転スピードを制御するために使えます。

ブザーでメロディを再生する

それでは、tone()関数とnoTone()関数を使って簡単なメロディを再生するためのスケッチです。Arduino Unoへ書き込むと、定義したメロディが1回だけ再生されます。

#define PIN_BZ 9 // ブザーは9番ピンに繋がっている

// メロディを構成する音の周波数[Hz]
int melody[] = {
  880, 988, 1109, 988, 880, 0, 880, 988, 1109, 988, 880, 988
};

// 音の長さ: 4 = 四分音符, 8 = 八分音符, など
int noteDurations[] = {
  8, 8, 2, 8, 8, 4, 8, 8, 8, 8, 8, 2
};

void setup() {
  // 順にメロディを構成する音を発生していく
  for (int thisNote = 0; thisNote < 12; thisNote++) {

    // 音符ごとの長さ[ms]を計算するため, 1秒を音符の種類で割る
    // 例) 四分音符 = 1000/4, 八分音符 = 1000/8, など
    int noteDuration = 1000 / noteDurations[thisNote];
    // 音を発生
    tone(PIN_BZ, melody[thisNote], noteDuration);

    // 音符どうしを区別するために音符間にスペースを入れる
    // 音符の長さ+30%程度がちょうどいい
    int pauseBetweenNotes = noteDuration * 1.30;
    delay(pauseBetweenNotes);
    // 音の発生を停止
    noTone(PIN_BZ);
  }
}

void loop() {
  // 繰り返し処理は無し
}

さて、何のメロディだったでしょうか? 休符の長さが違いますが、明星食品のホームページに楽譜がありました。

それではスケッチの説明をしていきます。下図のようにシールド上のブザーBZはArduino Unoの9番ピンに繋がっていますので、defineマクロ定数「PIN_BZ」を「9」で定義しておきます。

melody[ ]とnoteDurations[ ]は整数(int)型の配列です。この2つの配列はそれぞれメロディの音程[Hz]と1音の長さ[ms]を表現しています。整数(int)型ですから、いずれも小数点がある実数は含められません。また、それぞれの配列要素を1つずつ対応付けて1つの音符を表現しますので、いずれも12個の要素で構成してあります。配列内の個々の要素はカンマ「,」で区切り、全ての要素は波括弧(中括弧)「{」と「}」で括ります。

スケッチ例では3行に渡って定義していますが、次のように改行せずに1行で記述しても構いません。ただ、配列の要素が増えてくると全体を見通しにくくなってしまいます。

// メロディを構成する音の周波数[Hz]
int melody[] = {880, 988, 1109, 988, 880, 0, 880, 988, 1109, 988, 880, 988};

実際にメロディを再生する部分は、for文で繰り返しているブロックです。for文は前回、繰り返し処理に使う文として紹介しましたね。

for文の初期化式の「thisNote = 0」は、0番目の音符から再生を開始することを意味します。条件式の「thisNote < 12」は、11番目の音符まで再生することを意味します。変化式の「thisNote++」は、音符を1つ再生したら、thisNoteの値を1だけ増加して、次の音符を指し示すという意味です。

// 順にメロディを構成する音を発生していく
for (int thisNote = 0; thisNote < 12; thisNote++) { ... }

音の振幅をPWM制御で発生させるのはtone()関数です。tone(PWM制御するピン番号, 周波数[Hz], 持続時間[ms])のように指定します。

// 音を発生
tone(PIN_BZ, melody[thisNote], noteDuration);

1番目の引数「PIN_BZ」はどのピンをPWM制御するかを指定します。スケッチの冒頭で「PIN_BZ」は「9」で定義されていて、ブザーが繋がっている「9番」ピンを意味します。

2番目の引数「melody[thisNote]」は再生する音の周波数[Hz]を意味しますが、for文の処理が始まった当初は「thisNote」の値は「0」ですから「melody[0]」、つまりメロディを構成する最初の音符の周波数が指定されます。for文でブロックの処理が繰り返されるたび、「thisNote」の値は「1」ずつ増加していきますので、「melody[1]、melody[2]、melody[3]、、、」と次々に音符が再生されていきます。

3番目の引数「noteDuration」はPWM制御を継続する期間[ms]を指定します。最後の持続時間は省略できますが、その場合はnoTone()を実行するまで、動作を継続します。なお、音量はコントロールできません。

// 音の発生を停止
noTone(PIN_BZ);

noTone()関数で音の発生を停止します。引数の「PIN_BZ」はPWM制御を停止するピン番号を指定します。

自由に作曲してみる

仕組みが分かったところで自由に作曲してみましょう。音符ごとに周波数を指定するのは面倒なのでdefineマクロ定数で音の名前と音程を定義しておきます。音程の定義だけでかなりスケッチが長くなってしまいますが気にせず進めましょう。

例えば、ドの音は「C5」で523[Hz]と定義されています。また、「C5S」など最後にSが付いているのはシャープの意味で、ピアノで言うと黒鍵です。「0」とすれば0[Hz]ですから休符となります。なお、シールド上のブザーは小さいのでなるべく高い音を使った方が再現性が良いです。

#define B0  31
#define C1  33
#define C1S 35
#define D1  37
#define D1S 39
#define E1  41
#define F1  44
#define F1S 46
#define G1  49
#define G1S 52
#define A1  55
#define A1S 58
#define B1  62
#define C2  65
#define C2S 69
#define D2  73
#define D2S 78
#define E2  82
#define F2  87
#define F2S 93
#define G2  98
#define G2S 104
#define A2  110
#define A2S 117
#define B2  123
#define C3  131
#define C3S 139
#define D3  147
#define D3S 156
#define E3  165
#define F3  175
#define F3S 185
#define G3  196
#define G3S 208
#define A3  220
#define A3S 233
#define B3  247
#define C4  262
#define C4S 277
#define D4  294
#define D4S 311
#define E4  330
#define F4  349
#define F4S 370
#define G4  392
#define G4S 415
#define A4  440
#define A4S 466
#define B4  494
#define C5  523
#define C5S 554
#define D5  587
#define D5S 622
#define E5  659
#define F5  698
#define F5S 740
#define G5  784
#define G5S 831
#define A5  880
#define A5S 932
#define B5  988
#define C6  1047
#define C6S 1109
#define D6  1175
#define D6S 1245
#define E6  1319
#define F6  1397
#define F6S 1480
#define G6  1568
#define G6S 1661
#define A6  1760
#define A6S 1865
#define B6  1976
#define C7  2093
#define C7S 2217
#define D7  2349
#define D7S 2489
#define E7  2637
#define F7  2794
#define F7S 2960
#define G7  3136
#define G7S 3322
#define A7  3520
#define A7S 3729
#define B7  3951
#define C8  4186
#define C8S 4435
#define D8  4699
#define D8S 4978

#define PIN_SW 5 // SW1は5番ピンに繋がっている
#define PIN_BZ 9 // ブザーは9番ピンに繋がっている

// メロディを構成する音の周波数
int melody[] = {
  A5, B5, C6S, B5, C6S, B5, A5, 0, A5, B5, C6S, B5, C6S, B5, A5, B5
};

// 音の長さ: 4 = 四分音符, 8 = 八分音符, など
int noteDurations[] = {
  8, 8, 1, 16, 16, 8, 8, 4, 4, 8, 8, 12, 16, 16, 8, 1
};

void setup() {
  pinMode(PIN_BZ, OUTPUT);
  pinMode(PIN_SW, INPUT_PULLUP);
}

void loop() {
  while (digitalRead(PIN_SW) == HIGH) ; // SW1を押すまで待機

  // 順にメロディを構成する音を発生していく
  for (int thisNote = 0; thisNote < sizeof(melody)/sizeof(int)
; thisNote++) {

    // 音符ごとの長さ[ms]を計算するため, 1秒を音符の種類で割る
    // 例) 四分音符 = 1000/4, 八分音符 = 1000/8, など
    int noteDuration = 1000 / noteDurations[thisNote];
    // 音を発生
    tone(PIN_BZ, melody[thisNote], noteDuration);

    // 音符どうしを区別するために音符間にスペースを入れる
    // 音符の長さ+30%程度がちょうどいい
    int pauseBetweenNotes = noteDuration * 1.30;
    delay(pauseBetweenNotes);
    // 音の発生を停止
    noTone(PIN_BZ);
  }
}

今度は、デバッグしやすいように、SW1が押されるたびにメロディが再生されるように書き換えました。詳しくは別の機会に解説しますが、loop()関数の先頭にある、

while (digitalRead(PIN_SW) == HIGH) ; // SW1を押すまで待機

によって、SW1が押されるまで待機しておき、その先へ処理が進まないようにすることができます。

また、for文のパラメータのうち2つ目の条件式を変更しました。条件式をこのようにしておくと、

thisNote < sizeof(melody)/sizeof(int)

配列の最後まで順に音符が処理されるようになります。sizeof(melody)は配列melody[ ]の全体の大きさを、sizeof(int)は整数(int)型の要素1つ分の大きさを表します。つまり、sizeof(melody)/sizeof(int)を計算すると配列に含まれる全要素の数を知ることができます。再生中の要素の位置が全要素より小さければ、処理が継続します。こうしておくと、メロディを構成する音符が変わるたびに数値で指定するより楽ですね。

例えば、「カエルの歌」の音符を配列で定義すると、下記のようになります。音の長さは整数しか定義できませんので注意してください。実数で定義しようとしても小数点以下は無視されてしまいます。

// メロディを構成する音の周波数
int melody[] = {
// か  え  る  の  う  た  が      き  こ  え て   く  る  よ     クワ    クワ   クワ    クワ   ケ  ロ  ケ  ロ   ケ  ロ  ケ  ロ  クワクワクワ 
  C5, D5, E5, F5, E5, D5, C5, 0, E5, F5, G5, A5, G5, F5, E5, 0, C5, 0, C5, 0, C5, 0, C5, 0, C5, C5, D5, D5, E5, E5, F5, F5, E5, D5, C5
};

// 音の長さ: 4 = 四分音符, 8 = 八分音符, など
int noteDurations[] = {
  4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 8, 8, 8, 8, 4, 4, 2
};

配列との付き合い方

今回は、整数(int)型の配列を使ってメロディを再生してみました。音程については次のように定義しましたが、

int melody[ ] = {880, 988, 1109, 988, 880, 0, 880, 988, 1109, 988, 880, 988};

次のように要素数を明示的に指定しても構いません。

int melody[12] = {880, 988, 1109, 988, 880, 0, 880, 988, 1109, 988, 880, 988};

この配列の要素は12個ですが、個々の要素の数え方に注意します。最初の要素を得るにはmelody[0]と表現し、1ではなく0から開始します。つまり最後の要素はmelody[12]ではなく、melody[11]です。

1つ目のスケッチでfor文の「thisNote < 12」となっていたのはそのためです。melody[12]という要素は存在しませんから、thisNoteが12になる直前まで音符melody[thisNote]の発生を繰り返します。

melody[thisNote]の[ ]内の整数thisNoteの部分を添え字またはインデックスと呼びます。for文の中でthisNoteの値を1つずつ変化させることで、指定したインデックスの配列要素へアクセスし、定義された音符の情報を得ていたわけです。

今回はメロディを構成する音符を配列で表現しました。このように同じ型の変数を多数、扱いたいときに配列は重宝します。実は配列はまだまだ奥が深く、多次元の配列による表現やポインタとの関連などをお話したいのですが、それはまた別の機会に。

次回はLCDに表示するための文字列を扱う予定です。

カテゴリー: プログラミング入門 パーマリンク