ダブルドライバー両スロット&ロストドライバー対応 ジョーカーメモリをつくる

前回記事の最後で予告していたとおり、ハザードメモリから無線機能を除いて、「ダブルドライバーの両スロットと、ロストドライバーに対応する(=スロットごとに音声・発光の再生タイミングを最適化する)」という機能に絞ったメモリを作ってみました。この機能が最も活きるメモリと言えば、実際に劇中でも利用シーンのあったこれしかないでしょう、ということで、ジョーカーメモリを作ってみました。

特徴

メインボタンの長押し操作で内部モードが変化し、音声と発光のタイミングが最適化されます。今がどのモードに入っているかはパッと見ではわからないため、モード変化時に劇中の台詞を再生することで、どのモードに入っているかをわかりやすくしてみました。

 

 

ジョーカーの正位置であるダブルドライバーのLスロット時は、フィリップがいることが前提なのでこの台詞。

 

 

ロストドライバー時。正確にはこの台詞はT2のジョーカーメモリで変身したときのものですが、仮面ライダージョーカーと言えばやっぱりこの台詞が一番カッコ良いと思いましたので、こうしています。

 

 

そしてイレギュラーな、ダブルドライバーのRスロット時。ディケイドのファイナルフォームライドでのみ発現する姿になりますので、台詞も通りすがりの仮面ライダーにお願いすることにしました。最初は「ファイナルフォームライド ダ・ダ・ダ・ダブル!」にするつもりだったのですが、ここだけシステムボイスにするのも統一感がないかなと思って、こちらの台詞にしました。

ハードウェア解説

ベースはもちろん、DX版のジョーカーメモリです。

ジョーカーメモリはやはり人気があるのか、中古市場でも他のメモリに比べてちょっと高めで取引されていますね。

ボタンと端子部はそれぞれシルバーとゴールドで塗装しています。遊んでいるうちに塗装が剥がれてくる部分ではあるのですが、やはりここの塗装のあるなしで印象がだいぶ変わるので、ここは塗装しておきました。

塗装は例の如くガンダムマーカーエアブラシで行っていますが、今回は構成を変えて、エア缶ではなく充電式コンプレッサーに繋いで使用してみました。というのも、ガンダムマーカーエアブラシ、毎回塗料カップの掃除をしなくて良くて手軽に使えるという強力なメリットがある一方、エア缶があっという間に冷えてしまってすぐにエアーが弱くなるという、逆に強力なデメリットもあります。これはちょっと何とかしたいなあということで、色々調べてみた結果、少なくとも以下の構成でガンダムマーカーエアブラシを充電式コンプレッサーに繋げて使用できることがわかりました(※もちろん、他の選択肢もあります)。

実際に繋げてみた結果がこちらです。

「よし、これで連続稼働時間が延びるぜ!」と喜び勇んで試してみたところ、「一応使えはするが完璧ではない」という感じがしました。特に、ガンダムマーカーの塗料が充分にあるときは問題なさそうですが、少なくなってくると塗料がかなり飛びにくくなりました。また、マーカーのペン先もエアブラシ用のものに変えていたのですが、逆に元々のペン先に戻した方が上手くいったりもしたので、もうちょっと色々試行錯誤しながら使っていく必要がありそうです。

 

ハザードメモリのときとの内部構造の一番の違いは、無線通信を使用する必要がなくなったので、マイコンはATOM Liteでなくても良いというところです。従来通りArduino Pro Mini (3.3V)を使用しても良いのですが、同じぐらい小型でかつ安価で扱いやすいマイコンとしてSeeeduino XIAOというものが最近出てきていますので、こちらを使用してみました。

使用感は良好ですので、今後はこちらをメインに使用していくことになると思います。

あとは、基本的にハザードメモリのときに使用したものと同じ具材を使用します。

回路図としてはこんな感じです。

実際に配線すると、こうなります。

マイコンを変えたことで少し配置の自由度が上がりましたので、全体の配置を見直して、電池蓋の中ではありますが電源スイッチを追加して遊びやすくしました。また、ハザードメモリのときは内部スペース確保のためにギリギリまで内部部品を削っていて、結果、蓋は両面テープ留めの形になっていたのですが、今回の部品配置見直しで蓋のネジ留めも可能になりました。

スイッチ部分の配線の仕方はハザードメモリのときと同じですが、改めて説明しておきます。

まず、基板の右上の音声IC部分は切り取ってしまい、

LEDの足を切ってしまって、基板に残った足とスイッチ部分を配線します。

あとは、裏面をこんな感じに配線します。先に載せた回路図と併せて見てもらうとわかりやすいと思います。

ソフトウェア解説

