第44回 構造体

今回はメロディーデータの形式を変更します。

目次

メロディーデータの問題点

前回の記事では2次元配列を使用して、メロディーデータを「音程データ」「音の長さデータ」の組み合わせのようなデータ形式にしました。

このようにすると、スケッチでは音程データとその音の長さデータが組み合わせのように表現できるのでわかりやすく、また変更しやすくなります。

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次元配列にした場合、スケッチを読むときにちょっとだけ気になることがあります。

例えば、最初の音を鳴らす場合は以下のようにスケッチに書くことになります。

tone( melody[0][0] );

また、この音を一定の長さで鳴らすために、次のようにdelay関数を使って時間待ちをします。

delay( melody[0][1] );

スケッチを自分で作った場合はこれで問題ないですが、初めてこのスケッチを見た人は、この2次元配列の部分がちょっとわかづらそうって感じがしませんか?

具体的には、音程データはmelody[0][0]、音の長さデータはmelody[0][1]になっていて、「0」が音階データ、「1」が音の長さデータにしています。

でも、そもそも「0」「1」というのは自分で決めただけなので、他の人が見たらわかりづらいですよね。


そこで、もう少しスケッチが読みやすくなるように例えば、次のように#define定義すればもう少しわかりやすくなりそうです。

#define ONTEI  0 // 2次元配列の音程の添え字
#define LENGtH 1 // 2次元配列の音の長さの添え字

tone( melody[0][ONTEI] );   // 引数は音程データであることがわかりやすい
delay( melody[0][LENGTH] ); // 引数は音の長さデータであることがわかりやすい

このように書くと、tone関数やdelay関数の引数は音程データなのか音の長さデータなのか、数字で書くよりはわかりやすくなります。

でも相変わらず#define定義の部分で「0」と「1」という謎の?数値が出てきますので、この0と1はどういう意味なんだろう?と思ってしまうかもしれません。

C++言語では、このようにデータに密接な関係があるときに、もう少し見通しが良い仕組みが用意されています。

これから、その仕組みを使ってスケッチを変更してみます。

構造体とは?

キッチンタイマーで扱うメロディーデータは、「音程データ」「その音の長さデータ」で、これら2つのデータは密接な関係があります。

このようにデータを一つの「組」として扱うために「構造体」という仕組みが用意されています。


この構造体とは、「複数の変数を組にして、その組に対して名前をつける仕組み」です!

なんて説明されてもよくわかりませんよね。そこで、メロディーデータの例で考えてみます。

メロディー音の一つ一つのデータは、「音程」「音の長さ」のデータが組になっています。

これを変数で扱う場合、例えば次のように宣言したとします。

uint16_t ontei;
uint16_t length;

このように2行続けて書くと、なんとなく密接な関係があるような感じがしなくもないです。

でも次のように、この2行以外にも変数の宣言がたくさんあったら、onteilengthという2つの変数が組になっている、とパッと見ではわかりづらくなってきます。

uint16_t ontei;
uint16_t length;
uint8_t  count;
uint8_t  timer;

そこで、スケッチを見ただけで誰もが「このonteilengthの2つの変数は組になっているんだ!」と理解できるようになるといいですよね。


「構造体」とは、複数の変数を誰が見ても組になっていることがわかるようにする仕組みです。

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

上の図の左側では、変数を単に宣言しているだけですので、組になっているかどうかはスケッチを読んでみて使われ方を確認してみる必要があります。

構造体という仕組み使うと、複数の変数をまとめてグループ名のようなものをつけることができます

上の図の右側はその様子を説明したもので、onteilengthという2つの変数をグループにして、そのグループにnoteという名前をつけています。(「note」は日本語で「音符」の意味です)

このように構造体にすると、2つの変数は「noteontei」「notelenght」と表現できるため、2つの変数が組になっていることがわかりやすい、という仕掛けです。

構造体の宣言と使い方

構造体の宣言

それでは、最初に構造体の宣言方法から確認しましょう。

この説明だけではよくわかりませんので、具体例で確認していきましょう!

