第43回 2次元配列

今回はメロディーのそれぞれの音の長さを変えられるように変更します。

メロディーの音の長さを変えたい

タイマー時間になったらメロディーを演奏していますが、このメロディーのそれぞれの音の長さは全て同じです。例えば今のメロディーを変更して

「ドミソーミードミソーミードミソーシードーー」

というように音の長さを異なるようにしたい場合、今のスケッチでは対応できません。そこで、音の長さを変えられるように工夫をして、上のようなメロディーを演奏できるように変更したいと思います。

 

配列で実現する方法

現在のメロディー音のデータは以下のようにグローバル変数として配列にしています。

uint16_t alarm_melody[] = {DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, SI_4, DO_5};

このメロディーデータを演奏する部分は以下のようなスケッチで実現しています。

// メロディーを演奏する
for( oto_bangou=0; oto_bangou<ALARM_ONKAISUU; oto_bangou++ ) {
  tone(SPEAKER, alarm_melody[oto_bangou]);
  delay(200);
}

メロディーデータをtone命令を使用して鳴らしていますが、音の長さはdelay命令で決めています。現在のスケッチでは「delay(200);」としていますので、全ての音の長さは200msとなってしまっています。

このdelay命令のパラメータをそれぞれの音で変更できるようにすればいいのですが、どのように実現すればいいでしょうか。今まで習得した知識をもとに考えてみましょう。

例えば、音階データの配列と同じように、音の長さデータの配列を用意して、それぞれの音の長さのデータを設定しておく、という方法はどうでしょうか。

具体的には、現在の音階データは「alarm_melody[13]」という配列に入れてありますが、これとは別に配列、例えば「alarm_nagasa[13]」という配列を用意して以下のようにそれぞれの音の長さを入れておく、という方法です。

「ドミソーミドミソーミドミソーシードーー」という場合、普通の長さは200ms、「ソー」のようにちょっと長い場合は「400ms」、最後の「ドーー」は「800ms」の長さにしたい場合、alarm_nagasa配列を以下のように準備します。

配列要素番号 alarm_melody配列 alarm_nagasa配列
0 DO_4 200
1 MI_4 200
2 SO_4 400
3 MI_4 400
4 DO_4 200
5 MI_4 200
6 SO_4 400
7 MI_4 400
8 DO_4 200
9 MI_4 200
10 SO_4 400
11 SI_4 400
12 DO_5 800

メモ帳のイメージでは以下のような感じになります。

音階データ配列と音の長さデータ配列

つまり、スケッチで書くと以下のようになります。

uint16_t alarm_melody[] = {DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, SI_4, DO_5};
uint16_t alarm_nagasa[] = {200, 200, 400, 400, 200, 200, 400, 400, 200, 200, 400, 400, 800};

このように音階データの配列と音の長さデータの配列を用意すれば、例えば配列要素番号が2の音階データは「alarm_melody[2]」、その音の長さのデータは「alarm_nagasa[2]」というように配列を指定するときに同じ要素番号を指定すればOKです。

このように配列を用意しておくと、メロディー演奏部分では以下のようにスケッチを書けば演奏できるようになります。

// メロディーを演奏する
for( oto_bangou=0; oto_bangou<ALARM_ONKAISUU; oto_bangou++ ) {
  tone(SPEAKER, alarm_melody[oto_bangou]);
  delay(alarm_nagasa[oto_bangou]);
}

これで音の長さを指定することができるようになりましたが、メロディー音階や音の長さを変更したい場合、ちょっと不便な気がしませんか。

音階データと音の長さデータは以下のように別々の配列に入れています。

uint16_t alarm_melody[] = {DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, SI_4, DO_5};
uint16_t alarm_nagasa[] = {200, 200, 400, 400, 200, 200, 400, 400, 200, 200, 400, 400, 800};

このように配列を2つ用意した場合、それぞれの音階と音の長さの対応が分かりづらいですよね。例えば「ドミソーミドミソーミドミソーシードーー」の2回目に出てくる「ソー」(赤文字部分)の長さを普通の長さ(200ms)に変更したい場合、alarm_nagasa配列のどの数字を変更すればよいか、ちょっと分かりづらい感じがします。

