第38回 配列

今回はアラームのメロディーを変更してみます。

目次

メロディーの変更

前々回の記事で、単調なアラーム音をメロディーに変更しましたが、今回はそのメロディーを変更してみます。

現在のメロディーを変更するのは簡単ですよね。

メロディーはスケッチの次の部分で処理しています。

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

  // ド
  tone(SPEAKER, DO_4);
  delay(200);
  // ミ
  tone(SPEAKER, MI_4);
  delay(200);
  // ソ
  tone(SPEAKER, SO_4);
  delay(200);
  // ミ
  tone(SPEAKER, MI_4);
  delay(200);
  // ド
  tone(SPEAKER, DO_4);
  delay(200);
  // ミ
  tone(SPEAKER, MI_4);
  delay(200);
  // ソ
  tone(SPEAKER, SO_4);
  delay(200);
  // ミ
  tone(SPEAKER, MI_4);
  delay(200);
  // ド
  tone(SPEAKER, DO_4);
  delay(200);
  // ミ
  tone(SPEAKER, MI_4);
  delay(200);
  // ソ
  tone(SPEAKER, SO_4);
  delay(200);
  // シ
  tone(SPEAKER, SI_4);
  delay(200);
  // 1オクターブ高いド
  tone(SPEAKER, DO_5);
  delay(200);
  
  // 音を消す
  noTone(SPEAKER);

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

}

ところで、メロディーを変えたいとき、毎回この部分を変更するのはちょっと面倒ですよね。

音程を変更したい場合は、変更する場所を探す手間もかかりますし、1つの音を追加する場合は、上のスケッチの適切な場所にtonedelayを追加する必要があります。

メロディーのデータは、音階のデータ(周波数のデータ)の集まりです。このようにデータの集まりをまとめて扱ういい方法はないのでしょうか。

実は、C++言語に限らず、どのようなブログラミング言語でも、データの集まりをまとめて扱う方法が用意されているんです。

今回はその方法を習得しますが、考え方はどのプログラミング言語でも共通ですので、今回の記事でぜひ新しい知識を習得されてください!

「配列」〜データの集まりをまとめて扱う仕組み〜

C++言語には、データの集まりをまとめて扱う方法として「配列」という仕組みが用意されています

この「配列」という仕組みは、データの集まりをどのようにしてまとめて扱っているのか、最初にイメージを確認してきおましょう!

配列のイメージ

このシリーズ記事では、「変数」は「メモ用紙」のイメージで説明してきました。

「配列」は「変数」を拡張したようなものです。つまり、「メモ用紙」を拡張したようなものなんです。

そこで、再度「変数」の「メモ用紙」イメージを確認から始めて、「配列」がどのようなものなのか、そのイメージを説明していきます。


例えばuint8_t hensuu;と宣言すると、次のような0〜255までの数字を書くことができる「メモ用紙」のようなものが用意されるんでしたよね。

「配列」とは変数を集めたものです。

つまり上の「メモ用紙」を集めたもので、イメージとしては「メモ帳」のようなものです。

「メモ用紙」(変数)と「メモ帳」(配列)の一番の違いは、「メモ帳にはページ番号が振られている」という点です。

例えば、uint8_t型の「hairetsu」という名前のメモ帳(配列)は次のようなものです。

まずは「配列とは変数を集めたもので、それぞれを区別するために番号が振られている」という点を押さえていただければと思います。

この理解をベースに、配列の特徴をみていきます。

配列の特徴

「変数」つまりメモ用紙にはいくつか特徴がありましたが、「配列」(メモ帳)にも似たような特徴があります。変数と違う点については、マーカーで目印をつけています。

  1. 何度でも書き直しができる。
    ただし数字を書く場合は「タイトル」と「ページ番号」を指定する必要がある(変数の場合はタイトルだけでした)
  2. メモ帳にはサイズがいくつかあり、それぞれのサイズで書ける数字の範囲が異なる。
    なお、メモ帳ではサイズを混在することができない。(最初のページはuint8_t型、次のページはuint16_t型などの混在はできず、「uint8_t型のメモ帳」というように全ページ同じサイズになる)
  3. メモ帳には必ず「タイトル」が必要
  4. メモ帳を用意してもらうときは、「タイトルが〇〇、サイズが□□、ページ数が△△のメモ帳と指定する。なお、ページ番号は必ず0から始まる。(普通の感覚では最初は1ページですが、Arduinoのメモ帳は違いますので注意が必要です)
  5. メモ帳を用意してもらうと、各ページに適当な数字が書かれている(全ページ同じ数字ではなく、各ページバラバラな数字)

配列をメモ帳として捉えた場合の特徴ですが、まずはこのイメージで押さえていただければと思います。

Arduino(C++言語)のメモ帳の特徴で特筆する点としては、「サイズ混在ができない」「ページ番号は0から始まる」という点でしょうかね。

一般的なメモ帳では、三つ折りページなどが挿入されていてサイズが混在していたり、ページ番号は1ページから始まりますのでちょっと違和感があるかもしれませんね。

それでは、実際にスケッチで配列を扱う場合はどのようにすればよいか確認していきましょう。