ソースコード的には、前回のハザードメモリからBLE関係の機能を削除して、ATOM Liteマイコンに特化した記述になっていたところ(←analogWrite()の代わりにledcWrite()を使っていたり、ソフトウェアシリアルでなくハードウェアシリアルを使っているところなど)を、一般的なArduino向けの記述に修正しています。また、一部実装がごちゃついていたところも整理しました。

状態遷移自体は、ハザードメモリのときから変わっていません。ただ、「ボタン長押しでモード変更」という仕様にした結果、「ボタンを押すと『ジョーカー!』が鳴る」という仕様ではなく「ボタンを押してすぐに離すと『ジョーカー!』が鳴る」という仕様になっています。前者の仕様のままでの実装も可能なのですが、それだとモード変更のたびに、切り替え台詞の前に「ジョーカー!」が鳴ってしまうので、個人的にはそれがちょっと嫌で後者の仕様にしました。気にならなければ、前者の方が実装としては少しシンプルになると思います。

あと、そんなに難しいことをしているわけではありませんが、「音声と発光のタイミング変更をどう実装しているか?」については少し解説しておきます。

実装の仕方は多分2つあって、シンプルなのは「各スロットに対応した音声ファイルを別々に用意して、モードごとに再生する音声ファイルを変える」というやり方だと思います。ただ、音声としてはどれも結局「ドライバーが開く音」「ジョーカー!」「変身音」の組み合わせで、それぞれの再生までの時間が違うだけになります。

似たような音声ファイルを複数用意するのが個人的に嫌だったのと、あと、発光も併せて再生タイミングを容易に微調整できるようにした方が後々良かろうと思いましたので、音声ファイルとしては「ドライバーが開く音」「ジョーカー!」「変身音」の3つの音声ファイルを用意して、それぞれを再生するまでの時間をプログラム内で変数管理してモード毎に変更する、というやり方にしました。

ソースコードの全文は記事の最後に載せますが、

#define WAIT_FOR_W_R_NAME_MS 1200 // ドライバー開音再生開始後のメモリ名音までの再生待ち時間(WドライバーRスロット側)
#define WAIT_FOR_W_L_NAME_MS 2000 // ドライバー開音再生開始後のメモリ名音までの再生待ち時間(WドライバーLスロット側)
#define WAIT_FOR_LOST_NAME_MS 1200 // ドライバー開音再生開始後のメモリ名音までの再生待ち時間(ロストドライバー)

#define WAIT_FOR_W_R_CHANGE_MS 2200 // メモリ名音再生開始後の変身音までの再生待ち時間(WドライバーRスロット側)
#define WAIT_FOR_W_L_CHANGE_MS 3000 // メモリ名音再音開始後の変身音までの再生待ち時間(WドライバーLスロット側)
#define WAIT_FOR_LOST_CHANGE_MS 1500 // メモリ名音再音開始後の変身音までの再生待ち時間(ロストドライバー)

この部分の値を変更することで、それぞれの音声が再生されるまでの時間を自由に変更することができるようになっています。

まとめ

以上、『ダブルドライバー両スロット&ロストドライバー対応 ジョーカーメモリ』のご紹介でした。ハザードメモリから部品構成を見直してだいぶ遊びやすくなりましたので、実は個人的にはこちらの方でよく遊んでいます。おそらく、無線通信機能付きのハザードメモリより、こちらの方が需要は高いのではないかなと思っています。

動画を観てくださった方はご存知だと思いますが、実は今回ジョーカーメモリの改造と言いながら、スカルメモリにも同様の改造を行いました。スピーカーを交換するついでに、内部の骨のイメージでスピーカーをシルバー塗装してみました。

スカルメモリについては、『仮面ライダーW』の正統続編である『風都探偵』の第6集で色々詳細が判明しましたが、もしシュラウドの想定したとおり『サイクロンスカル』が誕生していたら、もしスカルメモリが壊れずに残っていたら、という「ifの世界」をちょっと見てみたくて、今回スカルメモリも改造してみました。ジョーカーメモリでやり方はだいぶ確立できたので、こちらはそこまで手間はなくできました。

ということで、前回作ったメモリも合わせると、かれこれ5本ほどガイアメモリを改造したことになります。流石にこれだけやれば個人的にはもう充分、という感じなので、心置きなく次の作品作りに移れそうです。

と言っても、具体的に何を作るか決まっているわけではありません。多分『仮面ライダーセイバー』のワンダーライドブックを色々いじりながら考えることになると思いますが、その前に一つレビューを頼まれていることがあるので、それが終わってから取り組むことになると思います。

 

ソースコード

最後に、今回作ったジョーカーメモリのソースコードの全文を掲載します。