先ほどのイメージのように、uint16_t型のonteinagasaという2つの変数をまとめて組にしてNoteという名前を付ける場合を例にします。

この場合、構造体の宣言は以下のように書きます。

struct Note {
  uint16_t ontei;
  uint16_t length;
};

スケッチに構造体がこのように宣言されている場合、次のような意味で理解すればOKです。

構造体で組にまとめる変数は「メンバ」と呼ばれることがあります。グループのメンバ、というイメージでしょうかね。

構造体の使い方

次のように宣言したNoteという構造体はどのよう使うか確認していきましょう。

struct Note {
  uint16_t ontei;
  uint16_t length;
};

この構造体の宣言は、あくまで「このような型の構造体を作ります」というように、変数でいうところの変数型のようなものなんです。(正確な説明ではありませんが、イメージで捉えていただければと思います)

この構造体のデータを使用するときは、変数宣言したときのように宣言する必要があります。

例えば、次のように構造体の型を指定して、構造体のデータを作ることができます。

Note melody;

このようにmelodyという構造体データを宣言すると、メンバとして2つの変数onteiとlengthを持ったmelodyという構造体データを作ることができます。


次に、このように宣言したmelodyの中にあるonkainagasaに値を代入する方法を確認していきます。

構造体のメンバへの値の代入は以下のようにします。

melody.ontei  = 262;
melody.length = 200;

このように構造体のメンバを表現するには、構造体の名前に . (ドット)をつけて、メンバの名前を書きます。

.は「の」というような意味で捉えると良いかな、と思います。

例えばmelody.lengthであれば、melodylenght、というイメージです。

構造体の基本的な使い方は以上です。

構造体の配列

melodyという構造体を宣言して、実際に使えるようにしました。

でも、実際のメロディーは複数の音の集まりです。つまりmelodyという構造体データがメロディーの音の数あります。

このように構造体データを複数扱うには、構造体の配列を使います。

そこで、構造体の配列について確認していきましょう!


構造体の配列は、普通の配列と同じように宣言します。

キッチンタイマーのメロディーデータは音の数が13ですので、この場合は以下のように宣言します。

Note melody[13];

このように宣言すると、melodyという構造を持ったデータが13個用意されます。

次に、この構造体の配列のメンバに値を代入する方法です。

例えば配列の0番目に値を代入する場合、次のように書きます。

melody[0].ontei  = 262;
melody[0].length = 200;

melody構造体データの0番目はmelody[0]と表現されますので、その構造体の(.onteilength、というイメージですね。


最後に、構造体の配列を宣言するときの初期化方法について確認します。

このような構造体にした場合、実は2次元配列のときと同様に初期化できるんです。

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

初期化して宣言するときの配列の添字は、配列の要素数が明確ですので、配列の宣言と同じように省略することができます。

上のように初期化して構造体を宣言した場合、例えば0番目の音程データを使ってtone関数で音を出したい場合、次のように書けばOKです。

tone( melody[0].ontei );

また、0番目の音の長さのデータを使ってdelay関数で時間待ちをしたい場合のスケッチは次のようになります。

delay( melody[0].length );

このように構造体を使うと、関連性の高い変数(データ)をまとめて扱うことができます。

Arduinoのサンプルスケッチや他の人が書いたスケッチでたまに見かけますので、ぜひ理解を深めていただければと思います。

スケッチのメロディーデータを構造体にする

構造体とその配列について一通り説明しましたが、かなりややこしかったですよね。

これまでの知識をもとにスケッチを構造体の配列に変更しました。

これがベストなスケッチというわけではありませんが、参考にしてみてください。

ところで、第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次元配列に変更
    2025.1.9:   メロディーデータを構造体配列に変更
*/

// 各音程の周波数定義読み込み
#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 // アラーム音の音程

// 音データの構造体
struct Note {
  uint16_t ontei;
  uint16_t length;
};

#define MELODY_LENGTH 13 // アラームメロディーの音の数
Note melody[] = {
  {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].ontei);
      delay(melody[i].length);
    }
    // 音を消す
    noTone(SPEAKER);

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

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

}

更新履歴

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