📢

Arduinoで早押しボタンを作る 【その7 音声を再生する】

2022/10/20に公開

はじめに

目標

  • 正解と誤答の際に音声を再生する
    • リセットボタン→正解ボタン&誤答ボタンに変更する(ボタンを追加)
    • 音声を再生する

材料

ソフト

  • Audacity
  • Vim

効果音

正解、誤答等の音声はOtoLogic様のものを使わせてもらいます
https://otologic.jp/free/se/quiz01.html
利用していることを明記すれば編集・加工までOKというかなり自由度の高いライセンスです
今回はメモリ節約のため再生時間を短くしたり、モノラルにしたりと加工もしたいのでぴったりです
正解02-1を早押しボタンが押された時、正解02-2を正答時の音、誤答02-3を誤答時の音としたいと思います

その他

  • スピーカー
    音声を出力するのに必要です。アクティブのもの(電源が必要でボリュームが付いているもの)がよいと思います。今回のArduinoからの音声の出力では音量調整等ができないので

方法

こちらの方の記事を参考にさせていただきました
https://nn-hokuson.hatenablog.com/entry/2017/09/01/092945
基本的にはこちらの記事通りに進めれば問題なく進められるのですが、
唯一引っかかるポイントとしてはこちらの部分

Audacityで書き出したRAW形式の音声ファイルを、テキストデータに変換します。音声ファイルはバイナリ形式のため、xxdコマンドで出力します。

xxdコマンドはVimというテキストエディターののコマンドなので、それが標準搭載されているLinuxでは特に何の準備もなく動きますが、Windowsのコマンドプロンプトでは動きません[1]
一番簡単なのはVimをインストールして、インストールされたパスとrawファイルを置いたパスを両方絶対パスで指定する方法かと思います
たとえばこんな感じ

"C:\Program Files (x86)\Vim\vim90\xxd" -i "C:\Users\ユーザー名\Desktop\sample.raw"

vim90の90の部分はバージョンなので各自インストール時期によって異なると思います
sample.rawの場所はShiftを押しながらファイルを右クリックして「パスのコピー」を選ぶとタイプミスがなくて便利です

あとはバイナリ表示が可能なテキストエディタがあれば.rawを開いて置換などを駆使してなんとかすることもできます

回路図

スピーカーの接続には以下のような治具のいずれかがあると便利です

ステレオミニ治具
右上は市販品、それ以外は自作です
D3ピンにL,Rをそれ以外をグランドに接続します
それ以外は前回とほとんど同じですが、D0, D1ピンがシステムのRX, TXに使われているのを忘れていて一部のピンを入れ替える必要がありました
プログラムと見比べながら挿し替えてください

プログラム

ソースコード

PWM出力が特定のピンでしか機能しない等の事情があり、ピンの設定をいろいろと入れ替えています。ブレッドボード上もぐっちゃぐちゃです。最終的にはきれいに整理したいのですが一旦はこれで勘弁してください

#include "my_sound.h"

int SOUND_OUT_PIN = 3;
int CORRECT_PIN = 6;
int WRONG_PIN = 7;
int BUTTON_PIN_0 = 2;
int BUTTON_PIN_1 = 5;
int LED_PIN_0 = 4;
int LED_PIN_1 = 8;
int ButtonPins[] = {BUTTON_PIN_0, BUTTON_PIN_1};
int LedPins[] = {LED_PIN_0, LED_PIN_1};
int IOpinCnt = -1;
int PushedButtonIdx = -1;
typedef enum event
{
  evBUTTON_PUSHED = 0,
  evRESET_PUSHED,
  evCORRECT_PUSHED,
  evWRONG_PUSHED,
  evINIT_END,

  evNONE = -1
} Event;
Event ev = evNONE;
typedef enum state
{
  stTURN_ON = 0,
  stSTANDBY,
  stANSWER,

  stNONE = -1
} State;
State st = stNONE;

void setup()
{
  // put your setup code here, to run once:
  // Stateの初期化
  st = stTURN_ON;

  // デバッグ用出力の有効化
  Serial.begin(115200);
  while (!Serial)
  {
  }

  Serial.println("setup start");

  // 何度も使うので、ボタンとLEDの個数を定数に取り出しておく(defineしておいてもいい)
  if (sizeof(ButtonPins) / sizeof(int) != sizeof(LedPins) / sizeof(int))
  {
    Serial.println("pin count error");
    return;
  }
  IOpinCnt = sizeof(ButtonPins) / sizeof(int);

  // LEDのピンを出力に設定する
  for (size_t i = 0; i < IOpinCnt; i++)
  {
    pinMode(LedPins[i], OUTPUT);
  }

  // 必要なピンの割り込みを有効にする
  PCICR |= B00000100;  //[X X X X X PCMSK2 PCMSK1 PCMSK0]
  // PCMSK0 |= B00000001; // PCINT0_vect[X X X X X D10 D9 D8]
  //  PCMSK1 |= B00000000;//PCINT1_vect[X X X X A3 A2 A1 A0]
  PCMSK2 |= B11100100; // PCINT2_vect[D7 D6 D5 D4 D3 D2 D1 D0]

  // 音の再生の初期設定
  initSoundOutput();
  // playSound(sound_buzzer, sound_buzzer_len);

  ev = evINIT_END;
}