////////// 基本定義 ////////////////////////////////////////////////////////////

#define LOOP_DELAY_MS 20

#define MP3_RX_PIN  0
#define MP3_TX_PIN  1
#define SW_1_PIN    2
#define SW_2_PIN    3
#define SW_3_PIN    4
#define LED_PIN     5

#define ON  LOW
#define OFF HIGH

// 3つのボタン入力の表現
#define N_BUTTON 3
const uint8_t OFF_OFF_OFF[] = {OFF, OFF, OFF};
const uint8_t OFF_OFF_ON[]  = {OFF, OFF,  ON};
const uint8_t OFF_ON_OFF[]  = {OFF,  ON, OFF};
const uint8_t OFF_ON_ON[]   = {OFF,  ON,  ON};
const uint8_t ON_OFF_OFF[]  = {ON,  OFF, OFF};
const uint8_t ON_OFF_ON[]   = {ON,  OFF,  ON};
const uint8_t ON_ON_OFF[]   = {ON,   ON, OFF};
const uint8_t ON_ON_ON[]    = {ON,   ON,  ON};
uint8_t prev_sw[] = {OFF, OFF, OFF};
uint8_t sw[]      = {OFF, OFF, OFF};

// 各モードで変身時の音声再生タイミングが異なる
#define MODE_W_R  0 // ダブルドライバーRスロットモード
#define MODE_W_L  1 // ダブルドライバーLスロットモード
#define MODE_LOST 2 // ロストドライバーモード

#define WAIT_FOR_W_R_NAME_MS  1200 // ドライバー開音再生開始後のメモリ名音までの再生待ち時間(WドライバーRスロット側)
#define WAIT_FOR_W_L_NAME_MS  2000 // ドライバー開音再生開始後のメモリ名音までの再生待ち時間(WドライバーLスロット側)
#define WAIT_FOR_LOST_NAME_MS 1200 // ドライバー開音再生開始後のメモリ名音までの再生待ち時間(ロストドライバー)

#define WAIT_FOR_W_R_CHANGE_MS  2200 // メモリ名音再生開始後の変身音までの再生待ち時間(WドライバーRスロット側)
#define WAIT_FOR_W_L_CHANGE_MS  3000 // メモリ名音再音開始後の変身音までの再生待ち時間(WドライバーLスロット側)
#define WAIT_FOR_LOST_CHANGE_MS 1500 // メモリ名音再音開始後の変身音までの再生待ち時間(ロストドライバー)

#define LONG_PRESS_MS 1500 // モード変更のための長押し時間

#define STATE_INIT           0
#define STATE_TEMP           1
#define STATE_CHANGE_READY   2
#define STATE_CHANGE         3
#define STATE_CRITICAL_READY 4
#define STATE_CRITICAL       5

uint8_t state      = STATE_INIT;
uint8_t prev_state = STATE_INIT;

boolean is_memory_ejected = false; // STATE_INITへの変化経路管理
boolean is_change_off     = false; // STATE_CHANGE_READYへの変化経路管理
boolean is_critical       = false; // STATE_CRITICAL_READYへの変化経路管理

uint8_t mode = MODE_W_L; // デフォルトはダブルドライバーLスロットモードとする
uint8_t prev_mode = mode;
unsigned long long_press_start_time = 0;
boolean is_sw_1_pressing = false;

void print_state(){
  if(prev_state != state){
    switch(state){
    case STATE_INIT:           Serial.println(F("STATE_INIT")); break;
    case STATE_TEMP:           Serial.println(F("STATE_TEMP")); break;
    case STATE_CHANGE_READY:   Serial.println(F("STATE_CHANGE_READY")); break;
    case STATE_CHANGE:         Serial.println(F("STATE_CHANGE")); break;
    case STATE_CRITICAL_READY: Serial.println(F("STATE_CRITICAL_READY")); break;
    case STATE_CRITICAL:       Serial.println(F("STATE_CRITICAL")); break;
    default: ;
    }
  }
}

////////// 音声処理 /////////////////////////////////////////////////////////////

#include <SoftwareSerial.h>

SoftwareSerial ss_mp3_player(MP3_RX_PIN, MP3_TX_PIN);

#define SOUND_VOLUME_DEFAULT 24 // 0〜30

#define SOUND_MEMORY_NAME     1
#define SOUND_CHANGE_READY    2
#define SOUND_MEMORY_EJECT    3
#define SOUND_DRIVER_OPEN     4
#define SOUND_CHANGE          5
#define SOUND_DRIVER_CLOSE    6
#define SOUND_CRITICAL_READY  7
#define SOUND_CRITICAL        8
#define SOUND_SIDE_L          9
#define SOUND_LOST           10
#define SOUND_SIDE_R         11
#define SOUND_CRITICAL_JOKER 12

