DX変身音叉 音角 ver.2.0をつくる

前回の友情バーストゴースト眼魂に引き続き、今回も過去のライダー玩具改造のご紹介です。今回ご紹介するのは、2005年発売の『DX変身音叉 音角』をベースにディスクアニマル関係の機能を強化した、名付けて『DX変身音叉 音角 ver.2.0』です。

開発経緯

自分にとって不動のNo.1の仮面ライダー作品は『仮面ライダークウガ』なのですが、『仮面ライダー響鬼』もかなり上位に入るぐらい好きです。放映当時はまだ子供用玩具に手を出すのは恥ずかしいと思うようなお年頃だったのですが、それにも関わらずディスクアニマルと音角だけはしっかり集めていたので、相当に好きだったのだと思います。

そしておそらく自分以上に『響鬼』が好きなのが、ディエンドライバーの改造作品でおなじみの同業者(?)のかばやんさんです。上図の音角は、そんなかばやんさんに特別に作成頂いた『プロップサイズ音角』です。3Dプリンタで作成されたもので、実際に手に持った人にしかわかりませんが重量感もすごく、まさに本物の音角と言っても過言ではないほどの仕上がりになっています(※本物の音角はもちろん存在しませんが、そう言いたくなるぐらいすごいのです)。

この『プロップサイズ音角』を作成頂くにあたり、私が所有していた『DX音角』(写真左。こうして並べてみるととてもデカイです)をかばやんさんに渡して、中の基板を移植して頂きました。

その結果、自分の手元には『一部の部品とガワだけの音角』が残ることになり、これをまた遊べるように復旧させるついでに、元々の音角に足りていなかった要素を何かプラスアルファできないかなと考えてみました。

すぐに思いついたのは、ディスクアニマル関連の音声の強化です。『響鬼』の劇中ではディスクアニマルによる索敵、および戦闘サポートの描写はかなり多く、歴代の仮面ライダー作品のサポートアイテムの中でも非常に活躍の機会の多かったアイテムであったと思います。個人的には、ディスクアニマルがNo.1、ついで『仮面ライダーオーズ』のカンドロイドかなと思っています。

『仮面ライダー響鬼』の玩具売上は正直かなり厳しかったようですが、その中でディスクアニマルは人気があったのか、劇場版に登場した音式神の三枚セット『ムービーエディション』、装甲声刄によって強化されたディスクの三枚セット『アームドディスク』が一般販売され、さらにWeb限定で、劇中での隠密行動時をイメージした『ステルスディスク』の8枚セットが 発売されたりしました。最近になって全部集めましたよええ。

そんな大活躍のディスクアニマルだったのですが、玩具開発のスケジュールと劇中での活躍の仕方のイメージが固まる時期がずれてしまったのか、元々の音角のディスクアニマル連携機能は、『ディスクをセットして回すと、ディスクアニマル(アカネタカ)の鼓動音が聞こえる』という、劇中のイメージとは少し違うものになっていました。劇中ではディスクアニマルをセットすると、鼓動音ではなくディスクアニマルからの情報読取音が流れており、実際、後に発売された『コンプリートセレクション 変身音叉 音角』では、音声がディスクアニマル読取音に差し替えられました。

他にも、『ディスクアニマルを音角で叩くとディスクアニマルが起動する』といった描写もあり、このあたりの音声を追加することで、劇中にだいぶ近い遊び方ができる音角ができるのではないかと思いました。

特徴

そんなわけで『ディスクアニマル関連の機能を強化する』という目的で作り始めた『DX変身音叉 音角 Ver.2.0』ですが、せっかくなので通常の変身シーケンスも少しだけ手直ししてみました。

 

玩具の音角ではプレイバリューを高めるためか目が赤色LEDで発光するようになっていましたが、そもそも劇中では音角の目は発光しません。そのため、劇中に合わせていっそ光らせない、という選択肢もあったのですが、やっぱり光る方が玩具的には面白いので、せっかくなので響鬼に合わせて紫色に発光させるようにしてみました。これだけで結構、玩具的に引き締まる感じがして、この変更は結構気に入っています。

余談ですが、この作品を公開する前に、全く無関係のツイートとして上記の写真つきで音角について投稿してみたのですが、特に誰からも『目が紫に光っている』ということについてのツッコミはありませんでした。おかげで、15年前の、しかもあまり売れなかった玩具の元々の仕様を知っている人は流石に少ないのだなということがわかりましたので、YouTube動画の方では動画の冒頭で『できれば他の方のレビューとかで元々の音角がどんなオモチャか見てみてくださいね』とお願いさせて頂くことにしました。

ここからが独自の追加機能になります。まず、『ディスクアニマルの起動』の再現です。

 

音角の手元の横スイッチを押しながら音角を叩くことで、ディスクアニマルが起動するときの音声が再生されます。これで、劇中でもよく見られた「よろしくな!」等々言いながらディスクアニマルを送り出すシーンを再現できるようになりました。

そして、戻ってきたディスクアニマルから情報を読み取るシーンも再現できるようにしています。

 

音声を劇中の読取音に差し替えているのはもちろんですが、それに加えて、『ディスクを直接音角にセットする』ということができるようにしています。

元々の音角では、上のような黒いトレイにディスクアニマルをセットしてから音角にセットする仕様になっています。この仕様は玩具としては全く正しくて、こうしないとディスクを回転させたときに、ディスクがバラバラになってしまうことがあります(特にニビイロヘビ)。ただ、劇中ではもちろんトレイは存在していないことと、あと少し回す程度であればディスクがバラバラになってしまうこともないので、今回は劇中の雰囲気再現重視で、ディスクを直接セットできるようにしました。元々の音角ではディスクを直接セットすると穴のサイズが合わなくてぶかぶかになってしまうので、ちょっと音角側の軸を太らせるという対応をしています。詳しくは「ハードウェア解説」の方で説明します。

そして今回の個人的に一番の目玉機能である『ディスクアニマルの個別認識』です。

 

セットしたディスクアニマルに応じて、それぞれ個別の鳴き声が再生されるようにしてみました。上の動画ではアカネタカ・ルリオオカミ・リョクオオザルの3つだけですが、YouTube動画の方では本編に登場した9体のディスクアニマル全ての個別認識を行っています(※なお、コガネオオカミの音声のみ、ルリオオカミと共通)。正確には劇中で「ディスクをセットすると鳴き声が鳴ってから読取が始まる」というようなシーケンスがあるわけではないのですが、この機能を追加することで、ディスクアニマルの生きている感じ(?)を出せているような気がして、個人的にはかなりお気に入りの機能になっています。

あと動画ではわかりにくいですが、ディスクアニマルを認識したときの音角の目の色も、ディスクアニマルの色に対応するように変化させています。

このディスクアニマルの個別認識ですが、ポイントは『ディスクアニマル側は無改造で実現している』というところです。そして、その実現のために、簡単なものですが機械学習技術を使っていたりします。このあたりのことは「ソフトウェア解説」の方で詳しく説明致します。

ハードウェア解説

まずはいつものごとく、使用した部品の紹介です。

ここ数年ほとんどこれしか使っていませんが、最近はちょっと新しいマイコンを模索したりしています。余談。

いつもは110mAhのものを使用するのですが、今回はスペースに余裕があったので400mAhにしてみました。

今回は再生する音声ファイル数が少ないので、DF Player+SDカードではなく、こちらで充分です。

ライダー玩具によく使われているスピーカーと外径が同じで、こっちの方が薄くて音が良い感じがするので、よくこれに置き換えています。

