第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つの音を追加するためには、tone()命令を追加してさらにdelay()命令を追加する必要があります。また、音程を変更したい場合は、変更する場所を探す手間もかかります。

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

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

配列

C/C++言語には、データの集まりをまとめて扱う方法として「配列」という仕組みが用意されています。配列は変数を拡張したようなものです。

配列の説明に入る前に、変数の復習をしておきましょう。例えば「uint8_t hensu;」と宣言すると、以下のような0〜255までの数字を書いておける「メモ用紙」のようなものが用意されるんでしたよね。

uint8_t型変数

「配列」とは変数の集まりで、イメージとしては「メモ帳」のようなものです。例えば「hairetsu」という名前のuint8_t型の配列は以下のようなものです。

uint8_t型配列

この「メモ帳」、つまり配列には以下の特徴があります。

  • メモ用紙(変数)の時と同様に、メモ帳(配列)にもタイトルがついています
  • メモ用紙(変数)の時と同様に、メモ帳(配列)にもサイズの種類(uint8_tなどの型)があります
  • メモ用紙(変数)の時はメモ用紙1枚でしたが、メモ帳(配列)は複数枚用意することができます
  • メモ帳(配列)にはページ番号がついています。ただしページ番号は0から始まります
  • メモ帳(配列)に数字を書いたり読む場合は「タイトルが〇〇というメモ帳の□□ベージ」などと指定します

メモ帳、つまり「配列」のイメージはなんとなくつかめたでしょうか。この段階では「なんとなく」で構いません。配列(メモ帳)は変数(メモ用紙)の集まりでページ番号が付いている、という程度の理解で大丈夫です。

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

配列の宣言

変数を使う場合は変数の宣言をしましたが、配列を使う場合も同様に宣言します。C/C++言語では配列を以下のように宣言します。

配列の宣言

「このように宣言します」なんて説明されてもさっぱりわかりませんので、これから具体的にどのように宣言するのか、変数の時と比較しながら一緒にみていきましょう。

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

uint16_t  oto;

このように宣言すると、以下のような「メモ用紙」がArduino Microの中に用意されます。用意された段階では適当な数字が書かれています。(メモ用紙は白紙の状態はないんでしたよね)

uint16_t型変数

次に、uint16_t型の数字を扱うことのできる「onkai」(音階)という名前の配列を宣言する場合、配列の個数を5個で宣言する場合は以下のようスケッチに書きます。

uint16_t  onkai[5];

このように宣言すると、以下のような「メモ帳」がArduino Microの中に用意されます。変数と同様に、用意された段階では全てのページに適当な数字が書かれています。メモ帳も結局はメモ用紙の集まりですので、白紙のページはありません。

uint16_t型配列

先ほどのメモ帳の特徴のところでも説明しましたが、ページ番号は「0」から始まることに注意してください。ページ番号が0から始まる5ページのメモ帳は、最後のページ番号はいくつになるかわかりますか?

答えは「4」です。メモ帳(配列)のページ番号の最終ページは、ページ数から1を引いた数になることに注意してください。日常生活では1から数えることに慣れていますが、プログラミングの世界では1から数える、ということは少なくなります。0から数えたり、場合によっては10から数えたりすることもありますので、数の数え方に慣れておきましょう。

また、メモ帳の各ページのことを「配列の要素」と呼んでいます。先ほど宣言した「uint16_t onkai[5];」は要素数が5の配列です。

なんだかいろいろと用語があって覚えるのがちょっと大変になってきましたね。すぐに忘れてしまうかもしれませんので、用意したノートの配列に関連する用語をまとめてみてください。このあとも新しい用語が出てきますので、整理しながら読み進めてみてください。

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

配列の扱い方

配列に数字を代入するには、変数と同様に「=」記号を使用します。例えば配列の最初の要素に880を代入する場合は、スケッチには以下のように書きます。

onkai[0] = 880;

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

また、この配列要素を使用して、880Hzで音を鳴らしたい場合は、スケッチの以下のように書きます。

tone(SPEAKER, onkai[0]);

配列の書き方はちょっとわかりづらいところがありますが、わかりづらかったらメモ帳のイメージを思い出してください。「onkai[0] = 880;」は、配列「onkai」の0ページ目に880という数字を書いています。また「tone(SPEAKER, onkai[0]);」は、tone関数の周波数パラメータを配列「onkai」の0ページ目の数字を使用しています。

それでは、今のスケッチにあるメロディー部分の音階のデータを配列にまとめてみましょう。メロディーは「ドミソミドミソミドミソシド」ですので、音のデータは13個あります。この音のデータを入れる配列の要素数は13個ですよね。配列名を「onkai」とした場合、配列は以下のように宣言します。

uint16_t  onkai[13];

このように配列を宣言すると、各要素に0〜65535の数字を代入できる13個の配列が用意されます。具体的には、onkai[0]、onkai[1] … onkai[12]の添字が0から12までの13個の配列が用意されます。

この配列にメロディーのデータを代入する場合、今までの知識を使うと以下のようになりますが、、、

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