配列の宣言

変数を使いたいときは、Arduinoに変数を用意してもらうために「変数の宣言」をしました。

配列を使う場合も同様に「配列を宣言」すると、Arduinoは配列(メモ帳)を用意してくれます。

スケッチで配列を宣言する場合、次のように書きます。

メモ帳の各ページはメモ用紙ですが、「配列」では「要素」と呼んでいる点に注意してください。

これだですとスケッチでどのように書けばどうなるのかよくわかりませんので、これから具体的にどのように宣言するのか、変数の時と比較しながら一緒にみていきましょう。


例えばuint16_t型、つまり0〜65535までの数字を扱うことのできる「oto」(音)という名前の変数を宣言する場合、次のように宣言しました。

uint16_t oto;

このように宣言すると、次のような「メモ用紙」がArduinoボードの中に用意されます。用意された段階では適当な数字が書かれています。


次に、uint16_t型の数字を扱うことのできる「melody」(メロディー)という名前の要素数が5の配列を宣言する場合、次のようスケッチに書きます。

uint16_t melody[5];

このように宣言すると、次のような「メモ帳」がArduinoボードの中に用意されます。

変数と同様に、用意された段階では全てのページに適当な数字が書かれています。メモ帳も結局はメモ用紙の集まりですので、白紙のページはありません。

また、右上にページ番号が付いていますが、要素数が5の場合、0から4になる点に注意してください。


これで配列(メモ帳)が準備できました。

次に、このように宣言した配列をどのように扱うのか確認しましょう!

配列の扱い方

配列の扱い方はポイントを押さえれば変数と同様に扱うことができます。

スケッチでの具体的な扱い方についてみていきましょう!

代入

変数に数値を代入するには(メモ用紙に数字を書くには)、次のように=記号を使いました。

oto = 269;  // 「ド」の周波数

配列に数字を代入するには、同様に=記号を使います。ただ、配列の場合「何ページ目に」数字を書くのか指定する必要鍵ありますよね。

「何ページ目に」というのは配列名の後に[ページ番号]と書いて指定します。

例えば、uint16_t melody[5];で宣言した配列の「最初の要素」に269を代入する場合、スケッチには次のように書きます。

melody[0] = 269;  // 「ド」の周波数

この例では、最初の要素を [0] と指定しましたが、このように配列の何番目かを指定する[ ]内の数字を「添字」(そえじ)と呼んでいます。

参照

次に、メモ帳に書いてある数字を参照したい場合(読みたい場合)です。

先ほどmelody[0]に「ド」の周波数を代入しましたが、これを使って「ド」の音程を鳴らした場合、スケッチには次のように書きます。

tone(A0, melody[0]);  // 「ド」の音程を出す

「代入」(メモ帳に数字を書く)にしても「参照」(メモ帳の数字読み取る)にしても、変数との大きな違いである「[添え字]」で、メモ帳のどのページに書いたり呼んだりするかを指定する点を押さえていただければ大丈夫です。

メロディーの配列を用意する

それでは、今のスケッチにあるメロディー部分の音階のデータを配列にまとめてみましょう。

メロディーは「ドミソミドミソミドミソシド」ですので、音階のデータは13個あります。

このデータを入れる配列の要素数は13個必要ですので、配列名を「melody」とした場合、次のように宣言します。

uint16_t meoldy[13];  // メロディーデータ

このように配列を宣言すると、各要素に0〜65535の数字を代入できる13個の配列が用意されます。この配列に「ドミソミドミソミドミソシド」を入れておこう、というわけです。

スケッチでは次のように書けば問題ありませんが、ちょっと何とかならないか?という感じですよね。

melody[0]  = DO_4;
melody[1]  = MI_4;
melody[2]  = SO_4;
melody[3]  = MI_4;
melody[4]  = DO_4;
melody[5]  = MI_4;
melody[6]  = SO_4;
melody[7]  = MI_4;
melody[8]  = DO_4;
melody[9]  = MI_4;
melody[10] = SO_4;
melody[11] = SI_4;
melody[12] = DO_5;

そこで配列を宣言するときに、配列の各要素に指定した数値を代入しておくことができます。

これを「配列の初期化」と呼んでいます。

スケッチでは次のように書きます。

この配列の初期化について、注意点がありますで最初に確認しておきしょう。

例えば要素数が3、uint16_t型の「doremi」という名前の配列を宣言するときに、最初の要素から順番に440, 880, 1760という数字を入れておきたい場合は次のように書きます。

uint16_t doremi[3] = {440, 880, 1760};

このように配列の各要素に数字を入れるのは配列の宣言のときだけ、という点に注意してください。

例えば次のように配列を宣言したあとに、{ } で要素を囲んで配列に代入することはできません。

// 配列の宣言
uint16_t doremi[3];

// まとめて代入はできない
doremi[3] = {440, 880, 1760};

それでは、先ほどの13個のメロディーの配列を初期化して宣言しましょう。

uint16_t  melody[13] = {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  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};

これでメロディーのデータが配列で用意できましたので、このmelody配列を使ってスケッチを書き直してみましょう!

配列を使ってメロディーを演奏する