音角の目を発光させるためのものです。単色のLEDでも良いのですが、せっかくこれを使ったので、認識したディスクアニマルに合わせて目の色を変えたりしてみました。

こちらは音角を叩いて反応させるために使用したセンサです。元々の音角の玩具基板を『プロップサイズ音角』に移植頂いた際に、振動センサも一緒に移植してもらったので、代わりとして用意しました。

これが今回の要となるセンサです。明るさ、色(RGB)、近接距離、それと簡単なジェスチャを認識できる優れものです。今回は色(RGB)と近接距離の認識機能を使用して、ディスクアニマルの個別認識機能を実現しています。

あとは、電源用のスライドスイッチと、色センサの認識をサポートするための白色LEDを使っています。このあたりの部品の使いこなしについては、この後説明します。

ちなみに、『DX変身音叉 音角』を使用するのは当然なのですが、こちらは今はもちろん生産されておらず、入手するには中古市場をあたる必要があります。完動品の音角は結構値が張りますが、音が鳴らないジャンク品であれば比較的安く入手できるかもしれません。

 

回路図はこんな感じになります。

実際に部品を詰め込んだのがこちら。

Arduino、MP3ボイスモジュール、リチウムイオンポリマー充電池は元々の電池ケース側に詰め込んでいます。

普段はブレッドボードを使ってプロトタイプを綿密に作り込んでから、最後に部品を筐体に詰め込んでいくという手順で作品を作っていて、それが正しい手順だと思っています。ただ、今回は例外的に、プロトを作らずいきなり部品を本体に埋め込んでいくという、ぶっつけ本番、行き当たりばったりな作り方をしました。というのも、今回使用した色・距離センサの値の出方が、ブレッドボード上で検証しているときと実際に音角に組み込んだときで大きく異なってきそうな気がしていたので、「実際作ってみんとわからんな。元々動かなくなっていたものだし、うまく動けば儲けもんだ」ぐらいの気持ちでエイやとやってしまいました。最終的にうまく動いてくれたので良かったですが、これはある程度自分のこれまでの経験に裏打ちされた結果でもあるので、良い子はマネしないでください。

 

以下、細部の説明です。まずは簡単に、ディスクアニマルの取付部について。

左に見えているのが、元々の音角のディスクアニマル取付部です。先にも述べましたが、ここにはディスクアニマルが直接はめ込まれるのではなく、黒トレイがはめ込まれる形になるため、ディスクアニマルを直接はめ込むとぶかぶかになってしまいます。

そこで、こんな感じのアダプタを3Dプリンタでサクッと作って見ました。内径10.5cm、外径14cm、高さ0.72cmです。

これで、ディスクアニマルが直接着脱可能になりました。

 

続いて振動センサについて。

これは解析用に、振動センサの上半分を切って中身を露出させてみた状態です。上図のように、中央に一本芯線が入っていって、その周りを柔らかいバネが覆っています。振動によってこのバネが芯線に接触することで通電するようになるという仕組みです。

振動センサの取付位置はここ、音角の二本角の付け根の位置がベストです。自分は最初他の位置に取り付けていたのですが、それだとイマイチ感度が悪く、この位置に取り付けることで感度がだいぶ改善されました。元々の音角の基板と振動センサの移植はかばやんさんの方で行って頂いたため、私の方では玩具版の振動センサがどこについていたかわからず、かばやんさんに問い合わせてしまいました。すみません、ご回答頂きありがとうございました。

 

最後に、今回の要の色・距離センサです。

センサはここ、持ち手部分に取り付けています。

外装を取り付けても問題なくセンシングできるよう、若干不細工ですが持ち手部分に大きく穴を開けています。

この色・距離センサの取付位置は色々試行錯誤していて、最初は音角の底部から必要時に引き出す形での実装をイメージしていました。細い持ち手の部分にセンサが仕込めるとは思っていなかったので。ただ、実際にセンサを入手して設計を始めてみると、引き出し式はかなり困難なことがわかり、また持ち手の部分も内側のプラ部分を削りまくればギリギリセンサを収めることができるのもわかったため、最終的にこの形に落ち着きました。

このセンサの使い方ですが、まず常時近接距離を計測していて、ディスクアニマルのセットによって距離が常時短くなったことを検出して、内部モードを「ディスクアニマル識別状態」に遷移させています。

「ディスクアニマル識別状態」に入った後に、ディスクアニマルを識別するためにディスクの色をRGB値で取得しているのですが、このとき取得できるRGB値は毎回同じ値を返してくれるわけではありません。極端な話、部屋を真っ暗にしてしまえばRGB値の各値は0になってしまいます。そのため、色のRGB値を読み取る際に、センサの近くからディスクに白色LEDを照射するようにして、RGB値を取得する際の環境をなるべく安定させるようにしています。

 

ハードウェアとしての解説は以上になります。

ソフトウェア解説

今回のソフトウェア的なポイントは、『取得した色情報(RGB値)から、どのようにしてディスクアニマルを特定するか』に尽きます。これは、読み替えると『RGB値を特徴量としてディスクアニマルを予測する』という機械学習の分類問題と捉えることができますので、この解き方を考えていきます。

まず前提として、予測モデルは最終的にArduinoマイコン上で動かすことになりますので、予測のための計算量が膨大になるような機械学習アルゴリズムは使用できません。従って、なるべく軽量な機械学習アルゴリズムを選んで実装していく形になります。

機械学習を始めるにはとにもかくにもデータを集めなくてはいけないので、ディスクアニマル1体につき100回、計900回分のRGB値のデータを取得してみました。それらをR,G,Bを三軸として三次元空間上にプロットしたものが以下になります。

なんとなく分離はできそうな気もしますが、同じディスクアニマルでも結構値がばらついていることがわかります。また、人が見ても色が似ているものについては、やはり分布の重なりが見えます。線形に分離できるかがちょっと怪しい感じがしたので、今回はk近傍法(k-NN, k-nearest neighgor)のアルゴリズムを使用してみることにしました。

k近傍法は簡単に言ってしまうと、「自分に距離的に近いものから順にk個のデータを選び、その中での多数決に従う」というアルゴリズムです。イメージとしては上図のような感じになります。特徴量が多くなり過ぎるとうまく機能しなかったり、予測のために学習データを全て保持しておかなくてはならないという短所はありますが、今回のような玩具レベルの規模の問題であれば問題なく使用できます。

ということで早速、取得したRGB値のデータ900個の内の8割にあたる720個(各ディスクアニマルにつき80個)を学習用データとし、残り2割の180個(各ディスクアニマルにつき20個)を未知の評価用データとして、k近傍法による分類性能を評価してみました。結果は以下のようになりました。なお、kの値は3とし、距離計算の方法はユークリッド距離を使うのが基本ですが、計算の簡略化のためマンハッタン距離にしています。

ディスクアニマル 正解数 誤判定数 何と誤判定したか
アカネタカ 19 1 キアカシシ
ルリオオカミ 20 0 なし
リョクオオザル 17 3 ニビイロヘビ、アサギワシ×2
キハダガニ 20 0 なし
ニビイロヘビ 18 2 リョクオオザル×2
キアカシシ 19 1 アカネタカ
アサギワシ 18 2 ルリオオカミ、リョクオオザル
セイジガエル 19 1 アサギワシ
コガネオオカミ 20 0 なし

