第43回 2次元配列

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

目次

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

タイマー時間になったらメロディーを演奏していますが、現在のスケッチではメロディーのそれぞれの音の長さは全て同じです。

例えば今のメロディーを変更して

ドミソーミードミソーミードミソーシードーー

というように音の長さを異なるようにしたい場合、今のスケッチでは対応できません。

そこで、音の長さを変えられるようにスケッチを変更して、上のようなメロディーを演奏できるように変更したいと思います。

配列で実現する方法

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

uint16_t 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(uint8_t i=0; i<MELODY_LENGTH; i++ ) {
  tone(SPEAKER, melody[i]);
  delay(200);
}

tone関数を使用してメロディーのそれぞれの音を鳴らしていますが、音の長さはdelay関数で決めています。

現在のスケッチではdelay(200);としていますので、全ての音の長さは200ms固定となってしまっています。


このdelay関数の引数をそれぞれの音で変更できるようにすればいいのですが、どのように実現すればいいでしょうか?

今まで習得した知識をもとに考えてみましょう。

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

具体的には、現在の音階データはmelody[13]という配列に入れてありますが、これとは別の配列、例えばlength[13]という配列を用意してそれぞれの音の長さを入れておく、という方法です。(「lengh」は日本語で「長さ」という意味なのでこの配列名にしてみました)

具体的なスケッチで考えてみます。

「ドミソーミドミソーミドミソーシードーー」というメロディーの場合、音の長さは3種類あります。

出だしの「ド」は一番短く、「ドミソー」の「ソー」はその2倍の長さ、最後の「ドーー」はさらに2倍の長さという感じです。

一番短い音の長さは200msで演奏していましたので、他の音は「ソー」のように2倍の長さの場合は「400ms」、最後の「ドーー」は「800ms」の長さにしてみようと思います。

この場合、length[13]という配列を準備して次のような値を代入しておくことにします。

配列要素番号melody配列の値
(音程の情報)
length配列の値
(音の長さの情報)
0DO_4200
1MI_4200
2SO_4400
3MI_4200
4DO_4200
5MI_4200
6SO_4400
7MI_4200
8DO_4200
9MI_4200
10SO_4400
11SI_4400
12DO_5800

メモ帳のイメージでは次のような感じです。

スケッチで書くと次のようになります。

uint16_t 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 length[] = {200, 200, 400, 400, 200, 200, 400, 400, 200, 200, 400, 400, 800};

このように音階データの配列と音の長さデータの配列を用意すれば、例えば配列の要素番号が2の音階データはmelody[2]その音の長さのデータはlength[2]というように指定できます。

このように配列を用意しておくと、メロディー演奏部分では以下のようにスケッチを書けば演奏できるようになります。(3行目のdelay関数の引数でlength配列を指定しています)

for(uint8_t i=0; i<MELODY_LENGTH; i++ ) {
  tone(SPEAKER, melody[i]);
  delay(length[i]);
}

これで音の長さを個別に指定できるようになりました!


よかった、よかった、といきたいところですが、、、

音の長さを変更したい場合、ちょっと不便な気がしませんか?

もう一度、メロディーの配列を見てみましょう。

uint16_t 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 length[] = {200, 200, 400, 400, 200, 200, 400, 400, 200, 200, 400, 400, 800};

「音程データ」と「音の長さデータ」は別々の配列に入れています

このように配列を別々に用意した場合、それぞれの音の音程と長さの対応が分かりづらいですよね。

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

この音は7番目なので、length配列の7番目(配列要素番号6)の「400」を探して「200」に変更すれば良いのですが、いちいち探すのも大変ですよね。

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

2次元配列

2次元配列のイメージ

最初に以前習得した「配列」の確認をしておきましょう。

「配列」は、イメージとしてはメモ用紙がまとめられたメモ帳でした。

1次元配列

「配列」にはタイトルがあって、そのタイトルの何ページ目かを指定できるメモ帳でしたよね。(変な特徴はありましたが…)


「2次元配列」とは、このメモ帳を複数まとめて扱う仕組みというイメージです。イメージとしては「メモ帳グループ」という感じでしょうか。

具体的なイメージとしては次のような感じです。

2次元配列

このメモ帳グループ(2次元配列)の特徴として、次のようなものがあります。