ちょっと大変ですよね。何度も「onkai」と入力する必要がありますし、行数も配列の要素数分あるのでスケッチも長くなってしまいます。

そこで配列を宣言するときに、配列に入れるデータも一緒に指定する方法があります。

配列の初期化

配列を宣言するときに、要素をカンマで区切って { } で囲んで = で代入することができます。このように配列を宣言するときに要素も代入することを「配列の初期化」と呼んでいます。

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

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

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

uint16_t  onkai[5];  // 要素数5の配列を宣言して、
onkai[5] = {440, 880, 440, 880, 440};  // 怒られる。このようにあとから代入することはできない

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

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

Arduino IDEは、スケッチをArduinoボードに送るとき、プリプロセッサで配列初期化のときに要素数を数えて以下のように解釈してスケッチをArduinoボード用に変換しています。

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

このように宣言した配列を使用してメロディーを演奏したいのですが、スケッチはどのように書けばいいでしょうか。

例えば最初の音を演奏する場合は以下のように、

tone(SPEAKER, onkai[0]);

とすればOKです。このような書き方でメロディーを演奏するスケッチを書くと、、、

//0番目の音
tone(SPEAKER, onkai[0]);
delay(200);
//1番目の音
tone(SPEAKER, onkai[1]);
delay(200);
//2番目の音
tone(SPEAKER, onkai[2]);
delay(200);
//3番目の音
tone(SPEAKER, onkai[3]);
delay(200);
//4番目の音
tone(SPEAKER, onkai[4]);
delay(200);
//5番目の音
tone(SPEAKER, onkai[5]);
delay(200);
//6番目の音
tone(SPEAKER, onkai[6]);
delay(200);
//7番目の音
tone(SPEAKER, onkai[7]);
delay(200);
//8番目の音
tone(SPEAKER, onkai[8]);
delay(200);
//9番目の音
tone(SPEAKER, onkai[9]);
delay(200);
//10番目の音
tone(SPEAKER, onkai[10]);
delay(200);
//11番目の音
tone(SPEAKER, onkai[11]);
delay(200);
//12番目の音
tone(SPEAKER, onkai[12]);
delay(200);

となります。

でも、、、なんだか同じような命令がズラーッと並んでいて見づらいですよね。見づらい以前にそもそも入力するのが大変です。

このスケッチをよく見てみると、コメントを除くと以下の命令の繰り返しになっています。

tone(SPEAKER, onkai[添字]);
delay(200);

この2行のセットを13回繰り返していますが、正確には上の2つの命令を、配列の添字0から始めて1ずつ増やしながら13回繰り返していますよね。最初の音はtone命令で使用している配列の添字は0 (上の赤太文字部分)、次の音は配列の添字が1、、、というように配列の添字が12になるまで繰り返しています。

今まで習得したC/C++文法で、このように「○○から始めて□□まで△△になるまで繰り返す」という命令がありましたよね。

そうです、for文です。例えば、0から12まで、13回繰り返す場合は、

uint8_t  count;
for( count=0; count<13; count++ ) {
  命令;
}

と書きました。このfor文ではcountが0から12まで変化しながら13回命令を実行します。

このようにfor文を使うと、メロディーの演奏のプログラムは以下のように書くことができます。添字を変数「oto_bangou」(音の番号の意味)を使ってfor文で0から12まで繰り返しています。

uint8_t  oto_bangou;
for( oto_bangou=0; oto_bangou<13; oto_bangou++) {
  tone(SPEAKER, onkai[oto_bangou]);
  delay(200);
}

これでメロディーを演奏する部分ができました。スケッチをまとめてみましょう。なお、ヘッダファイル(onkai.h)は省略します。

以下のスケッチでは、アラームのメロディーを「alarm_melody」という配列で定義しています。またこの配列の要素数を#defineで定義しています。また、メロディーは3回演奏しますので、for文が二重になっています。

スケッチがだんだん複雑になってきました。変数や配列、for文やwhile文など、ひとつひとつの項目はそれほど難しくありませんが、このように組み合わせるとだんだん難しくなってきますよね。

プログラミングの難しさは、このように命令をどのように組み合わせれば、自分が思った動作になるかを考えるところにあります。すぐにわからないようでしたら、以下のスケッチをじっくり読み解いて、完全に理解するようにしてください。また、チャレンジ課題もありますので、いろいろな動作のスケッチを自分で考えて、プログラミングに慣れていくようにしてください。

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

// 音階定義ファイルのインクルード
#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};  // アラームメロディー

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を点灯
  digitalWrite(LED_MIDORI, HIGH);

}


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 ) {
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
    }

    if( count == AKA_JIKAN ){
      digitalWrite(LED_KIIRO, LOW);
      digitalWrite(LED_AKA, HIGH);
    }

    // 残り時間に応じて処理を変える
    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( count=0; count<3; count++) {

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

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

  }

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

}

更新履歴

日付 内容
2019.11.11 新規投稿
2021.8.27 新サイトデザイン対応
2022.2.17 誤字訂正

通知の設定
通知タイミング
guest
0 コメント
本文中にフィードバック
全てのコメントを見る
目次