#define WAIT_SOUND_STATE_NONE   0
#define WAIT_SOUND_STATE_NAME   1
#define WAIT_SOUND_STATE_CHANGE 2

uint8_t wait_sound_state = WAIT_SOUND_STATE_NONE;
unsigned long sound_wait_start_time = 0;

void mp3_play(uint8_t track){
  // この定義だと再生曲数は255に限定されるので注意。
  // Upper-Byte, Lower-Byteをちゃんと処理してやれば65535曲まで対応可能だが、
  // 容量的にそこまで使うことはほぼないと思われる。

  // Macの場合は、MP3データを全て"ZH"フォルダにコピーした後、
  // ターミナルでZHフォルダに入り、"$ rm ._*"のコマンドで、
  // "._01.mp3"のような名前で裏で勝手に生成されているファイル
  // ("$ ls -a"のコマンドでないと見えない、AppleDouble Header file)
  // を削除しないと、挙動がおかしくなるので注意

  uint8_t play[6] = {0xAA,0x07,0x02,0x00,track,track+0xB3};
  ss_mp3_player.write(play,6);
}

void mp3_stop(){
  unsigned char stop[4] = {0xAA,0x04,0x00,0xAE};
  ss_mp3_player.write(stop,4);
}

void mp3_set_volume(uint8_t vol){
  uint8_t volume[5] = {0xAA,0x13,0x01,vol,vol+0xBE};
  ss_mp3_player.write(volume,5);
}

void control_sound(){

  unsigned long now_ms = millis();

  if(prev_state != state){ // 状態遷移直後の処理
    switch(state){
    case STATE_INIT:
      if(prev_state == STATE_TEMP){
        if(prev_mode == mode && !is_memory_ejected){
          mp3_play(SOUND_MEMORY_NAME);
        }
      }else{
        mp3_play(SOUND_MEMORY_EJECT);
      }
      wait_sound_state = WAIT_SOUND_STATE_NONE;
      break;
    case STATE_TEMP:
      if(prev_state != STATE_INIT){
        mp3_play(SOUND_MEMORY_EJECT);
        wait_sound_state = WAIT_SOUND_STATE_NONE;
      }
      break;
    case STATE_CHANGE_READY:
      if(prev_state == STATE_INIT){
        mp3_play(SOUND_CHANGE_READY);
      }else if(prev_state == STATE_CHANGE){
        mp3_play(SOUND_DRIVER_CLOSE);
        wait_sound_state = WAIT_SOUND_STATE_NONE;
      }
      break;
    case STATE_CHANGE:
      // まずはドライバー開音を鳴らして、変身音鳴らしの待ちに入る
      mp3_play(SOUND_DRIVER_OPEN);
      sound_wait_start_time = now_ms;
      wait_sound_state = WAIT_SOUND_STATE_NAME;
      break;
    case STATE_CRITICAL_READY:
      if(prev_state == STATE_INIT){
        mp3_play(SOUND_CRITICAL_READY);
      }
      break;
    case STATE_CRITICAL:
      if(mode == MODE_LOST){
        mp3_play(SOUND_CRITICAL_JOKER);
      }else{
        mp3_play(SOUND_CRITICAL);
      }
      break;
    default:
      ;
    }
  }else{ // 状態遷移後の時間経過処理
    switch(wait_sound_state){
    case WAIT_SOUND_STATE_NONE:
      break;
    case WAIT_SOUND_STATE_NAME:
      if(mode == MODE_W_R){
        // ダブルドライバーRスロットモードになっているとき
        if(now_ms - sound_wait_start_time >= WAIT_FOR_W_R_NAME_MS){
          mp3_play(SOUND_MEMORY_NAME);
          sound_wait_start_time = now_ms;
          wait_sound_state = WAIT_SOUND_STATE_CHANGE;
        }
      }else if(mode == MODE_W_L){
        // ダブルドライバーLスロットモードになっているとき
        if(now_ms - sound_wait_start_time >= WAIT_FOR_W_L_NAME_MS){
          mp3_play(SOUND_MEMORY_NAME);
          sound_wait_start_time = now_ms;
          wait_sound_state = WAIT_SOUND_STATE_CHANGE;
        }
      }else{
        // ロストドライバーモードになっているとき
        if(now_ms - sound_wait_start_time >= WAIT_FOR_LOST_NAME_MS){
          mp3_play(SOUND_MEMORY_NAME);
          sound_wait_start_time = now_ms;
          wait_sound_state = WAIT_SOUND_STATE_CHANGE;
        }
      }
      break;
    case WAIT_SOUND_STATE_CHANGE:
      if(mode == MODE_W_R){
        // ダブルドライバーRスロットモードになっているとき
        if(now_ms - sound_wait_start_time >= WAIT_FOR_W_R_CHANGE_MS){
          mp3_play(SOUND_CHANGE);
          wait_sound_state = WAIT_SOUND_STATE_NONE;
        }
      }else if(mode == MODE_W_L){
        // ダブルドライバーLスロットモードになっているとき
        if(now_ms - sound_wait_start_time >= WAIT_FOR_W_L_CHANGE_MS){
          mp3_play(SOUND_CHANGE);
          wait_sound_state = WAIT_SOUND_STATE_NONE;
        }
      }else{
        // ロストドライバーモードになっているとき
        if(now_ms - sound_wait_start_time >= WAIT_FOR_LOST_CHANGE_MS){
          mp3_play(SOUND_CHANGE);
          wait_sound_state = WAIT_SOUND_STATE_NONE;
        }
      }
      break;
    default:
      ;
    }
  }
}