メモ帳グループ(2次元配列)の特徴
  • メモ帳グループ自体の「タイトル」がついています。
  • メモ帳グループの中に、「複数のメモ帳」があります。
  • メモ帳グループ内の複数のメモ帳は、すべて同じページ数です。(0冊目は10ページ、1冊目は25ページ、というようにそれぞれ異なるページ数にすることはできません)
  • メモ帳グループ内の複数のメモ帳のメモ用紙は、すべて同じサイズです。(0冊目はuint8_t型、1冊目はuint16_t型、というようにそれぞれ異なるサイズにすることはできません)
  • このメモ帳グループの特定のページを指定するには、どの「タイトル」のメモ帳グループ「何冊目のメモ帳」で、「そのメモ帳の何ページ目」か指定します。(このように指定すると特定のメモ帳グループの特定のページ指定ができますよね)
  • 「何冊目のメモ帳」か、「何ページ目」かの指定は、いずれも「0」から始まります。

ところで、このようなメモ帳グループをなぜ「2次元」と呼ぶのかピンときませんよね。

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

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

一方で、今まで出てきた普通の配列、つまりメモ帳は「何ページ目」という1つの情報で指定するので「1次元配列」と呼ぶこともあります。

ところで、、、「3次元配列」とか「4次元配列」、さらには「100次元配列」とかあるんでしょうか。実はこれらの配列も扱うことができます。(実際のスケッチではあまり見かけませんが…)

それでは、メモ帳グループ(2次元配列)の準備の仕方や、使い方について確認していきましょう!

2次元配列の宣言

最初に、2次元配列の用意の仕方、つまり宣言方法について確認します。

この説明はちょっと分かりづらいかもしれませんので、具体例で説明します。

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

uint16_t memo[2][3];

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

memo[2][3]

これで2次元配列が準備できました。

2次元配列の使い方

次に2次元配列の使い方を確認します。

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

memo[1][2] = 123;

メモ用紙、つまり配列の要素の指定ができれば1次元配列と同じと考えていただいて問題ありません。


それでは、この2次元配列を使用してメロディーデータをもう少し分かりやすくしてみます。

今回は、次のようにメロディーデータを2次元配列で用意してみます。

メモ帳グループのタイトルは「melody」にします。

メモ帳グループ内のそれぞれのメモ帳は、0冊目がメロディーの1番目の音、1冊目が2番目の音、というように、13個の音のデータをそれぞれのメモ帳として用意します。

それぞれのメモ帳は、0ページ目が音程のデータ、1ページ目が音の長さのデータにします。

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

つまり、melody配列の2つの添字は次のような意味を持つことになります。

このように2次元配列を利用すると、音程と長さのデータを別々の配列で用意するより、より密接な関係として表現できる、というメリットがあります。


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

2次元配列の初期化

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

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

こごて、具体的な初期化の例を確認しておきましょう。

例えば、2次元配列uint16_t memo[3][2]を用意したとします。

uint16_t memo[3][2];

このメモ帳グループは、メモ帳が3冊、それぞれのメモ帳は2ページです。

この2次元配列を次のような値で初期化して宣言したい場合を考えます。

このように2次元配列を初期化して宣言する場合は、次のようにスケッチに書きます。

初期化するデータは全体を{}で囲み、その中にそれぞれのメモ帳を{}で囲む、というようなイメージです。

なお、内側の{と}は次のように省略することができますが、このシリーズでは省略しないスタイルでスケッチを作成することにします。

uint16_t memo[3][2] = {2025, 7, 2026, 8, 2027, 9};

ところで、1次元配列を宣言するとき、要素数は以下にように省略することができましたよね。

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

2次元以上の配列も要素数は省略できますが、省略できるのは一番左側の要素数だけです。

2次元配列の場合は、次のようにメモ帳グループの数を省略することができます。メモ帳のページ数は省略できません。

uint16_t memo[][2] = { {2025, 7}, {2026, 8}, {2027, 9} };

2次元配列の宣言、初期化の方法が分かりましたので、キッチンタイマーのスケッチでメロディーデータを2次元配列に変更してみましょう!

2次元配列を使ったメロディーデータ

メロディーデータは音が13個、それぞれの音は音程データと音の長さデータがあります。

再度、用意するメロディーデータを確認します。

このようなデータを2次元配列をする場合、スケッチに1行で書いてしまうと1行がすごく長くなってしまいます。

そこで、スケッチを読みやすくするために1つの音データを1行で書いて、次のように初期化して宣言するようにしてみました。

uint16_t 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}};

このように2次元配列を使用すると、音程データとその音の長さデータの組み合わせがわかりやすくなりますよね。

この2次元配列でメロディーを演奏する場合、最初音の音程のデータはmelody[0][0]、音の長さデータはmelody[0][1]というようになります。

スケッチでは次のようにtone関数で音程を、delay関数で長さを指定すれば良いことになります。

for(uint8_t i=0; i<MELODY_LENGTH; i++ ) {
  tone(SPEAKER, melody[i][0]);
  delay(melody[i][1]);
}

