桜先輩ボタン作った

全体的にひどい文章です。「は?」と言いたくなる題ですが、落ち着いてください。桜先輩にランダムに喋っていただくデバイスArduinoとDFPlayer Miniで作ったというだけです(はぇ?)。

桜先輩とは

そこからかよ…面倒な人間だなコイツ。
桜先輩こと桜井美景はきらら系『恋する小惑星』(小惑星はアステロイドと読む)の登場人物です。いわゆるツンデレ成分あり。ちょっと不器用で悪い意味で現実的な娘でしたが、ほぐれましたねぇ。そういう成長は大好きです。アニメ放送当時に高校生だったこともあり、進路、将来の話は共感の嵐!大変美味でした。

で、なんじゃそりゃ

ボタンが押されると全mp3ファイルの中から1つをたぶん同様に確からしく(ランダムに)再生します。機能はこれだけです。


こういうものです。ここで作るものは台詞のmp3ファイルさえ用意できれば他のキャラクターにもそのまま流用可能です。だからといってしまりんverよこせ!とか怒らないでください。自分でやってね。
着想自体はこちらの記事から得ました。素敵なボタンです。
nixeneko.hatenablog.com

DFPlayer Mini とは

Arduinoでmp3ファイルを再生するのに便利なモジュールです。秋月電子でも取り扱いがあります。
akizukidenshi.com
ただAmazonでもっと安く売っているので、慣れている人はそちらで買えばいいと思います*1
DFRobot製品だけあって、Arduino用ライブラリが用意されています。ここではこのライブラリを使います。
github.com
その他、練習にはこちらのブログ記事が参考になります。ただライブラリのスケッチ例にFullFunctionというのがあり、それをじっくり見るのがライブラリの機能を活用する近道です。
blog.hrendoh.com

部品

部品は以下の表にまとめておきます。

部品名 個数
ブレッドボード 1
Arduino Nano 1
DFPlayerMini 1
スピーカー 1
タクトスイッチ 1
1kΩ抵抗 1
470Ω抵抗 1
配線 たくさん

スピーカーは秋月のこちらが手軽に扱えて良いです。今回はこれを使います。
akizukidenshi.com
基板に実装するならこれもあり。使ったことがありますが何も考えなくてもちゃんと動きました。
akizukidenshi.com
とりあえずダイナミックスピーカーと書いてあって、DFPlayerのアンプ出力3Wに近ければ何でもいいはずです。

配線

以下の図のようにつなぎます。ここはある程度自由にやってもらって構いません。ここの配線を変えたらスケッチ(プログラム)も修正してください。とはいえ注意すべき制限があるので、それを説明します。

配線

DFPlayerMiniとArduino間の配線

ここはシリアル通信関連に注意が必要です。シリアル通信は送信ピン(TX)と受信ピン(RX)を繋ぐので、下の表のようにつなぎます。

Arduino TX ↔ DFPlayerMini RX
Arduino RX ↔ DFPlayerMini TX

しかもArduinoとDFPlayerMiniの間の通信は(ここで紹介するプログラムでは)Software Serialですから、Arduino RX, TXは0ピンや1ピンではなく、自分で指定したピン(4,5番ピン)になります。ここで出すプログラムではArduinoの純粋なシリアル通信用の0ピン1ピンはパソコンとのシリアル通信で使われているので、他のことには使えません。何かを繋ぐことはできません。
もう一つシリアル通信で注意する点があります。それはDFPlayer Miniの電圧が3.3Vだという点です。Arduino Nanoの電圧は5Vなので当然シリアル通信の信号は5Vです。しかしDFPlayerは3.3Vで設計されていますから、①を直に繋いでいいのかは怪しいです。*2。ということで①の間に分圧回路を挟みます。こちらが参考ページ。高校物理をやった方なら原理はわかると思います。
www.kairo-nyumon.com
我が家には470Ωと1kΩが転がっていたので、これでだいたい5Vを1:2に内分しました。

DFPlayerの電源

3.3VだとArduinoの給電能力を超えたのか音が鳴りませんでした*3。USB給電で動かすときはArduinoの5Vピンから電源を取るとよいと思います。

ランダム関数

Arduinoでランダムに整数値を出す関数も本当はランダムに出しているのではなくて、予め準備された数を出しているに過ぎないのです(この理解でいいのかは自信ない)。予め準備された数のセットの番号はシード値といって、こいつを変えない限り毎回同じ順番で数が出てくることになります。従ってこいつを何も繋いでいないアナログ入力から読み取った値にすればランダムになるというわけ。ここではアナログ入力ピンにA0を使っているので、A0には何も繋いではいけません。
参考:Arduino 日本語リファレンス

SDカードの準備

microSDに各話の桜先輩の台詞を切り出したmp3ファイルを話別フォルダーに入れておきます*4

microSDカード内のフォルダ
各フォルダの中のmp3ファイル

フォルダー名は01, 02, ..., 12のようにしないと認識してくれませんでした。ファイル名は001.mp3, 002.mp3,...のようにします。

スケッチ(プログラム)

上でも書きましたが、ピンのつなぎ方を変えたらスケッチもピン番号を書き換えてください。ピンを変えたり音量を変えたりする部分は上部の//constants----にまとめてあります。
ちなみに私のArduino Nanoは3年以上前の古い互換品なので、最新のArduino IDEではoldBootloaderを選ばなければなりませんでした。oldとは何だ失礼な。このせいで30分くらい溶かしたのだ。

#include <SoftwareSerial.h>
#include <Arduino.h>
#include <DFRobotDFPlayerMini.h>
//https://github.com/DFRobot/DFRobotDFPlayerMini
//Play Sakurai Mikage line at random
//refer to https://jumbleat.com/2016/08/19/switch_without_chatter/ (chattering)