リョクオオザルが他に比べて若干厳しい感じです。また、この評価データでは「誤判定なし」になっていますが、実際作ったもので試してみたら、キハダガニとコガネオオカミを誤判定したりもします。それはまあ、人が見てもよく似ている色なので納得です。

実際に製品化するとかなると、これぐらいの予測精度ではほぼアウト(=クレームのもと)だと思いますが、私が趣味レベルで遊ぶ分には充分な予測精度が得られていると思いますので、これでOKとしました。

ソースコードの全文は本記事の一番最後に掲載しています。

まとめ

ということで、『DX変身音叉 音角 ver.2.0』のご紹介でした。今回のように『パッと見では改造しているかわからないもの』については、SNSなどでは評価されにくかったりするのですが、自分としてはかなり満足のいくものができたと思っています。簡単なものではありますが、前から取り入れたいと思っていた機械学習の技術も取り入れることができましたし。

おそらくそう遠くない未来に『CSM 変身音叉 音角』が発売されると思っているのですが、そのときはプロップサイズでかつ、これ以上のギミックを搭載してくれたら嬉しいなと思っています。

 

ソースコード

以下、ソースコードの全文です。内の100行ぐらいは、k近傍法で判定に用いるための学習用データになっています。なお、ここで掲載している学習用のRGB値のデータはあくまで私の環境で計測したものになりますので、同じセンサを使っていても他の方の環境だとそのまま流用はできないと思いますので、ご注意ください。

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

#define LOOP_DELAY_MS    0
#define STATE_KEEP_MS 6000
#define DISK_READ_INTERVAL 100
#define DISK_READ_START_COUNT 5

#define MP3_RX_PIN     2 // オレンジ
#define MP3_TX_PIN     3 // 白
#define SW_SIDE_PIN    4 // 紫
#define SW_DISK_PIN    5 // 緑
#define SW_VIBE_PIN    6 // 茶
#define LED_COLOR_PIN  7 // 青
#define LED_WHITE_PIN 10 // 灰

#define ON  LOW
#define OFF HIGH

#define STATE_INIT             0
#define STATE_READY            1
#define STATE_CHANGE_READY     2
#define STATE_CHANGING         3
#define STATE_DISK_ACTIVATING  4
#define STATE_DISK_IDENTIFYING 5
#define STATE_DISK_READY       6
#define STATE_DISK_READING     7

uint8_t state      = STATE_INIT;
uint8_t prev_state = STATE_INIT;

uint8_t sw_side_state      = OFF;
uint8_t prev_sw_side_state = OFF;
uint8_t sw_disk_state      = OFF;
uint8_t prev_sw_disk_state = OFF;
uint8_t sw_vibe_state      = OFF;
uint8_t prev_sw_vibe_state = OFF;

unsigned long state_changed_time = 0;
unsigned long latest_onoff_time = 0;
uint8_t disk_read_ready_counter = 0;

void print_state(){
  if(prev_state != state){
    switch(state){
    case STATE_INIT:             Serial.println(F("STATE_INIT")); break;
    case STATE_READY:            Serial.println(F("STATE_READY")); break;
    case STATE_CHANGE_READY:     Serial.println(F("STATE_CHANGE_READY")); break;
    case STATE_CHANGING:         Serial.println(F("STATE_CHANGING")); break;
    case STATE_DISK_ACTIVATING:  Serial.println(F("STATE_DISK_ACTIVATING")); break;
    case STATE_DISK_IDENTIFYING: Serial.println(F("STATE_DISK_IDENTIFYING")); break;
    case STATE_DISK_READY:       Serial.println(F("STATE_DISK_READY")); break;
    case STATE_DISK_READING:     Serial.println(F("STATE_DISK_READING")); break;
    default: ;
    }
  }
}

////////// 色・距離センサ ////////////////////////////////////////////////////////

#include "Adafruit_APDS9960.h"
Adafruit_APDS9960 apds;

uint16_t color_sensor_r = 0;
uint16_t color_sensor_g = 0;
uint16_t color_sensor_b = 0;
uint16_t color_sensor_c = 0;

#define DISK_FIX_COUNT 50
uint8_t disk_check_count = 0;
uint16_t sum_r = 0;
uint16_t sum_g = 0;
uint16_t sum_b = 0;
uint16_t sum_c = 0;

#define DISTANCE_THRESHOLD    8
#define DISTANCE_FIX_COUNT 1000

int distance_count = 0;

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

#include <SoftwareSerial.h>

#define SOUND_VOLUME_DEFAULT 20 // 0〜30

#define SOUND_POWER_ON      1
#define SOUND_CHANGE_READY  2
#define SOUND_CHANGE        3
#define SOUND_DISK_ACTIVATE 4
#define SOUND_DISK_READING  5
#define SOUND_AKANE_TAKA    6
#define SOUND_RURI_OKAMI    7
#define SOUND_RYOKU_OZARU   8
#define SOUND_KIHADA_GANI   9
#define SOUND_NIBIIRO_HEBI 10
#define SOUND_KIAKA_SHISHI 11
#define ASAGI_WASHI        12
#define SEIJI_GAERU        13

SoftwareSerial ss_mp3_player(MP3_RX_PIN, MP3_TX_PIN);

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

  // なぜか、指定したトラックに対して以下のようにファイルが再生される。Macの環境依存?
  // 指定:1 -> 再生:01.mp3
  // 指定:2 -> 再生:不可
  // 指定:3 -> 再生:02.mp3
  // 指定:4 -> 再生:不可
  // 指定:5 -> 再生:03.mp3
  // 指定:6 -> 再生:不可
  // 指定:7 -> 再生:04.mp3
  // 指定:8 -> 再生:不可
  // ...
  // 原因は不明だが、とりあえず上記仕様に合わせるようにしておく

  uint8_t temp_track = track*2 - 1;
  uint8_t play[6] = {0xAA,0x07,0x02,0x00,temp_track,temp_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);
}

////////// フルカラーLED処理 ////////////////////////////////////////////////////////

#include <Adafruit_NeoPixel.h>
#define N_COLOR_LED 2

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(N_COLOR_LED, LED_COLOR_PIN, NEO_GRB);

struct color_rgb {
  uint8_t r;
  uint8_t g;
  uint8_t b;
};

struct color_rgb COLOR_OFF     = {  0,  0,  0};
struct color_rgb COLOR_PURPLE  = {255,  0,255};
struct color_rgb COLOR_AKANE   = {255,  0,  0}; // 本来は{183, 40, 46}
struct color_rgb COLOR_RURI    = {  0,  0,255}; // 本来は{ 42, 92,170}
struct color_rgb COLOR_GREEN   = {  0,255,  0};
struct color_rgb COLOR_YELLOW  = {255,255,  0};
struct color_rgb COLOR_NIBI    = {114,113,113};
struct color_rgb COLOR_KIAKA   = {236,104,  0};
struct color_rgb COLOR_ASAGI   = {  0,165,191};
struct color_rgb COLOR_SEIJI   = {  0,255,  0}; // 本来は{126,190,165}
struct color_rgb COLOR_GOLD    = {255,215,  0};

struct color_rgb *prev_color = 0;
struct color_rgb *disk_color = &COLOR_OFF;

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;

uint8_t prev_r = 0;
uint8_t prev_g = 0;
uint8_t prev_b = 0;

int  prev_interval_ms = 0;
uint8_t prev_steps = 0;

void update_color(uint8_t r, uint8_t g, uint8_t b){
  if(prev_r != r || prev_g != g || prev_b != b){
    pixels.setPixelColor(0, pixels.Color(r,g,b));
    pixels.setPixelColor(1, pixels.Color(r,g,b));
  }
  prev_r = r;
  prev_g = g;
  prev_b = b;
}