そこで「2次元配列」という仕組みを利用してメロディーデータをもう少し分かりやすくしたいと思います。

 

2次元配列

これまでに習得した「配列」はメモ用紙がまとめられたメモ帳でした。この配列はタイトルがあって、そのタイトルの何ページ目かを指定するメモ帳でしたよね。

1次元配列

このメモ帳ですが、複数冊まとめて扱うことができる仕組みがあるんです。ここでは「メモ帳グループ」と呼んでおきます。

2次元配列

この「メモ帳グループ」は「タイトル」があり、その中に複数のメモ帳があります。特定のページを指定するには、「何冊目のメモ帳か」と「そのメモ帳の何ページ目か」を指定します。いずれの指定も「0」から始まることに注意してください。

このようなメモ帳グループを「2次元配列」と呼んでいます。最初に2次元配列の宣言方法を確認しましょう。

2次元配列宣言

この説明はちょっと分かりづらいかもしれませんが、先ほどの「メモ帳グループ」を思い出しながらこの宣言方法を理解してみてください。

次に具体例を説明します。

例えばメモ帳グループの名前を「memo」、メモ帳の冊数を2冊、メモ帳のページ数を3ページ、メモ用紙のサイズをuint16_t型とした場合、以下のように宣言します。

uint16_t memo[2][3];

このように宣言すると、以下のようなメモ帳グループが用意されます。

memo[2][3]

例えば、1冊目の2ページ目に「123」という数字を書きたい場合は、

memo[1][2] = 123;

と書きます。

この2次元配列を使用してメロディーデータをもう少し分かりやすくしてみます。具体的には、以下のようにメモディーデータを2次元配列に用意します。

メロディー配列

それぞれのメモ帳は、0ページ目が音階のデータ、1ページ目が音の長さにします。メロディーデータは13音ありますので、13冊のメモ帳を用意します。これらのメモ帳グループを「alarm_melody」という名前を付けるわけです。

このようなメモ帳グループを用意すると、最初の音階データは「alarm_melody[0][0]」、その音の長さのデータは「alarm_melody[0][1]」のメモ用紙を調べればわかることになります。

ところで、このような配列をなぜ「2次元」の配列と呼ぶのでしょうか。

私たちがいる世界は「3次元空間」と呼ばれていますよね。この「3次元」の「3」とは、空間の位置を特定するときに「縦」「横」「高さ」の3つの値が必要なためです。

今回の「2次元配列」は、メモ帳の特定のページを指定するのに「何冊目」の「何ページ目」か、という2つの値を使いましたよね。このように2つの値から特定の場所を指定していることから「2次元配列」と呼ばれています。

ということは、、、「3次元配列」とか「4次元配列」、さらには「100次元配列」とかあるんでしょうか。実はこれらの配列も宣言することができます。宣言方法は2次元配列と同じで、例えば

uint16_t memo[2][3][5];

と3次元配列を宣言した場合、「5ページ」のメモ帳を「3冊」のメモ帳グループとして用意し、さらにそのメモ帳グループを2つ用意する、というようになります。

ところで、2次元配列の仕組みや宣言方法はいいとして、2次元配列の初期化はどのように書けばよいのか、次に確認します。

 

2次元配列の初期化

2次元配列の初期化は以下のように行います。

2次元配列の初期化

なんだか、どれがどれだかややこしくなってきましたよね。

こごて、具体的な初期化の例を確認しておきましょう。例えばuint16_t型の2次元配列「uint16_t memo[3][2]」を初期化する場合、以下のように書いたとします。

uint16_t memo[3][2] = { {2000, 12}, {2001, 13}, {2002, 14} };

このように書くと、以下のように2次元配列要素に数字が入ります。

2次元配列の初期化例

なお、普通の配列を宣言するときは要素数は以下にように省略することができましたよね。

uint16_t memo[] = {12, 34, 56, 78};