////////// 発光処理 /////////////////////////////////////////////////////////////

#define LED_BRIGHTNESS 255 // 255が上限値

boolean is_led_active = false; // 電源投入直後のSTATE_INITのときのLED発光を無効にするためのフラグ

unsigned long led_pattern_start_time = 0;
unsigned long prev_blink_time        = 0;
unsigned long inc_dim_start_time = 0;
boolean is_lighting = false;
boolean is_inc = true;

int  prev_interval_ms = 0;
uint8_t prev_steps = 0;

// デフォルトはダブルドライバーLスロットモードとする
unsigned long memory_name_start_time = WAIT_FOR_W_L_NAME_MS;
unsigned long change_start_time = WAIT_FOR_W_L_NAME_MS + WAIT_FOR_W_L_CHANGE_MS;

void led_base_pattern_on(){
  analogWrite(LED_PIN, LED_BRIGHTNESS);
  prev_interval_ms = 0;
  prev_steps = 0;
}

void led_base_pattern_off(){
  analogWrite(LED_PIN, 0);
  prev_interval_ms = 0;
  prev_steps = 0;
}

void led_base_pattern_blink(unsigned long now_ms, int interval_ms){
  if(now_ms - prev_blink_time >= interval_ms){
    if(is_lighting){
      analogWrite(LED_PIN, 0);
    }else{
      analogWrite(LED_PIN, LED_BRIGHTNESS);
    }
    is_lighting = !is_lighting;
    prev_blink_time = now_ms;
  }

  prev_interval_ms = interval_ms;
  prev_steps = 0;
}

void led_base_pattern_inc(unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_time = now_ms;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_time) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t l_step = LED_BRIGHTNESS/steps;
  analogWrite(LED_PIN, l_step*current_step);

  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_base_pattern_dim(unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_time = now_ms;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_time) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t l_step = LED_BRIGHTNESS/steps;
  analogWrite(LED_PIN, l_step*(steps-current_step));

  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_base_pattern_blink_slowly(unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_time = now_ms;
    is_inc = true;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_time) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t l_step = LED_BRIGHTNESS/steps;
  if(is_inc){
    analogWrite(LED_PIN, l_step*current_step);
  }else{
    analogWrite(LED_PIN, l_step*(steps-current_step));
  }
  if(now_ms - inc_dim_start_time >= interval_ms){
    is_inc = !is_inc;
    inc_dim_start_time = now_ms;
  }

  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_pattern_memory_name(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 500){                          led_base_pattern_dim(now_ms, 500, 10);}
  else if(500 < passed_ms && passed_ms <= 1300){ led_base_pattern_dim(now_ms, 800, 20);}
  else{                                          led_base_pattern_off();}
}

void led_pattern_memory_ejected(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 300){                          led_base_pattern_on();}
  else if(300 < passed_ms && passed_ms <= 1000){ led_base_pattern_dim(now_ms, 700, 20);}
  else{                                          led_base_pattern_off();}
}

void led_pattern_change_ready(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 525){                         led_base_pattern_on();}
  else if(525 < passed_ms && passed_ms <= 800){ led_base_pattern_dim(now_ms, 275, 10);}
  else{                                         led_base_pattern_blink_slowly(now_ms, 550, 10);}
}