//------ constants --------------
const int PlayerRX = 5;  //Arduino側RXピン DFPlayerのTXと結線
const int PlayerTX = 4;  //Arduino側TXピン DFPlayerのRXと結線
int volume = 20;           //音量設定(1~30)

const int CharaLines[] = {32, 56, 10, 49, 53, 42, 17};  //各話の台詞数(まだ7話までしか追えていない)
const int StoryNumber = sizeof(CharaLines) / sizeof(int);
int AllLines = 0;

const int Button = 2;
const int PushThreshould = 1000;
const int BusyPin = 7;

//--------------------------------


SoftwareSerial PlayerSerial(PlayerRX, PlayerTX);
DFRobotDFPlayerMini Player;

void setup() {
  //全台詞数をカウント
  for (int i = 0; i < StoryNumber; i++) {
    AllLines += CharaLines[i];
  }

  pinMode(Button, INPUT_PULLUP);
  pinMode(BusyPin, INPUT_PULLUP);
  PlayerSerial.begin(9600);
  Serial.begin(115200);

  Serial.println();
  Serial.println(AllLines);
  Serial.println(StoryNumber);
  Serial.println(F("Initializing DFPlayer ... (May take 3~5 seconds)"));

  if (!Player.begin(PlayerSerial)) {  //Use softwareSerial to communicate with mp3.
    Serial.println(F("Unable to begin:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while (true) {
      delay(200);
    };  //loop, Restart to break this loop.
  }
  Serial.println(F("DFPlayer Mini online."));

  Player.setTimeOut(500); //Set serial communictaion time out 500ms
  //----Set volume----
  Player.volume(volume);
  //----Set different EQ----
  Player.EQ(DFPLAYER_EQ_NORMAL);

  randomSeed(analogRead(0));
}

void loop() {
  unsigned long gauge = 0;   //チャタリング防止
  while (true) {
    if (!(digitalRead(Button))) {
      gauge++;
    }
    if (gauge > PushThreshould) {
      break;
    }
  }

  int LineNumber = random(1, AllLines + 1); //無作為に台詞を一つ選ぶ
  Serial.println(LineNumber);
  int Folder = 1;
  int NumInFolder = 0;
  //どのフォルダのどのファイルかを求める
  for (int i = 0; i < StoryNumber; i++) {
    if (LineNumber <= CharaLines[i]) {
      NumInFolder = LineNumber;
      break;
    } else {
      LineNumber -= CharaLines[i];
      Folder++;
    }
  }
  //play specific mp3 in SD:/Folder/NumInFolder.mp3; Folder Name(01~99); File Name(001~255)
  Player.playFolder(Folder, NumInFolder);

  while (true) {
    delay(300);
    if (digitalRead(BusyPin) == HIGH) {
      break; //再生終了まで待つ
    }
  }

  //Print the detail message from DFPlayer to handle different errors and states.
  if (Player.available()) {
    printDetail(Player.readType(), Player.read());
  }
}

void printDetail(uint8_t type, int value) {   //copied from FullFunction Sketch
  switch (type) {
    case TimeOut:
      Serial.println(F("Time Out!"));
      break;
    case WrongStack:
      Serial.println(F("Stack Wrong!"));
      break;
    case DFPlayerCardInserted:
      Serial.println(F("Card Inserted!"));
      break;
    case DFPlayerCardRemoved:
      Serial.println(F("Card Removed!"));
      break;
    case DFPlayerCardOnline:
      Serial.println(F("Card Online!"));
      break;
    case DFPlayerUSBInserted:
      Serial.println("USB Inserted!");
      break;
    case DFPlayerUSBRemoved:
      Serial.println("USB Removed!");
      break;
    case DFPlayerPlayFinished:
      Serial.print(F("Number:"));
      Serial.print(value);
      Serial.println(F(" Play Finished!"));
      break;
    case DFPlayerError:
      Serial.print(F("DFPlayerError:"));
      switch (value) {
        case Busy:
          Serial.println(F("Card not found"));
          break;
        case Sleeping:
          Serial.println(F("Sleeping"));
          break;
        case SerialWrongStack:
          Serial.println(F("Get Wrong Stack"));
          break;
        case CheckSumNotMatch:
          Serial.println(F("Check Sum Not Match"));
          break;
        case FileIndexOut:
          Serial.println(F("File Index Out of Bound"));
          break;
        case FileMismatch:
          Serial.println(F("Cannot Find File"));
          break;
        case Advertise:
          Serial.println(F("In Advertise"));
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }

}

長くて読みにくい。まずいな、これはGitを使い始めなければいけない予感。
基本的にconstantsのところをいじれば動きます。解説しようと思ったけど長くなりすぎるし疲れたからいいや。後で編集します。

チャタリング防止

ここは他人のアイデアを借用したのでしっかり書きます。剽窃にならないようにね。こちらの記事から持ってきました。この用途に合うように少し変更を加えています。
jumbleat.com
チャタリングを回避するためにはコンデンサなどを用いた回路を使うことがあります。それを真似したもので、合計である一定以上の時間スイッチが押された時にON判定を下すというものです。

サムネイル用

*1:しかしこんなこともある模様。市場に出回っているDFPlayerには色々なタイプが存在する | Program Resource

*2:ちなみに②は大丈夫です。5VのArduinoに3.3Vの信号を渡しても大丈夫なようになっています。たしか閾値が2Vくらいになっていたはず。要出典。

*3:3.3Vはノイズが少ないからそっちが良いという情報があり、そうしたかったのですが。

*4:切り出し時の作業を楽にするためにフォルダーに分けただけなので別に連番にしてもよいですが、のちのち台詞を増やしたいとなったときはこちらの方が対応しやすいのでおすすめです。