2次元以上の配列も要素数は省略できますが、省略できるのは一番左側の要素数だけです。2次元配列の場合は、以下のようにメモ帳グループの数を省略することができます。メモ帳のページ数は省略できません。

uint16_t memo[][2] = { {1, 2}, {3, 4}, {5, 6}};

メロディーデータを2次元配列にしますが、具体的には以下のように2次元配列を用意することにします。

メロディーデータの2次元配列

これでメロディーデータを2次元配列で用意する準備ができました。それでは実際にメロディーデータを用意しましょう。

 

メロディーデータを2次元配列で宣言する

先程のようにスケッチに書けばいいのですが、スケッチに1行で書いてしまうとデータが長いので1行がすごく長くなってしまいます。そこで、スケッチを読みやすくするために1つの音データを1行で書くことにします。

uint16_t alarm_melody[][2] = {
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {SI_4, 400},
  {SO_5, 800}};

このように用意した2次元配列を使うと、0番目の音階データは「alarm_melody[0][0]」0番目の音階データは「alarm_melody[0][1]」になります。

このように2次元配列を使用すると、音階データとその音の長さデータの組み合わせで用意することができますので、あとから変更する場合、よりわかりやすくなります。

 

スケッチ

それでは最後にスケッチをまとめましょう。

今回変更した部分はメロディーデータの配列宣言部分と、メロディー演奏部分ですので、以下のスケッチを読み解いてみてください。

/*
 * キッチンタイマー
 * 
 * 内容: スイッチ、LED、スピーカーを使ったキッチンタイマー
 * 変更履歴:
 *   2019. 8.11: 新規作成
 *   2019. 8.15: スタートスイッチ処理を追加
 *   2019. 8.17: スイッチ関連の#define追加
 *   2019. 9. 8: 点滅回数カウント追加
 *               最終的に繰り返し処理をfor文で作成
 *   2019. 9.16: スケッチ動作開始時にLEDを点滅
 *   2019.10. 5: アラーム音追加
 *               動作開始時とタイマー時間の時に青色LEDを点灯するように変更
 *   2019.10.13: 残り時間LED制御追加
 *   2019.10.14: 残り時間が5秒以下になったらスピーカーを鳴らす動作を追加
 *   2019.11. 4: アラーム音をメロディーに変更
 *               音階定義をファイル化
 *   2019.11.11: メロディーを配列化
 *   2019.11.23: 残り時間表示LEDの関数化
 *   2019.11.24: 残り時間表示LED関数をswitch文に変更
 *   2019.12. 1: 残り時間を列挙型で宣言
 *   2019.12. 7: メロディーの長さを変えられるように変更
 */

// 音階定義ファイルのインクルード
#include "onkai.h"

// 秒を表現するLED関連(青色LED)
#define BYOU_LED 12 // 秒を表現する青色LEDの端子番号
#define BYOU_ON  50 // 秒を表現するLEDをつけている時間 (単位:ミリ秒)
#define BYOU_OFF 1000 - BYOU_ON // 秒を表現するLEDを消している時間 (単位:ミリ秒)

// 残り時間を表現するLED関連(緑色、黄色、赤色LED)
#define LED_MIDORI 8 // 緑色LEDの端子番号
#define LED_KIIRO  6 // 黄色LEDの端子番号
#define LED_AKA    4 // 赤色LEDの端子番号

// スタートスイッチ関連
#define SWITCH 23    // スイッチを接続している端子番号
#define SWITCH_OFF 1 // スイッチOFFの時のdigitalReadの値
#define SWITCH_ON  0 // スイッチONの時のdigitalReadの値

// タイマー時間設定(単位:秒)
#define TIMER_JIKAN 10

// 残り時間を表現するLEDの制御時間
#define KIIRO_JIKAN TIMER_JIKAN / 3
#define AKA_JIKAN   TIMER_JIKAN / 3 * 2

// アラーム音関連
#define SPEAKER 18  // スピーカーの端子番号
#define ALARM   880 // 通常のアラーム音周波数