void led_pattern_change(unsigned long passed_ms, unsigned long now_ms){
  // ドライバー開
  if(passed_ms <= 600){
    led_base_pattern_inc(now_ms, 600, 10);
  } else if(600 < passed_ms && passed_ms <=  1100){
    led_base_pattern_dim(now_ms, 500, 10);}
  else if(1100 < passed_ms && passed_ms <=  memory_name_start_time){
    led_base_pattern_off();
  }
  // メモリ名
  else if(memory_name_start_time < passed_ms && passed_ms <= memory_name_start_time+500){
    led_base_pattern_dim(now_ms, 500, 10);
  }else if(memory_name_start_time+500  < passed_ms && passed_ms <= memory_name_start_time+1300){
    led_base_pattern_dim(now_ms, 800, 20);
  }else if(memory_name_start_time+1300 < passed_ms && passed_ms <= change_start_time){
    led_base_pattern_off();
  }
  // 変身
  else if(change_start_time < passed_ms && passed_ms <=  change_start_time+450){
    led_base_pattern_dim(now_ms, 450, 10);
  }else if(change_start_time+450 < passed_ms && passed_ms <= change_start_time+800){
    led_base_pattern_dim(now_ms, 350, 10);
  }else if(change_start_time+800 < passed_ms && passed_ms <= change_start_time+1050){
    led_base_pattern_dim(now_ms, 250, 10);
  }else if(change_start_time+1050 < passed_ms && passed_ms <= change_start_time+6050){
    led_base_pattern_on();
  }else if(change_start_time+6050 < passed_ms && passed_ms <= change_start_time+7550){
    led_base_pattern_dim(now_ms, 1500, 20);
  }else{
    led_base_pattern_off();
  }
}

void led_pattern_change_off(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 300){                          led_base_pattern_on();}
  else if(300 < passed_ms && passed_ms <= 1000){ led_base_pattern_dim(now_ms, 700, 20);}
  else{                                          led_base_pattern_off();}
}

void led_pattern_critical_ready(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 525){                           led_base_pattern_on();}                 // 装填
  else if( 525 < passed_ms && passed_ms <=  800){ led_base_pattern_dim(now_ms, 275, 10);}
  else if( 800 < passed_ms && passed_ms <= 1200){ led_base_pattern_dim(now_ms, 400, 10);} // ジョーカー
  else if(1200 < passed_ms && passed_ms <= 2000){ led_base_pattern_dim(now_ms, 800, 20);}
  else if(2000 < passed_ms && passed_ms <= 2300){ led_base_pattern_off();}
  else if(2300 < passed_ms && passed_ms <= 2500){ led_base_pattern_on();}                 // マキシマムドライブ
  else if(2500 < passed_ms && passed_ms <= 2900){ led_base_pattern_dim(now_ms, 400, 10);}
  else if(2900 < passed_ms && passed_ms <= 3000){ led_base_pattern_on();}
  else if(3000 < passed_ms && passed_ms <= 3700){ led_base_pattern_dim(now_ms, 700, 20);}
  else if(3700 < passed_ms && passed_ms <= 4000){ led_base_pattern_off();}
  else if(4000 < passed_ms && passed_ms <= 4500){ led_base_pattern_dim(now_ms, 500, 10);} // 待機音
  else if(4500 < passed_ms && passed_ms <= 6000){ led_base_pattern_inc(now_ms, 1500, 20);}
  else{                                           led_base_pattern_blink_slowly(now_ms, 170, 10);}
}

void led_pattern_critical(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 300){                            led_base_pattern_blink(now_ms, 80);}
  else if(  300 < passed_ms && passed_ms <= 1000){ led_base_pattern_dim(now_ms, 700, 20);}
  else if( 1000 < passed_ms && passed_ms <= 1600){ led_base_pattern_inc(now_ms, 600, 10);}
  else if( 1600 < passed_ms && passed_ms <= 1700){ led_base_pattern_off();}
  else if( 1700 < passed_ms && passed_ms <= 2200){ led_base_pattern_blink(now_ms, 60);}
  else if( 2200 < passed_ms && passed_ms <= 3000){ led_base_pattern_dim(now_ms, 800, 20);}
  else if( 3000 < passed_ms && passed_ms <= 3700){ led_base_pattern_blink(now_ms, 60);}
  else if( 3700 < passed_ms && passed_ms <= 5000){ led_base_pattern_dim(now_ms, 1300, 20);}
  else{                                            led_base_pattern_off();}
}