void led_base_pattern_on(struct color_rgb *color){
  update_color(color->r,color->g,color->b);
  prev_color       = 0;
  prev_interval_ms = 0;
  prev_steps       = 0;
}

void led_base_pattern_off(){
  update_color(0,0,0);
  prev_color       = 0;
  prev_interval_ms = 0;
  prev_steps       = 0;
}

void led_base_pattern_blink(struct color_rgb *color, unsigned long now_ms, int interval_ms){
  if(now_ms - prev_blink_time >= interval_ms){
    if(is_lighting){
      update_color(0,0,0);
    }else{
      update_color(color->r, color->g, color->b);
    }
    is_lighting = !is_lighting;
    prev_blink_time = now_ms;
  }
}

void led_base_pattern_inc(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(color != prev_color || 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 r_step = color->r/steps;
  uint8_t g_step = color->g/steps;
  uint8_t b_step = color->b/steps;
  update_color(r_step*current_step, g_step*current_step, b_step*current_step);

  prev_color = color;
  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_base_pattern_dim(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(color != prev_color || 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 r_step = color->r/steps;
  uint8_t g_step = color->g/steps;
  uint8_t b_step = color->b/steps;
  update_color(r_step*(steps-current_step), g_step*(steps-current_step), b_step*(steps-current_step));

  prev_color = color;
  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_base_pattern_blink_slowly(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps){
  // いずれかの条件が変化していたら、処理をリセットする
  if(color != prev_color || 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 r_step = color->r/steps;
  uint8_t g_step = color->g/steps;
  uint8_t b_step = color->b/steps;
  if(is_inc){
    update_color(r_step*current_step, g_step*current_step, b_step*current_step);
  }else{
    update_color(r_step*(steps-current_step), g_step*(steps-current_step), b_step*(steps-current_step));
  }
  if(now_ms - inc_dim_start_time >= interval_ms){
    is_inc = !is_inc;
    inc_dim_start_time = now_ms;
  }

  prev_color = color;
  prev_interval_ms = interval_ms;
  prev_steps = steps;
}

void led_pattern_power_on(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 4500){                           led_base_pattern_on(&COLOR_PURPLE);}
  else if(4500 < passed_ms && passed_ms <= 5500){ led_base_pattern_dim(&COLOR_PURPLE, now_ms, 1000, 20);}
  else{                                           led_base_pattern_off();}
}

void led_pattern_init(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_off();
}

void led_pattern_ready(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_blink_slowly(&COLOR_PURPLE, now_ms, 800, 20);
}

void led_pattern_change_ready(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 1500){                          led_base_pattern_dim(&COLOR_PURPLE, now_ms, 1500, 20);}
  else if(1500 < passed_ms && passed_ms <= 3500){ led_base_pattern_inc(&COLOR_PURPLE, now_ms, 2000, 20);}
  else{                                           led_base_pattern_blink_slowly(&COLOR_PURPLE, now_ms, 300, 20);}
}

void led_pattern_changing(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 1000){                           led_base_pattern_on(&COLOR_PURPLE);}
  else if(1000 < passed_ms && passed_ms <= 2000){ led_base_pattern_dim(&COLOR_PURPLE, now_ms, 1000, 20);}
  else if(2000 < passed_ms && passed_ms <= 3900){ led_base_pattern_blink(&COLOR_PURPLE, now_ms, 50);}
  else if(3900 < passed_ms && passed_ms <= 4600){ led_base_pattern_on(&COLOR_PURPLE);}
  else if(4600 < passed_ms && passed_ms <= 5600){ led_base_pattern_dim(&COLOR_PURPLE, now_ms, 1000, 20);}
  else{                                           led_base_pattern_off();}
}

void led_pattern_disk_activating(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms <= 1000){                          led_base_pattern_on(&COLOR_PURPLE);}
  else if(1000 < passed_ms && passed_ms <= 3000){ led_base_pattern_dim(&COLOR_PURPLE, now_ms, 2000, 20);}
  else{                                           led_base_pattern_off();}
}

void led_pattern_disk_identifying(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_on(&COLOR_PURPLE);
}

void led_pattern_disk_ready(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_on(disk_color);
}

void led_pattern_disk_reading(unsigned long passed_ms, unsigned long now_ms){
  led_base_pattern_blink(disk_color, now_ms, 50);
}

void control_led(unsigned long now_ms){

  if(prev_state != state){
    led_pattern_start_time = now_ms;
  }

  unsigned long passed_ms = now_ms - led_pattern_start_time;

  switch(state){
  case STATE_INIT:
    led_pattern_init(passed_ms, now_ms);
    break;
  case STATE_READY:
    led_pattern_ready(passed_ms, now_ms);
    break;
  case STATE_CHANGE_READY:
    led_pattern_change_ready(passed_ms, now_ms);
    break;
  case STATE_CHANGING:
    led_pattern_changing(passed_ms, now_ms);
    break;
  case STATE_DISK_ACTIVATING:
    led_pattern_disk_activating(passed_ms, now_ms); // ここはOFFでも良いかも
    break;
  case STATE_DISK_IDENTIFYING:
    led_pattern_disk_identifying(passed_ms, now_ms);
    break;
  case STATE_DISK_READY:
    led_pattern_disk_ready(passed_ms, now_ms); // ここはOFFでも良いかも
    break;
  case STATE_DISK_READING:
    led_pattern_disk_reading(passed_ms, now_ms);
    break;
  default:
    ;
  }

  pixels.show();
}

////////// ディスクアニマル識別処理 ////////////////////////////////////////

// kNN(k近傍法)ベースの判定、k=3の前提

// 学習用データ
#define NUM_TRAIN_DATA 900
const uint8_t TRAIN_DATA[][4] PROGMEM = {
  // アカネタカ
  {0,7,4,5},{0,6,4,5},{0,8,5,7},{0,5,4,5},{0,6,3,4},{0,4,3,3},{0,6,4,4},{0,9,5,6},{0,6,4,5},{0,7,4,5},
  {0,6,4,5},{0,5,3,4},{0,5,3,4},{0,5,3,4},{0,5,3,4},{0,6,3,4},{0,7,3,4},{0,6,4,5},{0,7,4,5},{0,7,5,5},
  {0,5,4,5},{0,8,5,6},{0,9,5,7},{0,6,5,6},{0,6,3,4},{0,6,5,6},{0,7,4,5},{0,7,5,6},{0,5,3,4},{0,6,4,5},
  {0,4,3,3},{0,5,4,4},{0,7,4,5},{0,9,7,8},{0,5,4,5},{0,8,4,5},{0,5,3,4},{0,11,6,7},{0,6,3,4},{0,8,5,6},
  {0,6,4,5},{0,4,3,3},{0,10,6,7},{0,12,8,9},{0,16,7,9},{0,8,5,6},{0,8,4,5},{0,6,4,4},{0,14,10,12},{0,8,4,4},
  {0,8,4,5},{0,10,4,6},{0,10,6,7},{0,10,5,6},{0,8,4,5},{0,9,5,6},{0,10,4,5},{0,15,7,9},{0,7,3,4},{0,7,4,5},
  {0,8,5,6},{0,9,5,6},{0,7,5,5},{0,11,8,10},{0,8,4,5},{0,8,4,5},{0,7,5,6},{0,8,4,5},{0,6,4,5},{0,5,3,3},
  {0,4,2,3},{0,6,4,6},{0,5,4,4},{0,4,2,3},{0,8,4,6},{0,9,5,7},{0,5,3,4},{0,11,6,8},{0,6,5,6},{0,7,4,5},
  {0,5,3,3},{0,6,5,6},{0,10,6,8},{0,8,5,6},{0,6,4,5},{0,6,5,6},{0,7,5,6},{0,8,5,6},{0,6,5,6},{0,8,6,7},
  {0,8,5,7},{0,6,3,4},{0,7,4,5},{0,10,6,7},{0,5,3,3},{0,6,3,4},{0,5,4,5},{0,7,4,5},{0,5,3,3},{0,5,3,4},
  // ルリオオカミ
  {1,4,8,10},{1,5,9,11},{1,5,9,11},{1,4,7,9},{1,4,7,10},{1,4,8,11},{1,4,8,10},{1,3,7,9},{1,5,8,10},{1,4,7,9},
  {1,4,7,10},{1,4,8,10},{1,3,5,7},{1,4,7,9},{1,3,6,8},{1,4,8,10},{1,3,7,9},{1,3,7,9},{1,6,11,14},{1,4,8,11},
  {1,5,10,12},{1,4,8,11},{1,4,8,10},{1,5,9,12},{1,5,9,11},{1,4,8,10},{1,3,6,8},{1,5,9,12},{1,3,7,9},{1,4,8,10},
  {1,4,8,10},{1,3,7,9},{1,3,6,7},{1,5,9,12},{1,4,8,10},{1,4,8,10},{1,4,8,10},{1,5,10,12},{1,4,8,10},{1,4,8,10},
  {1,6,10,13},{1,7,10,13},{1,4,7,9},{1,5,10,12},{1,3,6,8},{1,5,9,12},{1,3,6,8},{1,4,8,10},{1,4,8,10},{1,4,8,10},
  {1,6,9,12},{1,5,9,12},{1,5,9,11},{1,4,9,11},{1,4,8,11},{1,3,7,10},{1,4,8,11},{1,3,7,10},{1,3,7,9},{1,6,9,12},
  {1,5,8,11},{1,4,8,10},{1,2,5,7},{1,4,8,10},{1,4,8,10},{1,4,8,10},{1,5,10,12},{1,3,7,10},{1,3,6,8},{1,5,9,11},
  {1,4,7,9},{1,3,6,7},{1,4,7,9},{1,4,9,11},{1,4,8,10},{1,3,7,10},{1,5,9,12},{1,5,10,12},{1,5,9,11},{1,5,9,11},
  {1,5,9,12},{1,4,8,10},{1,5,9,11},{1,4,9,11},{1,5,10,12},{1,4,8,10},{1,7,11,13},{1,8,13,16},{1,4,9,11},{1,4,8,10},
  {1,5,10,12},{1,6,10,13},{1,6,11,14},{1,5,9,11},{1,5,10,13},{1,5,9,12},{1,5,10,13},{1,6,10,13},{1,7,13,16},{1,5,10,13},
  // リョクオオザル
  {2,3,5,5},{2,3,6,6},{2,4,6,7},{2,6,9,9},{2,2,4,5},{2,4,6,7},{2,3,6,6},{2,3,6,6},{2,3,5,5},{2,4,6,6},
  {2,2,4,4},{2,7,9,10},{2,2,4,4},{2,2,5,5},{2,3,5,6},{2,2,4,4},{2,3,5,6},{2,3,5,6},{2,3,6,7},{2,3,5,6},
  {2,2,4,5},{2,5,7,7},{2,2,4,5},{2,3,6,6},{2,2,4,4},{2,2,4,4},{2,2,4,4},{2,3,6,7},{2,3,5,6},{2,3,5,6},
  {2,3,6,6},{2,2,4,4},{2,3,6,6},{2,3,5,5},{2,2,4,4},{2,2,4,5},{2,3,5,5},{2,2,5,5},{2,3,5,6},{2,2,5,5},
  {2,2,4,5},{2,4,6,6},{2,3,5,6},{2,2,4,5},{2,3,5,6},{2,4,6,7},{2,3,5,5},{2,3,5,6},{2,4,6,6},{2,3,6,6},
  {2,4,6,7},{2,3,6,6},{2,3,5,6},{2,5,7,7},{2,2,4,5},{2,5,7,7},{2,2,5,5},{2,3,6,6},{2,3,5,5},{2,6,9,10},
  {2,3,5,6},{2,2,4,4},{2,2,4,5},{2,3,6,7},{2,2,5,5},{2,2,4,4},{2,2,5,5},{2,3,6,6},{2,3,6,6},{2,3,6,6},
  {2,3,6,6},{2,2,5,5},{2,3,6,6},{2,3,6,7},{2,2,5,5},{2,2,5,5},{2,3,6,6},{2,3,6,6},{2,3,5,6},{2,3,6,6},
  {2,3,6,6},{2,4,7,7},{2,2,5,5},{2,3,6,6},{2,5,8,8},{2,6,9,9},{2,4,8,8},{2,3,7,7},{2,3,6,6},{2,4,7,7},
  {2,5,8,9},{2,6,9,9},{2,2,5,5},{2,3,6,6},{2,3,6,6},{2,4,6,6},{2,6,10,10},{2,3,5,5},{2,3,6,7},{2,3,6,6},
  // キハダガニ
  {3,10,11,8},{3,8,9,6},{3,12,13,9},{3,9,11,9},{3,13,14,9},{3,10,11,8},{3,17,18,11},{3,20,21,13},{3,12,13,8},{3,8,9,6},
  {3,11,13,8},{3,10,12,9},{3,10,12,8},{3,8,10,7},{3,9,11,6},{3,11,12,8},{3,9,11,7},{3,11,12,8},{3,9,10,6},{3,17,18,12},
  {3,18,19,12},{3,11,12,8},{3,12,13,9},{3,8,10,7},{3,10,12,7},{3,9,11,7},{3,9,11,7},{3,10,12,8},{3,12,13,9},{3,19,22,16},
  {3,19,20,12},{3,9,10,7},{3,9,11,7},{3,14,17,14},{3,9,11,6},{3,18,20,14},{3,19,20,12},{3,15,17,12},{3,8,9,6},{3,9,10,7},
  {3,9,10,7},{3,9,11,8},{3,10,12,7},{3,10,11,7},{3,11,12,7},{3,19,21,13},{3,10,11,7},{3,12,13,9},{3,11,13,8},{3,12,14,12},
  {3,12,13,8},{3,10,11,7},{3,9,10,7},{3,22,23,17},{3,19,21,14},{3,10,11,7},{3,11,11,7},{3,11,11,7},{3,11,12,9},{3,11,12,7},
  {3,9,11,7},{3,9,11,7},{3,15,16,10},{3,20,21,13},{3,8,9,6},{3,9,10,6},{3,9,10,7},{3,9,10,7},{3,10,11,7},{3,12,13,8},
  {3,9,10,6},{3,11,12,8},{3,9,11,7},{3,12,13,7},{3,19,20,12},{3,10,11,8},{3,8,10,7},{3,12,14,10},{3,12,14,9},{3,7,8,5},
  {3,10,11,8},{3,12,15,13},{3,8,10,6},{3,11,12,8},{3,14,15,10},{3,8,9,6},{3,10,11,8},{3,9,11,8},{3,9,10,6},{3,12,14,9},
  {3,10,11,8},{3,12,13,9},{3,9,10,6},{3,10,11,7},{3,10,12,8},{3,11,12,8},{3,14,16,11},{3,9,10,7},{3,10,11,7},{3,13,15,10},
  // ニビイロヘビ
  {4,3,4,5},{4,3,4,5},{4,3,4,5},{4,3,4,5},{4,3,4,5},{4,3,4,5},{4,3,4,4},{4,7,9,10},{4,3,4,5},{4,3,4,5},
  {4,3,4,5},{4,5,6,7},{4,3,4,5},{4,5,6,6},{4,5,7,7},{4,4,5,6},{4,3,5,5},{4,4,6,6},{4,3,5,6},{4,4,5,5},
  {4,3,4,4},{4,4,5,6},{4,4,5,6},{4,4,5,6},{4,4,6,6},{4,4,5,6},{4,3,4,4},{4,3,5,5},{4,5,6,7},{4,3,5,5},
  {4,4,6,6},{4,5,6,8},{4,4,5,5},{4,4,5,5},{4,4,5,6},{4,2,3,4},{4,6,7,8},{4,8,10,12},{4,3,4,5},{4,4,5,6},
  {4,4,5,5},{4,5,6,7},{4,5,6,7},{4,3,5,5},{4,5,6,7},{4,4,6,6},{4,5,6,7},{4,5,6,7},{4,5,6,7},{4,4,5,6},
  {4,4,5,6},{4,4,5,6},{4,5,6,7},{4,4,5,6},{4,3,4,4},{4,3,4,5},{4,4,5,6},{4,4,6,6},{4,4,5,6},{4,5,6,6},
  {4,3,4,5},{4,5,7,7},{4,5,7,7},{4,2,4,4},{4,5,6,7},{4,5,6,7},{4,3,5,5},{4,8,10,11},{4,6,8,8},{4,5,7,8},
  {4,5,6,7},{4,4,5,5},{4,2,4,4},{4,4,5,5},{4,6,7,8},{4,7,9,9},{4,9,11,13},{4,3,4,5},{4,4,5,6},{4,7,10,10},
  {4,5,7,7},{4,3,4,5},{4,3,4,5},{4,9,10,12},{4,5,6,7},{4,4,5,6},{4,7,9,9},{4,6,8,8},{4,3,4,5},{4,2,4,4},
  {4,4,5,6},{4,6,7,8},{4,5,7,7},{4,5,6,7},{4,6,7,8},{4,6,7,8},{4,8,10,11},{4,5,6,6},{4,5,7,8},{4,5,6,7},
  // キアカシシ
  {5,9,7,7},{5,8,7,6},{5,7,5,5},{5,7,5,5},{5,9,7,7},{5,6,4,4},{5,7,5,5},{5,9,7,7},{5,6,5,4},{5,7,5,5},
  {5,9,7,7},{5,10,8,7},{5,7,5,5},{5,10,8,7},{5,10,8,7},{5,9,7,7},{5,10,9,9},{5,10,7,7},{5,8,6,5},{5,9,8,7},
  {5,9,7,7},{5,8,7,7},{5,7,6,6},{5,9,8,8},{5,6,4,4},{5,8,6,6},{5,10,8,7},{5,8,6,5},{5,6,4,4},{5,10,8,7},
  {5,9,7,7},{5,7,5,4},{5,8,6,5},{5,7,5,5},{5,7,5,5},{5,8,6,6},{5,9,7,7},{5,6,4,4},{5,7,5,5},{5,7,6,6},
  {5,7,6,6},{5,10,8,9},{5,7,5,5},{5,6,5,4},{5,11,10,10},{5,6,4,4},{5,10,8,7},{5,10,8,7},{5,9,7,7},{5,8,7,6},
  {5,10,8,7},{5,10,9,9},{5,11,9,8},{5,10,7,7},{5,10,8,7},{5,7,5,4},{5,12,9,9},{5,8,7,7},{5,9,7,7},{5,7,5,4},
  {5,13,11,12},{5,13,10,10},{5,8,6,6},{5,9,7,7},{5,10,7,6},{5,9,6,6},{5,10,8,7},{5,9,7,6},{5,11,8,7},{5,10,7,7},
  {5,8,5,5},{5,8,6,5},{5,7,5,4},{5,10,7,7},{5,8,6,6},{5,8,6,5},{5,11,9,8},{5,10,8,7},{5,11,9,8},{5,12,9,8},
  {5,8,6,5},{5,11,8,8},{5,11,9,8},{5,8,6,6},{5,9,6,6},{5,10,7,6},{5,8,5,5},{5,13,9,9},{5,11,8,7},{5,10,8,7},
  {5,8,6,5},{5,11,9,8},{5,10,8,7},{5,11,8,7},{5,9,7,6},{5,11,8,7},{5,11,9,8},{5,9,6,6},{5,9,6,6},{5,11,9,8},
  // アサギワシ
  {6,5,10,10},{6,6,12,13},{6,4,10,10},{6,8,16,18},{6,5,10,11},{6,4,9,10},{6,3,7,8},{6,4,9,10},{6,4,8,9},{6,4,8,9},
  {6,3,6,7},{6,3,7,7},{6,3,7,7},{6,3,7,8},{6,3,7,8},{6,3,7,8},{6,5,10,10},{6,5,11,11},{6,3,7,8},{6,3,7,8},
  {6,4,8,9},{6,3,7,7},{6,5,10,11},{6,4,9,10},{6,5,9,9},{6,4,9,9},{6,3,8,8},{6,5,10,11},{6,4,8,9},{6,3,7,7},
  {6,3,7,8},{6,4,8,9},{6,6,11,12},{6,4,8,8},{6,4,8,8},{6,5,10,11},{6,4,9,10},{6,4,9,10},{6,4,8,8},{6,6,12,13},
  {6,5,10,11},{6,8,15,17},{6,5,10,11},{6,3,7,7},{6,7,13,14},{6,4,7,8},{6,6,12,13},{6,3,7,8},{6,3,8,8},{6,5,10,11},
  {6,4,8,9},{6,4,8,9},{6,3,7,8},{6,4,9,9},{6,6,12,13},{6,8,16,18},{6,3,7,7},{6,3,7,7},{6,4,8,9},{6,3,7,8},
  {6,3,7,8},{6,7,14,16},{6,6,12,13},{6,8,15,17},{6,4,9,10},{6,5,11,12},{6,4,8,8},{6,7,12,14},{6,5,10,10},{6,6,12,13},
  {6,5,9,10},{6,6,11,12},{6,4,9,9},{6,5,10,11},{6,4,9,9},{6,6,13,15},{6,4,8,8},{6,4,9,10},{6,5,10,11},{6,3,7,8},
  {6,4,9,9},{6,3,7,7},{6,4,9,10},{6,6,12,13},{6,8,16,17},{6,4,9,10},{6,5,11,12},{6,6,11,13},{6,6,11,11},{6,6,12,13},
  {6,4,9,10},{6,9,16,17},{6,4,8,8},{6,11,21,21},{6,5,10,11},{6,6,12,12},{6,4,9,10},{6,7,13,14},{6,5,10,11},{6,3,7,7},
  // セイジガエル
  {7,5,9,7},{7,5,9,8},{7,4,6,6},{7,4,7,6},{7,3,6,6},{7,3,6,5},{7,5,9,7},{7,4,7,6},{7,5,9,7},{7,5,8,7},
  {7,5,8,7},{7,3,6,5},{7,4,7,6},{7,6,9,8},{7,2,5,4},{7,4,7,6},{7,7,11,10},{7,6,11,8},{7,6,9,8},{7,4,7,6},
  {7,5,9,9},{7,5,8,7},{7,4,8,7},{7,4,7,6},{7,6,10,8},{7,3,7,5},{7,4,8,6},{7,5,8,7},{7,3,6,5},{7,5,9,7},
  {7,7,10,11},{7,4,7,5},{7,6,11,8},{7,5,8,7},{7,4,7,6},{7,7,11,9},{7,5,8,6},{7,7,11,9},{7,4,7,6},{7,4,8,6},
  {7,4,8,6},{7,5,9,8},{7,5,8,7},{7,6,11,8},{7,6,10,8},{7,5,9,7},{7,7,11,9},{7,6,10,8},{7,6,10,8},{7,4,7,6},
  {7,5,8,6},{7,5,8,7},{7,7,11,9},{7,9,13,13},{7,7,12,9},{7,4,8,7},{7,5,9,7},{7,5,9,7},{7,4,7,5},{7,4,8,6},
  {7,5,9,7},{7,3,6,5},{7,3,7,5},{7,8,12,10},{7,3,6,5},{7,4,8,6},{7,4,8,6},{7,4,7,6},{7,5,10,7},{7,5,9,8},
  {7,6,10,8},{7,4,7,5},{7,5,8,7},{7,6,9,8},{7,3,6,5},{7,7,10,9},{7,3,6,5},{7,3,6,5},{7,5,8,7},{7,4,6,6},
  {7,5,8,6},{7,3,6,5},{7,6,9,8},{7,7,13,10},{7,4,8,6},{7,3,6,5},{7,4,8,7},{7,7,11,10},{7,4,8,6},{7,4,8,6},
  {7,4,7,6},{7,6,10,8},{7,3,6,5},{7,4,7,6},{7,4,8,6},{7,3,5,4},{7,6,10,8},{7,5,9,7},{7,3,6,5},{7,3,6,5},
  // コガネオオカミ
  {8,8,7,5},{8,9,8,6},{8,10,9,6},{8,12,11,8},{8,12,10,8},{8,12,11,8},{8,10,9,7},{8,12,11,8},{8,14,14,12},{8,11,10,7},
  {8,10,8,6},{8,12,12,9},{8,12,10,7},{8,12,12,9},{8,13,12,9},{8,11,10,8},{8,14,13,10},{8,13,12,8},{8,12,11,8},{8,11,9,7},
  {8,12,11,8},{8,8,7,5},{8,11,10,8},{8,12,11,8},{8,12,11,8},{8,11,10,7},{8,9,8,6},{8,8,7,5},{8,12,11,8},{8,10,9,7},
  {8,10,9,7},{8,8,7,5},{8,13,12,9},{8,12,11,9},{8,9,8,6},{8,9,8,6},{8,8,7,5},{8,12,10,7},{8,11,9,7},{8,11,10,7},
  {8,10,9,6},{8,9,8,6},{8,13,11,8},{8,11,10,7},{8,13,12,8},{8,12,11,8},{8,9,8,6},{8,9,8,5},{8,10,9,7},{8,9,8,6},
  {8,11,9,7},{8,11,10,7},{8,12,11,8},{8,10,9,7},{8,12,11,7},{8,12,12,9},{8,12,10,7},{8,11,10,7},{8,11,10,7},{8,11,10,8},
  {8,13,11,8},{8,11,9,7},{8,11,10,7},{8,12,10,8},{8,12,11,8},{8,10,9,6},{8,11,10,7},{8,11,9,7},{8,12,11,8},{8,12,10,7},
  {8,13,12,10},{8,13,12,9},{8,11,9,7},{8,14,13,10},{8,15,13,10},{8,13,12,9},{8,11,9,7},{8,12,10,8},{8,11,9,7},{8,11,10,7},
  {8,14,13,9},{8,14,13,11},{8,12,11,8},{8,15,14,10},{8,12,11,8},{8,11,10,7},{8,12,11,8},{8,11,10,7},{8,10,9,6},{8,12,11,7},
  {8,11,9,7},{8,10,8,6},{8,12,11,8},{8,10,9,6},{8,12,11,8},{8,11,10,7},{8,13,12,9},{8,10,9,7},{8,10,9,7},{8,12,11,8}
};

uint8_t predict_disk_animal(uint16_t r, uint16_t g, uint16_t b){
  uint8_t  nearest_disk_id[3] = {0,0,0};
  uint16_t nearest_distance[3] = {9999, 9999, 10000}; // 初期値は適当に大きな値
  for(uint16_t i=0;i<NUM_TRAIN_DATA;i++){
    uint8_t train_disk_id = pgm_read_byte(&(TRAIN_DATA[i][0]));
    uint8_t train_r       = pgm_read_byte(&(TRAIN_DATA[i][1]));
    uint8_t train_g       = pgm_read_byte(&(TRAIN_DATA[i][2]));
    uint8_t train_b       = pgm_read_byte(&(TRAIN_DATA[i][3]));
    uint16_t distance = abs(train_r-r) + abs(train_g-g) + abs(train_b-b); // 計算簡略化のためマンハッタン距離を使用
    if(distance < nearest_distance[0]){
      nearest_distance[2] = nearest_distance[1];
      nearest_distance[1] = nearest_distance[0];
      nearest_distance[0] = distance;
      nearest_disk_id[2]  = nearest_disk_id[1];
      nearest_disk_id[1]  = nearest_disk_id[0];
      nearest_disk_id[0]  = train_disk_id;
    }else if(distance < nearest_distance[1]){
      nearest_distance[2] = nearest_distance[1];
      nearest_distance[1] = distance;
      nearest_disk_id[2]  = nearest_disk_id[1];
      nearest_disk_id[1]  = train_disk_id;
    }else if(distance < nearest_distance[2]){
      nearest_distance[2] = distance;
      nearest_disk_id[2]  = train_disk_id;
    }
  } 

  uint8_t count_neighbors[9] = {0,0,0,0,0,0,0,0,0};
  for(uint8_t i=0;i<3;i++){
    count_neighbors[nearest_disk_id[i]]++;
  }

  uint8_t pred_disk_id  = 0;
  uint8_t neighbors_max = 0;
  for(uint8_t i=0;i<9;i++){
    uint8_t count_neighbor = count_neighbors[i];
    if(count_neighbor > neighbors_max){
      pred_disk_id = i;
      neighbors_max = count_neighbor;
    }
  }

  // デバッグ用出力
  Serial.print(F("Nearest Disk IDs: "));
  for(uint8_t i=0;i<3;i++){
    Serial.print(nearest_disk_id[i]);
    Serial.print(F(" "));
  }
  Serial.println("");
  Serial.print(F("Nearest Distances: "));
  for(uint8_t i=0;i<3;i++){
    Serial.print(nearest_distance[i]);
    Serial.print(F(" "));
  }
  Serial.println("");

  return pred_disk_id;
}
///////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  pinMode(SW_SIDE_PIN,   INPUT_PULLUP);
  pinMode(SW_DISK_PIN,   INPUT_PULLUP);
  pinMode(SW_VIBE_PIN,   INPUT_PULLUP);
  pinMode(LED_COLOR_PIN, OUTPUT);
  pinMode(LED_WHITE_PIN, OUTPUT);

  // ---------- MP3プレイヤー初期設定 ----------
  ss_mp3_player.begin(9600);
  mp3_set_volume(SOUND_VOLUME_DEFAULT);

  // ---------- 色・距離センサ初期設定 ----------
  if(!apds.begin()){
    Serial.println("failed to initialize device! Please check your wiring.");
  }else{
    //Serial.println("Device initialized!");
  }

  //enable color sensign mode
  apds.enableColor(true);

  //enable proximity mode
  apds.enableProximity(true);

  // ---------- 起動処理 ----------
  digitalWrite(LED_WHITE_PIN, LOW);
  mp3_play(SOUND_POWER_ON);
  led_pattern_start_time = millis();
  while(true){
    unsigned long now_ms = millis();
    unsigned long passed_ms =  now_ms - led_pattern_start_time;
    led_pattern_power_on(passed_ms, now_ms);
    pixels.show();
    if(passed_ms >= 6000){
      break;
    }
    delay(20);
  }

}