スケッチ

それでは最後にスケッチとしてまとめてみます。

今回変更した部分はメロディーデータの配列宣言部分と、メロディー演奏部分です。

/*
  キッチンタイマー
  
  内容: スイッチ、LED、スピーカーを使ったキッチンタイマー
  変更履歴:
    2024.11.25: 新規作成
    2024.12.01: スイッチが押されたらLED点滅開始
    2024.12.02: スイッチ関連の#define追加
    2024.12.05: 点滅回数カウント追加
    2024.12.06: 繰り返し処理をforに変更
    2024.12.13: アラーム音追加・動作開始時とタイマー時間の時に青色LEDを点灯
    2024.12.15: 残り時間のLED制御を追加
    2024.12.17: 残り時間が少なくなったら音も鳴らす制御を追加
    2024.12.17: アラーム音をメロディーに変更
    2024.12.22: 周波数定義部分をファイル化
    2024.12.23: メロディーデータを配列化
    2024.12.26: 残り時間LED制御の関数化
    2024.12.27: 残り時間LED制御をswitchに変更
    2025.1.6:   残り時間の引数を列挙体に変更
    2025.1.7:   メロディーデータを2次元配列に変更
*/

// 各音程の周波数定義読み込み
#include "onkai.h"

// 秒を表現する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 A5 // スイッチを接続している端子名
#define SWITCH_OFF 1 // スイッチOFFの時のdigitalReadの値
#define SWITCH_ON  0 // スイッチONの時のdigitalReadの値

// タイマー時間設定(LEDの点滅回数)
#define TIMER_JIKAN 30

// 残り時間を表現するLEDの制御時間
#define KIIRO_JIKAN 10  // 残り時間3分の2
#define AKA_JIKAN   20  // 残り時間3分の1

// アラーム音関連
#define SPEAKER A0  // スピーカーの端子番号
#define ALARM   880 // アラーム音の音程

#define MELODY_LENGTH 13 // アラームメロディーの音の数
uint16_t 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 Nokori {JYUUBUN, HANBUN, MOUSUGU};

// 残り時間表示用LEDの制御関数
//   引数: HIGHまたはLOW
//   返り値: なし
void controlTimeLed(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() {
  // 端子の設定
  // スピーカー接続端子はtoneの指示で自動的に出力設定になるので設定していません
  pinMode(BYOU_LED,   OUTPUT); // 秒表現のLED接続端子の出力設定
  pinMode(SWITCH,     INPUT_PULLUP); // スイッチ接続端子をプルアップ設定
  pinMode(LED_MIDORI, OUTPUT); // 緑色LED接続端子設定
  pinMode(LED_KIIRO,  OUTPUT); // 緑色LED接続端子設定
  pinMode(LED_AKA,    OUTPUT); // 緑色LED接続端子設定

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

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



  // タイマー開始時に残り時間十分の表示にする
  controlTimeLed(JYUUBUN);
}

void loop() {

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

    // 残り時間が3分の2のとき、緑色をOFF、黄色をONにする
    if( count == KIIRO_JIKAN ) {
      controlTimeLed(HANBUN);
    }

    // 残り時間が3分の1のとき、黄色をOFF、赤色をONにする
    if( count == AKA_JIKAN ){
      controlTimeLed(MOUSUGU);
    }

    // 残り時間が5秒以下かそうでないかで処理を変える
    if( (TIMER_JIKAN-count) <= 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(uint8_t count=0; count<3; count++) {
  
    // 1回分のメロディー演奏
    for(uint8_t i=0; i<MELODY_LENGTH; i++ ) {
      tone(SPEAKER, melody[i][0]);
      delay(melody[i][1]);
    }
    // 音を消す
    noTone(SPEAKER);

    // メロディーを区切るために少し時間待ちをする
    delay(500);
  }

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

}

更新履歴

日付内容
2019.12.1新規投稿
2021.8.27新サイトデザイン対応
2025.1.7説明補足
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
大学生
大学生
5 年 前

少し失礼します。少し話が違うのですが、今使っている素子のデータシートが回路図のものとピン配置の説明の図でピン配置自体が違うのですが、この場合どのような対策を行えばいいかわかりますか?なお、ネットでも情報の少ない素子でありあまりそちらには期待できません。

管理者
管理者
返信  大学生
5 年 前

回答が遅くなり申し訳ございませんでした(コメントがあったことの通知がきていませんでした…)

ご質問内容について確認させてください。「今使っている素子」は具体的には何になりますでしょうか。例えばArduino Microで使用されているAtmega32u4などでしょうか。

使用されている素子の情報がわかればお答えできるかもしれません。

目次