void led_pattern_critical_joker(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 300){                            led_base_pattern_blink(now_ms, 80);}
  else if(  300 < passed_ms && passed_ms <= 1000){ led_base_pattern_dim(now_ms, 700, 20);}
  else if( 1000 < passed_ms && passed_ms <= 2000){ led_base_pattern_dim(now_ms,1000, 20);}
  else if( 2000 < passed_ms && passed_ms <= 2600){ led_base_pattern_inc(now_ms, 600, 10);}
  else if( 2600 < passed_ms && passed_ms <= 2700){ led_base_pattern_off();}
  else if( 2700 < passed_ms && passed_ms <= 3200){ led_base_pattern_blink(now_ms, 60);}
  else if( 3200 < passed_ms && passed_ms <= 4000){ led_base_pattern_dim(now_ms, 800, 20);}
  else if( 4000 < passed_ms && passed_ms <= 4700){ led_base_pattern_blink(now_ms, 60);}
  else if( 4700 < passed_ms && passed_ms <= 6000){ led_base_pattern_dim(now_ms, 1300, 20);}
  else{                                            led_base_pattern_off();}
}

void control_led(){

  unsigned long now_ms = millis();

  if(prev_state != state){
    // 電源ON直後はLEDをOFFにしているので、最初の状態遷移以降にLEDを有効化させるための処理
    is_led_active = true;
    if(state == STATE_INIT){
      if(prev_state == STATE_TEMP){
        if(prev_mode != mode){
          // モード変更した直後は発光処理はさせないようにする
          is_led_active = false;
        }else if(is_memory_ejected){
          // このときは発光パターンをリセットさせない
          ;
        }else{
          led_pattern_start_time = now_ms;
        }
      }else{
        led_pattern_start_time = now_ms;
      }
    }else if(state == STATE_CHANGE){
      // モードごとで発光タイミングを切り替えるための処理
      switch(mode){
      case MODE_LOST:
        memory_name_start_time = WAIT_FOR_LOST_NAME_MS;
        change_start_time = WAIT_FOR_LOST_NAME_MS + WAIT_FOR_LOST_CHANGE_MS;
        break;
      case MODE_W_R:
        memory_name_start_time = WAIT_FOR_W_R_NAME_MS;
        change_start_time = WAIT_FOR_W_R_NAME_MS + WAIT_FOR_W_R_CHANGE_MS;
        break;
      case MODE_W_L:
        memory_name_start_time = WAIT_FOR_W_L_NAME_MS;
        change_start_time = WAIT_FOR_W_L_NAME_MS + WAIT_FOR_W_L_CHANGE_MS;
        break;
      }
      led_pattern_start_time = now_ms;
    }else if(state == STATE_CRITICAL_READY){
      if(!is_critical){
        // is_criticalがtrueのときは、発光パターンをリセットさせない
        led_pattern_start_time = now_ms;
      }
    }else{
      // 上記以外の状態遷移では、発光パターンのリセットのみ実施
      led_pattern_start_time = now_ms;
    }
  }

  unsigned long passed_ms = now_ms - led_pattern_start_time;

  switch(state){
  case STATE_INIT:
    if(is_led_active){
      if(is_memory_ejected){
        led_pattern_memory_ejected(passed_ms, now_ms);
      }else{
        led_pattern_memory_name(passed_ms, now_ms);
      }
    }
    break;
  case STATE_TEMP:
    if(is_memory_ejected){
      led_pattern_memory_ejected(passed_ms, now_ms);
    }
    break;
  case STATE_CHANGE_READY:
    if(is_change_off){
      led_pattern_change_off(passed_ms, now_ms);
    }else{
      led_pattern_change_ready(passed_ms, now_ms);
    }
    break;
  case STATE_CHANGE:
    led_pattern_change(passed_ms, now_ms);
    break;
  case STATE_CRITICAL_READY:
    if(is_critical){
      if(mode == MODE_LOST){
       led_pattern_critical_joker(passed_ms, now_ms);
      }else{
       led_pattern_critical(passed_ms, now_ms);
      }
    }else{
      led_pattern_critical_ready(passed_ms, now_ms);
    }
    break;
  case STATE_CRITICAL:
    if(mode == MODE_LOST){
     led_pattern_critical_joker(passed_ms, now_ms);
    }else{
     led_pattern_critical(passed_ms, now_ms);
    }
    break;
  default:
    ;
  }
}

////////// メイン処理 ////////////////////////////////////////////////////////////

void setup(){
  Serial.begin(115200);
  pinMode(SW_1_PIN, INPUT_PULLUP);
  pinMode(SW_2_PIN, INPUT_PULLUP);
  pinMode(SW_3_PIN, INPUT_PULLUP);
  pinMode(LED_PIN,  OUTPUT);

  // MP3プレイヤーセットアップ
  ss_mp3_player.begin(9600);
  mp3_set_volume(SOUND_VOLUME_DEFAULT);

  led_base_pattern_off();
}