// アラームメロディー関連
#define ALARM_ONKAISUU 13 // アラームメロディーの音の数
//uint16_t alarm_melody[] = {DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, SI_4, DO_5};  // アラームメロディー
uint16_t alarm_melody[][2] = {
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {SI_4, 400},
  {DO_5, 800}};

// 残り時間のenum型宣言
//            十分    半分以下  もうすぐ
enum nokori {jyuubun, hanbun, mousugu};


// 残り時間表示用LEDの制御関数
//   パラメータ: 残り時間目安
//       jyuubun: 残り時間十分
//       hanbun:  残り時間少ない
//       mousugu: もうすぐ設定時間
//   返り値:
//       なし
//
void controlTimeLed(enum nokori nokori_jikan) {
  
  // 残り時間に応じた処理
  switch(nokori_jikan) {

    // 残り時間十分
    case jyuubun:
      digitalWrite(LED_MIDORI, HIGH);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    LOW);
      break;
         
    // 残り時間半分以下
    case hanbun:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
      digitalWrite(LED_AKA,    LOW);
      break;

    // もうすぐ設定時刻
    case mousugu:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    HIGH);
      break;
  }

}


void setup() {
  // 端子の設定
  pinMode(BYOU_LED,   OUTPUT);   // 青色LED接続端子設定
  pinMode(LED_MIDORI, OUTPUT);   // 緑色LED接続端子設定
  pinMode(LED_KIIRO,  OUTPUT);   // 緑色LED接続端子設定
  pinMode(LED_AKA,    OUTPUT);   // 緑色LED接続端子設定
  pinMode(SWITCH, INPUT_PULLUP); // スイッチ接続端子の設定

  // 秒のLED(青色LED)を点灯する
  digitalWrite(BYOU_LED, HIGH);

  // スイッチが押されるまで待つ
  while(digitalRead(SWITCH) == SWITCH_OFF) {
  }

  // タイマー開始時に緑色LEDを点灯
  controlTimeLed(jyuubun); // 「残り時間十分」の表示

}


void loop() {
  
  uint8_t count;       // for文で回数を数えるために使用する変数
  uint8_t oto_bangou;  // for文でメロディーを鳴らすために使用する変数
  

  // TIMER_JIKAN分の回数を数える
  for( count=0; count<TIMER_JIKAN; count++) {

    // 残り時間表現用LEDの制御
    if( count == KIIRO_JIKAN ) {
      controlTimeLed(hanbun); // 「残り時間少ない」の表示
    }

    if( count == AKA_JIKAN ){
      controlTimeLed(mousugu); // 「もうすぐ設定時間」の表示
    }

    // 残り時間に応じて処理を変える
    if( count >= (TIMER_JIKAN - 5) ) {
      // 残り時間が5秒以下になったら、1秒に1回青色LEDを点滅して音を鳴らす
      digitalWrite(BYOU_LED, HIGH);
      tone(SPEAKER, ALARM);
      delay(BYOU_ON);
      digitalWrite(BYOU_LED, LOW);
      noTone(SPEAKER);
      delay(BYOU_OFF);
    } else {
      // そうでなければ、1秒に1回青色LEDを点滅する
      digitalWrite(BYOU_LED, HIGH);
      delay(BYOU_ON);
      digitalWrite(BYOU_LED, LOW);
      delay(BYOU_OFF);
    }
    
  }

  // 時間になったので秒のLED(青色LED)を点灯する
  digitalWrite(BYOU_LED, HIGH);

  // メロディー音を3回鳴らす
  for( count=0; count<3; count++) {

    // メロディーを演奏する
    for( oto_bangou=0; oto_bangou<ALARM_ONKAISUU; oto_bangou++ ) {
      tone(SPEAKER, alarm_melody[oto_bangou][0]);
      delay(alarm_melody[oto_bangou][1]);
    }
    
    // 音を消す
    noTone(SPEAKER);

    // 1.5秒あける
    delay(1500);

  }

  // 何もしないで待つ
  while( true ) {
  }

}

 

 

更新履歴

日付 内容
2019.12.7 新規投稿