これでメロディーのデータを、配列melodyとして用意することができました。

とは言っても、メロディーを演奏するにはスケッチはどのように書けば良いのでしょうか?

すぐにわかりませんので、最初は基本的なところから考えていきましょう。


例えば最初の音を演奏する場合はスケッチには次のように書けばOKです。

tone(A0, melody[0]);  // メロディーの最初の音を出す
delay(200);  // 200ms時間待ち

この調子でスケッチを書いていけばOKですので、次のようなスケッチではどうでしょうか?

// 配列を使ったメロディー演奏

//0番目の音
tone(A0, melody[0]);
delay(200);

//1番目の音
tone(A0, melody[1]);
delay(200);

//2番目の音
tone(A0, melody[2]);
delay(200);

//3番目の音
tone(A0, melody[3]);
delay(200);

//4番目の音
tone(A0, melody[4]);
delay(200);

//5番目の音
tone(A0, melody[5]);
delay(200);

//6番目の音
tone(A0, melody[6]);
delay(200);

//7番目の音
tone(A0, melody[7]);
delay(200);

//8番目の音
tone(A0, melody[8]);
delay(200);

//9番目の音
tone(A0, melody[9]);
delay(200);

//10番目の音
tone(A0, melody[10]);
delay(200);

//11番目の音
tone(A0, melody[11]);
delay(200);

//12番目の音
tone(A0, melody[12]);
delay(200);

なんだか同じような命令がズラーッと並んでいて、何とかならないのかな?という感じです。

このスケッチをよく見てみると、次の指示の繰り返しになっていますよね。

tone(A0, melody[添字]);
delay(200);

これを13回繰り返していますが、繰り返し方に特徴があります

上の内容を「配列の添字0から始めて、1ずつ増やしながら13回繰り返す」という動作になっていますよね。

C++言語で、このように「0から始めて1ずつ増やしながら13回繰り返す」という処理は、時間計測をするときにforを使って実現しましたよね。

例えば、次のようなスケッチです。

// 変数iを0から始めて1ずつ増やしながら13回繰り返す
for(uint8_t i=0; i<13; i++) {
  指示
}

このようにfor文を使うと、メロディーの演奏のスケッチは次のように書くことができます。

for(uint8_t i=0; i<13; i++) {
  tone(A0, melody[i]);
  delay(200);
}

これでメロディーを演奏する部分ができました。スケッチをまとめてみましょう。

スケッチにまとめる

まとめたスケッチでは、melodyの音の数(配列の要素数)をMELODY_LENGTH(メロディーの長さの意味)という名前で#defineで定義するようにしました。

#define MELODY_LENGTH 13 // アラームメロディーの音の数

スケッチで注意する点としては、メロディーは3回演奏するようにしていますので、forの中にforがある、というちょっとややこしいスケッチになってきました。

変数や配列、forwhileなど、ひとつひとつの項目はそれほど難しくありませんが、組み合わせるとだんだん難しくなってきますよね。

次のようにスケッチを作成してみました。特にメロディー演奏部分の114行目から117行目までが前回のスケッチからの大きな差分です。?と思ったら前回のスケッチのメロディー演奏部分と比べていただければと思います。

スケッチ本体

/*
  キッチンタイマー
  
  内容: スイッチ、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: メロディーデータを配列化
*/

// 各音程の周波数定義読み込み
#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[] = {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};

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 ) {
  }

  // タイマー開始時に緑色LEDを点灯
  digitalWrite(LED_MIDORI, HIGH);
}

void loop() {

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

    // 残り時間が3分の2のとき、緑色をOFF、黄色をONにする
    if( count == KIIRO_JIKAN ) {
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
    }

    // 残り時間が3分の1のとき、黄色をOFF、赤色をONにする
    if( count == AKA_JIKAN ){
      digitalWrite(LED_KIIRO, LOW);
      digitalWrite(LED_AKA, HIGH);
    }

    // 残り時間が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]);
      delay(200);
    }   
    // 音を消す
    noTone(SPEAKER);

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

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

}

各音程と周波数定義ファイル

// メロディー演奏用の音の周波数
#define DO_3   131
#define DOS_3  139
#define RE_3   147
#define RES_3  156
#define MI_3   165
#define FA_3   175
#define FAS_3  185
#define SO_3   196
#define SOS_3  208
#define RA_3   220
#define RAS_3  233
#define SI_3   247

#define DO_4   262
#define DOS_4  277
#define RE_4   294
#define RES_4  311
#define MI_4   330
#define FA_4   349
#define FAS_4  370
#define SO_4   392
#define SOS_4  415
#define RA_4   440
#define RAS_4  466
#define SI_4   494

#define DO_5   523
#define DOS_5  554
#define RE_5   587
#define RES_5  622
#define MI_5   659
#define FA_5   698
#define FAS_5  740
#define SO_5   784
#define SOS_5  831
#define RA_5   880
#define RAS_5  932
#define SI_5   988

更新履歴

日付内容
2019.11.11新規投稿
2021.8.27新サイトデザイン対応
2022.2.17誤字訂正
2024.12.23説明内容一部変更
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
目次