void loop(){

  unsigned long now_ms = millis();

  //////////////////// 状態遷移管理 ////////////////////

  sw[0] = digitalRead(SW_1_PIN);
  sw[1] = digitalRead(SW_2_PIN);
  sw[2] = digitalRead(SW_3_PIN);

  switch(state){
  case STATE_INIT:
    if(memcmp(prev_sw, OFF_OFF_OFF, N_BUTTON) == 0){
      if(memcmp(sw, ON_OFF_OFF, N_BUTTON) == 0){
        state = STATE_TEMP;
        prev_mode = mode;
        is_memory_ejected = false;
        is_sw_1_pressing = true; // モード変更長押し認識開始
        long_press_start_time = now_ms;
      }else if(memcmp(sw, OFF_ON_OFF, N_BUTTON) == 0){
        state = STATE_CHANGE_READY;
        is_change_off = false;
      }else if(memcmp(sw, ON_ON_OFF, N_BUTTON) == 0){
        state = STATE_CHANGE;
      }else if(memcmp(sw, OFF_OFF_ON, N_BUTTON) == 0){
        state = STATE_CRITICAL_READY;
        is_critical = false;
      }else if(memcmp(sw, ON_OFF_ON, N_BUTTON) == 0){
        state = STATE_CRITICAL;
      }
    }
    break;
  case STATE_TEMP:
    if(memcmp(prev_sw, ON_OFF_OFF, N_BUTTON) == 0){
      if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
        state = STATE_INIT;
        is_sw_1_pressing = false; // モード変更長押し認識終了
      }else if(memcmp(sw, ON_ON_OFF, N_BUTTON) == 0){
        state = STATE_CHANGE;
        is_sw_1_pressing = false; // モード変更長押し認識終了
      }else if(memcmp(sw, ON_OFF_ON, N_BUTTON) == 0){
        state = STATE_CRITICAL;
        is_sw_1_pressing = false; // モード変更長押し認識終了
      }
    }
    break;
  case STATE_CHANGE_READY:
    if(memcmp(prev_sw, OFF_ON_OFF, N_BUTTON) == 0){
      if(memcmp(sw, ON_ON_OFF, N_BUTTON) == 0){
        state = STATE_CHANGE;
      }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
        state = STATE_INIT;
        is_memory_ejected = true;
      }
    }
    break;
  case STATE_CHANGE:
    if(memcmp(prev_sw, ON_ON_OFF, N_BUTTON) == 0){
      if(memcmp(sw, OFF_ON_OFF, N_BUTTON) == 0){
        state = STATE_CHANGE_READY;
        is_change_off = true;
      }else if(memcmp(sw, ON_OFF_OFF, N_BUTTON) == 0){
        state = STATE_TEMP;
        is_memory_ejected = true;
      }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
        state = STATE_INIT;
        is_memory_ejected = true;
      }
    }
    break;
  case STATE_CRITICAL_READY:
    if(memcmp(prev_sw, OFF_OFF_ON, N_BUTTON) == 0){
      if(memcmp(sw, ON_OFF_ON, N_BUTTON) == 0){
        state = STATE_CRITICAL;
      }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
        state = STATE_INIT;
        is_memory_ejected = true;
      }
    }
    break;
  case STATE_CRITICAL:
    if(memcmp(prev_sw, ON_OFF_ON, N_BUTTON) == 0){
      if(memcmp(sw, OFF_OFF_ON, N_BUTTON) == 0){
        state = STATE_CRITICAL_READY;
        is_critical = true;
      }else if(memcmp(sw, ON_OFF_OFF, N_BUTTON) == 0){
        state = STATE_TEMP;
        is_memory_ejected = true;
      }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
        state = STATE_INIT;
        is_memory_ejected = true;
      }
    }
    break;
  default:
    ;
  }

  ////////// SW1長押し認識処理 ///////////////
  if(is_sw_1_pressing && now_ms - long_press_start_time >= LONG_PRESS_MS){
    prev_mode = mode;
    switch(mode){
    case MODE_W_L:
      mode = MODE_LOST;
      mp3_play(SOUND_LOST);
      break;
    case MODE_W_R:
      mode = MODE_W_L;
      mp3_play(SOUND_SIDE_L);
      break;
    case MODE_LOST:
      mode = MODE_W_R;
      mp3_play(SOUND_SIDE_R);
      break;
    default:
      ;
    }
    is_sw_1_pressing = false;
  }

  ////////// 音声再生処理 ////////////////////
  control_sound();

  ////////// LED発光処理 ////////////////////
  control_led();

  ////////// デバッグ処理 ////////////////////
  print_state();

  ////////// 処理状態の保持 //////////////////
  prev_sw[0] = sw[0];
  prev_sw[1] = sw[1];
  prev_sw[2] = sw[2];
  prev_state = state;

  delay(LOOP_DELAY_MS);
}