void loop() {
  unsigned long now_ms = millis();

  ////////// 横スイッチ処理 ////////////////////
  sw_side_state = digitalRead(SW_SIDE_PIN);

  if(prev_sw_side_state == OFF && sw_side_state == ON){
    if(state == STATE_INIT || state == STATE_DISK_IDENTIFYING){
      state = STATE_READY;
      delay(500); // ディスクアニマル起動処理の誤動作防止のためのディレイ
    }else if(state == STATE_CHANGE_READY){
      mp3_play(SOUND_CHANGE);
      state = STATE_CHANGING;
      state_changed_time = now_ms;
    }
  }

  ////////// ディスク読取スイッチ処理 ////////////////////
  sw_disk_state = digitalRead(SW_DISK_PIN);

  if(state == STATE_DISK_READY || state == STATE_DISK_READING){
    if(prev_sw_disk_state != sw_disk_state){
       // ON/OFFのいずれかが発生したら
       if(state == STATE_DISK_READY){
          if(disk_read_ready_counter > DISK_READ_START_COUNT){
            // 少し触っただけでは読取を開始しないように
            mp3_play(SOUND_DISK_READING);
            state = STATE_DISK_READING;
          }else{
            disk_read_ready_counter++;
          }
       }
       latest_onoff_time = now_ms;
    }
  }

  ////////// 振動スイッチ処理 ////////////////////
  sw_vibe_state = digitalRead(SW_VIBE_PIN);
  // ディスクアニマル起動処理の分岐のため、横スイッチの状態を取り直す
  sw_side_state = digitalRead(SW_SIDE_PIN);

  if(prev_sw_vibe_state == OFF && sw_vibe_state == ON){
    if(state == STATE_READY){
      if(sw_side_state == OFF){
        mp3_play(SOUND_CHANGE_READY);
        state = STATE_CHANGE_READY;
      }else{ // sw_side_state == ON
        // ディスクアニマルを起動させる
        mp3_play(SOUND_DISK_ACTIVATE);
        state = STATE_DISK_ACTIVATING;
        state_changed_time = now_ms;
      }
    }
  }

  ////////// ディスク着脱認識処理 ////////////////////

  int distance = apds.readProximity() ;

  if(state == STATE_INIT){
    if(distance >= DISTANCE_THRESHOLD){
      distance_count++;
      if(distance_count >= DISTANCE_FIX_COUNT){
        digitalWrite(LED_WHITE_PIN, HIGH);
        delay(10);
        state = STATE_DISK_IDENTIFYING;
        distance_count = 0;
        disk_check_count = 0;
        sum_r = 0;
        sum_g = 0;
        sum_b = 0;
      }
    }else{
      distance_count = 0;
    }
  }else if(state == STATE_DISK_READY){
    if(distance < DISTANCE_THRESHOLD){
      distance_count++;
      if(distance_count >= DISTANCE_FIX_COUNT){
        mp3_stop();
        state = STATE_INIT;
        distance_count = 0;
        disk_color = &COLOR_OFF;
        disk_read_ready_counter = 0;
      }
    }else{
      distance_count = 0;
    }
  }

  ////////// ディスク個別認識処理 ////////////////////
  if(state == STATE_DISK_IDENTIFYING){
    while(!apds.colorDataReady()){
      delay(5);
    }
    apds.getColorData(&color_sensor_r, &color_sensor_g, &color_sensor_b, &color_sensor_c);
    sum_r += color_sensor_r;
    sum_g += color_sensor_g;
    sum_b += color_sensor_b;
    sum_c += color_sensor_c;
    disk_check_count++;
    if(disk_check_count == DISK_FIX_COUNT){
      // ディスクを特定
      uint16_t avg_r = sum_r/disk_check_count;
      uint16_t avg_g = sum_g/disk_check_count;
      uint16_t avg_b = sum_b/disk_check_count;
      uint16_t avg_c = sum_c/disk_check_count;
      //Serial.println(F("Color:"));
      //Serial.print(avg_r); Serial.print(F(","));
      //Serial.print(avg_g); Serial.print(F(","));
      //Serial.print(avg_b); Serial.print(F(","));
      //Serial.println(avg_c);
      uint8_t pred_disk_id = predict_disk_animal(avg_r, avg_g, avg_b);
      Serial.print(F("Disk: "));
      switch(pred_disk_id){
      case 0: disk_color = &COLOR_AKANE;  mp3_play(SOUND_AKANE_TAKA);   Serial.println("AkaneTaka");   break;
      case 1: disk_color = &COLOR_RURI;   mp3_play(SOUND_RURI_OKAMI);   Serial.println("RuriOkami");   break;
      case 2: disk_color = &COLOR_GREEN;  mp3_play(SOUND_RYOKU_OZARU);  Serial.println("RyokuOzaru");  break;
      case 3: disk_color = &COLOR_YELLOW; mp3_play(SOUND_KIHADA_GANI);  Serial.println("KihadaGani");  break;
      case 4: disk_color = &COLOR_NIBI;   mp3_play(SOUND_NIBIIRO_HEBI); Serial.println("NibiiroHebi"); break;
      case 5: disk_color = &COLOR_KIAKA;  mp3_play(SOUND_KIAKA_SHISHI); Serial.println("KiakaShishi"); break;
      case 6: disk_color = &COLOR_ASAGI;  mp3_play(ASAGI_WASHI);        Serial.println("AsagiWashi");  break;
      case 7: disk_color = &COLOR_SEIJI;  mp3_play(SEIJI_GAERU);        Serial.println("SeijiGaeru");  break;
      case 8: disk_color = &COLOR_GOLD;   mp3_play(SOUND_RURI_OKAMI);   Serial.println("KoganeOkami"); break; // コガネオオカミの音声はルリオオカミと同一
      default: ;
      }

      state = STATE_DISK_READY;
      digitalWrite(LED_WHITE_PIN, LOW);
    }
  }

  ////////// 時間経過処理 ////////////////////

  if(state == STATE_CHANGING || state == STATE_DISK_ACTIVATING){
    if(now_ms - state_changed_time >= STATE_KEEP_MS){
      state = STATE_INIT;
    }
  }

  if(state == STATE_DISK_READING){
    if(now_ms - latest_onoff_time >= DISK_READ_INTERVAL){
      // ディスク読取終了
      mp3_stop();
      disk_read_ready_counter = 0;
      state = STATE_DISK_READY;
      // 状態を遷移させるのは、ディスクを取り外した時
    }
  }  

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

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

  ////////// 処理状態の保持 ////////////////////
  prev_sw_side_state = sw_side_state;
  prev_sw_disk_state = sw_disk_state;
  prev_sw_vibe_state = sw_vibe_state;
  prev_state = state;

  delay(LOOP_DELAY_MS);
}