void loop()
{
  switch (st)
  {
  case stTURN_ON:
    switch (ev)
    {
    case evINIT_END:
      ev = evNONE;
      Serial.println("setup end");
      st = stSTANDBY;
      break;
    default:
      break;
    }
  case stSTANDBY:
    switch (ev)
    {
    case evBUTTON_PUSHED:
      ev = evNONE;
      // 何番が押されたかチェックする
      for (size_t i = 0; i < IOpinCnt; i++)
      {
        if (digitalRead(ButtonPins[i]) == LOW)
        {
          PushedButtonIdx = i;
          Serial.print(i);
          Serial.println(" was pushed");
          break;
        }
      }

      if (PushedButtonIdx != -1)
      {
        // 対応するLEDを点灯する
        digitalWrite(LedPins[PushedButtonIdx], HIGH);
        // 音声を再生する
        playSound(sound_buzzer, sound_buzzer_len);
        // 解答中状態に遷移する
        st = stANSWER;
      }
      break;
    default:
      break;
    }
    break;
  case stANSWER:
    switch (ev)
    {
    case evBUTTON_PUSHED:
      ev = evNONE;

      // 押されたのがリセットボタンかチェックする
      if (digitalRead(CORRECT_PIN) == LOW)
      {
        PushedButtonIdx = -1;
        ev = evCORRECT_PUSHED;
        break;
      }
      if (digitalRead(WRONG_PIN) == LOW)
      {
        PushedButtonIdx = -1;
        ev = evWRONG_PUSHED;
        break;
      }
      break;
    case evCORRECT_PUSHED:
      Serial.print("stANSWER");
      Serial.println(" evCORRECT_PUSHED");
      ev = evNONE;
      // LEDを消灯する
      clearLeds();
      // 音を再生する
      playSound(sound_correct, sound_correct_len);
      // ボタン待機状態に戻る
      st = stSTANDBY;
      break;
    case evWRONG_PUSHED:
      Serial.print("stANSWER");
      Serial.println(" evWRONG_PUSHED");
      ev = evNONE;
      // LEDを消灯する
      clearLeds();
      // 音を再生する
      playSound(sound_wrong, sound_wrong_len);
      // ボタン待機状態に戻る
      st = stSTANDBY;
      break;

    default:
      break;
    }
    break;
  default:
    break;
  }
}

// PCINT2のいずれかのピンに割り込み(CHANGE)が発生したときに呼ばれる関数
ISR(PCINT2_vect)
{
  ev = evBUTTON_PUSHED;
}

// 音声の再生
// 初期設定
void initSoundOutput()
{
  Serial.println("initSoundOutput start");
  pinMode(SOUND_OUT_PIN, OUTPUT);
  digitalWrite(SOUND_OUT_PIN, LOW);
  TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(CS20);
  Serial.println("initSoundOutput end");
}

// 音声を再生する
void playSound(unsigned char sound[], int len)
{
  Serial.println("play sound");
  for (int i = 0; i < len; i++)
  {
    OCR2B = pgm_read_byte_near(&sound[i]);
    delayMicroseconds(100);
  }
  Serial.println("play sound end");
}

// すべてのLEDを消灯する
void clearLeds()
{
  for (size_t i = 0; i < IOpinCnt; i++)
  {
    digitalWrite(LedPins[i], LOW);
  }
}

音源の配列は長くて邪魔なので別のファイル"my_sound.h"に以下のような感じで分けることにしました

const int sound_correct_len = 6850;
const unsigned char sound_correct[] PROGMEM = {
    0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x7f,
    /* 中略 */
    0x7f, 0x80, 0x80, 0x80, 0x80};
const int sound_buzzer_len = 5083;
const unsigned char sound_buzzer[] PROGMEM = {
    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x80, 0x80,
    /* 中略 */
    0x7f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80};
const int sound_wrong_len = 4877;
const unsigned char sound_wrong[] PROGMEM = {
     0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
    /* 中略 */
    0x7f, 0x7f, 0x7f, 0x7f, 0x7f
};

いずれファイル構成も含めてgithubなどで公開したいと思いますのでお待ちください

結果

https://youtu.be/O_VSWVblTIo
音が小さすぎて少し聞こえづらいですが、ボタンを押したとき、正誤判定時に音声を再生することができました

まとめ

音声の再生を実装することができました
PROGMEM, pgm_read_byte_nearの挙動や、PWMによる音声再生の原理などについては十分に理解しないままできてしまったので、理解を進めていずれまとめたいなーと思っています

今度こそほとんど完成でしょうか?

脚注
  1. 【 xxd 】コマンド――ファイルを16進数でダンプする、ダンプから復元する ↩︎

Discussion