バイスタンプの音声を差し替えて色々遊ぶ

今回はバイスタンプ本体を改造不要で音声を差し替えられるツールを作ったので、それで色々遊んでみました。

製作経緯

実は今回はこれを作ろうと思って作ったのではなく、別の目的で作ったものが結果的にこういう用途(音声差し替え)にも使えた、というのが実態です。この音声差し替え機能だけでも、技術的にはそれなりに面白いことをやっているかなと思ったので、せっかくなのでこの機能単体での遊びもご紹介しようと思った次第です。

このツールの本当の目的は、またいずれ。

 

特徴

冒頭で述べたとおり、バイスタンプ本体を改造することなく音声を差し替えることができます。ものすごく頑張れば全音声の差し替えも不可能ではないのですが、今回作ったツールでは、差し替えられる音声は以下の3つです。

  1. 天面ボタン操作音(通常:バイスタンプ名)
  2. スタンプ全押し音(通常:押印音)
  3. スタンプ半押し音(通常:変身待機音)

これだけでもいろんな遊び方ができますよ、ということで、動画では3つのケースを紹介してみました。

また、音声が差し替わるだけでなく、きちんと発光が伴うところもポイントです。

なお、バイスタンプ本体は改造していないので、このツールを外せば元のバイスタンプの音声に戻ります。

 

これがこのツールの良いところです。普通のライダー玩具で音声差し替えをやろうとすると、中身総入れ替えの大改造になってしまうのが常なのですが、今回は後述するバイスタンプの特徴によって、外付け型で(比較的)簡単に音声の差し替え遊びが楽しめます。おかげで、動画のように多数のバイスタンプの音声差し替えを楽しむ、ということができるようになりました。

ちなみにお気に入りは一輝/バイスバイスタンプと、大二/カゲロウバイスタンプです。どちらも、劇中の使われ方をうまく玩具の形に落とし込めたかなと思っています。

 

ハードウェア解説

今回使用した具材は以下になります。

今回の用途に限っていえばメインマイコンがM5StickC Plusである必要は全然ないのですが、冒頭述べたとおり、今回は元々別目的で作っていたものを本来とは違う用途で紹介しているものなので、M5StickC Plusである理由は、またそのときに改めてご説明する予定です。

私の工作で毎度お馴染みのMP3プレイヤーです。MP3プレイヤーの選択肢は他にもこちらがあるのですが、本体サイズの都合でDFPlayerを使わざるを得ないことが多いです。こちらの方がSDカードを用意しなくて良い分、コスト的には有利なのですが。

スピーカーは今回はサイズ制限がキツかったので、小型のこちらを採用しました。

バイスタンプ関係の改造のキーパーツです。これ一つで赤外線信号の送受信を行います。

これらをこんな感じで配線しまして。

Fusion360で設計、3Dプリンタで出力した筐体に突っ込んで完成です。

音声を色々差し替えて遊ぶものなので、MP3プレイヤーにセットするSDカードにアクセスしやすいようにしておきました。

 

ソフトウェア解説

今回のキモとなるソフトウェア解説です。まず、音声の差し替えはバイスタンプの以下の3つの特徴を使って実現しています。

  1. リバイスドライバーにセットしているときは無反応
  2. 無反応のときもリバイスドライバーと通信している
  3. 音声なしで発光だけも可能

順に解説していきます。なお、「そもそもバイスタンプとリバイスドライバーとの間の通信ってどうなってるんだ?」というところについては、既にこちらの記事で詳しく解説しているので、ここでは省略します。必要に応じて、前記リンク先をご参照ください。

 

1. リバイスドライバーにセットしているときは無反応

これは普通に遊んでいて気がついていた人も多いと思います。

 

バイスタンプは基本的に、リバイスドライバーにセットした後は、天面ボタンを押しても押印しても、バイスタンプは光りませんし音も鳴りません。例外は2つで、

  1. リミックス発動時
  2. バイスタンプ主題歌 Ver.での主題歌再生

になります。

 

前者は皆様ご存知のとおり、変身完了後にバイスタンプをレバーとして倒すと必殺技待機状態になり、このときは天面ボタン操作を受け付けてリミックス待機状態に切り替わります。

 

後者はかなり特殊な動作で、変身完了後は通常は天面ボタンを操作しても何も起きないところが、主題歌Verだけは主題歌が再生されるという動作になります。

 

2. 無反応のときもリバイスドライバーと通信している

これは、バイスタンプ⇄リバイスドライバー赤外線通信を解析しない限り、気付くのは難しいと思います。

バイスタンプは、1.の状態、すなわちリバイスドライバーにセットされて無反応になっているときも、実はリバイスドライバーに対して、各種操作に対応するコマンドを送っています。具体的には、

  • スタンプ全押し操作(要求): 131
  • スタンプ半押し操作(要求): 132
  • 天面ボタン押し操作(要求): 133

というコマンドを送信しています。また、リバイスドライバー側も、これらの信号を受信したときには、

  • スタンプ全押し操作(応答): 141
  • スタンプ半押し操作(応答): 142
  • 天面ボタン押し操作(応答): 143

という応答コマンドを返しています。

このやりとりは通常のバイスタンプだと意味がないのですが、例外はまたしても主題歌Ver.です。

主題歌Ver.は、天面ボタン操作コマンドに対する応答コマンドをリバイスドライバーから受け取ると、そこから更に「リバイスドライバーを発光させる」というコマンドをドライバーに送ります。主題歌Ver.で天面ボタンを操作したときにドライバー側が連動して発光するのはそのためです。

 

3. 音声なしで発光だけも可能

これはガンデフォンで遊んでいると気付きやすいと思います。

 

バイスタンプの発光/消灯は、バイスタンプをセットしたアイテム側からコントロールできるようになっています。また、発光については、私が調べた限りでは3パターンから選んで制御することができます。対応するコマンドは以下のとおりです。

  • 点滅発光要求(遅): 174
  • 点滅発光要求(速): 175
  • 多色発光要求: 186

このうち、「点滅発光要求(遅)」については、バイスタンプが2回点滅したら勝手に消灯してくれるのですが、「点滅発光要求(速)」と「多色発光要求」については、基本的には以下の消灯要求コマンドを送るまでバイスタンプが発光し続けます。

  • 消灯要求: 199

この仕様のおかげで、オーインバスター50、ガンデフォン、ツーサイドライバー、どの玩具にセットしても、発光が丁度良いタイミングで終了するようになっています。

 

最後に、これら3つの特徴を組み合わせます。

まず、バイスタンプに赤外線信号を送って、自身がドライバーにセットされたと錯覚させます。具体的には以下のような通信シーケンスになります。

これが正しい手順かどうかは不明ですが、これで一応バイスタンプはドライバーにセットされたと誤認して、音も発光もなくなります。

 

こんな感じですね。この状態でも、先述のとおり操作に対応したコマンドを赤外線通信で送ってきているので、それを受けてアダプター側のマイコンでMP3ファイルを再生させれば、音声の差し替えは完了です。

ただ、これだけだと音は鳴っても発光がない状態なので、最後にアダプター側から発光/消灯コマンドを送って完成となります。

 

まとめ

以上、バイスタンプの音声を差し替えて色々遊んでみました。

バイスタンプは、使いこなせばこんな感じで外部からの制御が容易なので、当初予定にない玩具が今後出てきたとしても、おそらく違和感無く連携させることができます。秘匿性も高いし柔軟性もある、本当に良くできた玩具だと思います。これを開発したバンダイ様の技術メンバー達には本当に頭が下がります。是非とも特別ボーナスでも差し上げて労ってあげて頂きたいです。

次回作は今回作ったツールに+α…というよりは、元々目指していたものに仕上げる形で、多分4〜5月ぐらい公開になると思います。

 

ソースコード

今回はエイヤと作ってしまったので、細かいところは雑かもしれません。ご了承ください。

////////// 基本設定 /////////////////////////////////////////////////////////////
#include <M5StickCPlus.h>

#define PIN_LED 10
#define PIN_MP3_RX 25
#define PIN_MP3_TX 26
#define PIN_IR_RX_TX_FOR_STAMP 33

#define STAMP_ID_REX          1
#define STAMP_ID_BRACHIO      4
#define STAMP_ID_KONG        11
#define STAMP_ID_MAMMOTH     16
#define STAMP_ID_LION        23
#define STAMP_ID_BAT         39
#define STAMP_ID_NEO_HOPPER  46
#define STAMP_ID_MAX        100
uint8_t stamp_id = 0;

#define STATE_INIT  0
#define STATE_SET   1
#define STATE_TOP   2
#define STATE_STAMP 3
uint8_t state = STATE_INIT;

#define BAT_MODE_LIVE 0
#define BAT_MODE_EVIL 1
uint8_t bat_mode = BAT_MODE_LIVE;
uint8_t voice_id = 0;

#define RESET_MS 200
unsigned long reset_start_ms = 0;
unsigned long blink_duration_ms = 0;
unsigned long blink_start_time_ms = 0;
boolean is_blinking = false;

uint8_t ready_top(){
  uint8_t send_data = 0;
  switch(stamp_id){
  case STAMP_ID_REX:
    send_data = 174; // バイスタンプ点滅発光(遅)要求
    break;
  case STAMP_ID_BRACHIO:
    send_data = 174; // バイスタンプ点滅発光(遅)要求
    break;
  case STAMP_ID_KONG:
    send_data = 174; // バイスタンプ点滅発光(遅)要求
    break;
  case STAMP_ID_MAMMOTH:
    send_data = 174; // バイスタンプ点滅発光(遅)要求
    break;
  case STAMP_ID_LION:
    send_data = 174; // バイスタンプ点滅発光(遅)要求
    break;
  case STAMP_ID_BAT:
    send_data = 175; // バイスタンプ点滅発光(速)要求
    voice_id++;
    if(bat_mode == BAT_MODE_LIVE){
      if(voice_id > 4){
        voice_id = 1;
      }
      switch(voice_id){
      case 1: blink_duration_ms = 2500; break;
      case 2: blink_duration_ms = 1500; break;
      case 3: blink_duration_ms = 2000; break;
      case 4: blink_duration_ms = 3500; break;
      default: ;
      }
    }else{
      if(voice_id > 3){
        voice_id = 1;
      }
      switch(voice_id){
      case 1: blink_duration_ms = 4500; break;
      case 2: blink_duration_ms = 4500; break;
      case 3: blink_duration_ms = 5500; break;
      default: ;
      }
    }
    is_blinking = true;
    blink_start_time_ms = millis();
    break;
  case STAMP_ID_NEO_HOPPER:
    send_data = 174; // バイスタンプ点滅発光(遅)要求
    break;
  default:
    ;
  }
  state = STATE_TOP;
  return send_data;
}

uint8_t ready_stamp(){
  uint8_t send_data = 0;
  switch(stamp_id){
  case STAMP_ID_REX:
    send_data = 175; // バイスタンプ点滅発光(速)要求
    blink_duration_ms = 5000;
    break;
  case STAMP_ID_BRACHIO:
    send_data = 186; // バイスタンプ多色発光要求
    blink_duration_ms = 10000;
    break;
  case STAMP_ID_KONG:
    send_data = 175; // バイスタンプ点滅発光(速)要求
    blink_duration_ms = 5000;
    break;
  case STAMP_ID_MAMMOTH:
    send_data = 175; // バイスタンプ点滅発光(速)要求
    blink_duration_ms = 5000;
    break;
  case STAMP_ID_LION:
    send_data = 186; // バイスタンプ多色発光要求
    blink_duration_ms = 7000;
    break;
  case STAMP_ID_BAT:
    if(bat_mode == BAT_MODE_LIVE){
      bat_mode = BAT_MODE_EVIL;
    }else{
      bat_mode = BAT_MODE_LIVE;
    }
    voice_id = 0;
    send_data = 186; // バイスタンプ多色発光要求
    blink_duration_ms = 5500;
    break;
  case STAMP_ID_NEO_HOPPER:
    send_data = 175; // バイスタンプ点滅発光(速)要求
    blink_duration_ms = 5000;
    break;
  default:
    ;
  }
  is_blinking = true;
  blink_start_time_ms = millis();
  state = STATE_STAMP;
  return send_data;
}

////////// IR信号送受信処理 ////////////////////////////////////////////////////////////

#define LEN_IR_DATA  8
#define IR_THRESHOLD_FOR_STAMP   500
#define READ_INTERVAL_MICROS    2050
#define SEND_IR_DELAY_MS          50

// デバッグ用プリント
void print_data(uint8_t data){
  for(uint8_t i=0;i<LEN_IR_DATA;i++){
    Serial.print(bitRead(data, LEN_IR_DATA-1-i)); // 8bitの左端から表示
  }
  Serial.print(" / ");
  Serial.println(data);
}

void send_start(uint8_t pin){
  digitalWrite(pin, HIGH);
  delayMicroseconds(6000);
  digitalWrite(pin, LOW);
  delayMicroseconds(500);
}

void send_bit_0(uint8_t pin){
  digitalWrite(pin, HIGH);
  delayMicroseconds(1500);
  digitalWrite(pin, LOW);
  delayMicroseconds(500);
}

void send_bit_1(uint8_t pin){
  digitalWrite(pin, HIGH);
  delayMicroseconds(500);
  digitalWrite(pin, LOW);
  delayMicroseconds(1500);
}

void send_ir(uint8_t pin, uint8_t data){
  pinMode(pin, OUTPUT); 

  send_start(pin);

  for(uint8_t i=0; i<LEN_IR_DATA; i++){
    // dataを左端から順に送信する
    if(bitRead(data, LEN_IR_DATA-1-i) == 0){
      send_bit_0(pin);
    }else{
      send_bit_1(pin);
    }
  }
}

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

#include <DFPlayerMini_Fast.h>

#define SOUND_TOP_DEFAULT    1
#define SOUND_TOP_HOPPER     2
#define SOUND_TOP_KONG       3
#define SOUND_TOP_MAMMOTH    4
#define SOUND_TOP_KUGA       5
#define SOUND_TOP_ZIO        6
#define SOUND_STAMP_DEFAULT  7
#define SOUND_STAMP_HOPPER   8
#define SOUND_STAMP_KONG     9
#define SOUND_STAMP_MAMMOTH 10
#define SOUND_STAMP_KUGA    11
#define SOUND_STAMP_ZIO     12
#define SOUND_CHAR_IKKI     13
#define SOUND_CHAR_VICE     14
#define SOUND_CHAR_LIVE_1   15
#define SOUND_CHAR_LIVE_2   16
#define SOUND_CHAR_LIVE_3   17
#define SOUND_CHAR_LIVE_4   18
#define SOUND_CHANGE_BAT    19
#define SOUND_CHAR_EVIL_1   20
#define SOUND_CHAR_EVIL_2   21
#define SOUND_CHAR_EVIL_3   22

#define SOUND_TYPE_NONE  0
#define SOUND_TYPE_TOP   1
#define SOUND_TYPE_STAMP 2
uint8_t sound_type = SOUND_TYPE_NONE;

HardwareSerial hs_mp3_player(1);
DFPlayerMini_Fast mp3_player;

#define SOUND_VOLUME_DEFAULT 23 // 0〜30 20

void play_sound(uint8_t sound_num){
  mp3_player.playFromMP3Folder(sound_num);
  Serial.print(F("Play sound: "));
  Serial.println(sound_num);
}

void pause_sound(){
  mp3_player.pause();
}

void play_sound_top(){
  switch(stamp_id){
  case STAMP_ID_REX:
    play_sound(SOUND_CHAR_IKKI);
    break;
  case STAMP_ID_BRACHIO:
    play_sound(SOUND_TOP_ZIO);
    break;
  case STAMP_ID_KONG:
    play_sound(SOUND_TOP_KONG);
    break;
  case STAMP_ID_MAMMOTH:
    play_sound(SOUND_TOP_MAMMOTH);
    break;
  case STAMP_ID_LION:
    play_sound(SOUND_TOP_KUGA);
    break;
  case STAMP_ID_BAT:
    if(bat_mode == BAT_MODE_LIVE){
      switch(voice_id){
      case 1: play_sound(SOUND_CHAR_LIVE_1); break;
      case 2: play_sound(SOUND_CHAR_LIVE_2); break;
      case 3: play_sound(SOUND_CHAR_LIVE_3); break;
      case 4: play_sound(SOUND_CHAR_LIVE_4); break;
      default: ;
      }
    }else{
      switch(voice_id){
      case 1: play_sound(SOUND_CHAR_EVIL_1); break;
      case 2: play_sound(SOUND_CHAR_EVIL_2); break;
      case 3: play_sound(SOUND_CHAR_EVIL_3); break;
      default: ;
      }
    }
    break;
  case STAMP_ID_NEO_HOPPER:
    play_sound(SOUND_TOP_HOPPER);
    break;
  default:
    play_sound(SOUND_TOP_DEFAULT);
  }
}

void play_sound_stamp(){
  switch(stamp_id){
  case STAMP_ID_REX:
    play_sound(SOUND_CHAR_VICE);
    break;
  case STAMP_ID_BRACHIO:
    play_sound(SOUND_STAMP_ZIO);
    break;
  case STAMP_ID_KONG:
    play_sound(SOUND_STAMP_KONG);
    break;
  case STAMP_ID_MAMMOTH:
    play_sound(SOUND_STAMP_MAMMOTH);
    break;
  case STAMP_ID_LION:
    play_sound(SOUND_STAMP_KUGA);
    break;
  case STAMP_ID_BAT:
    play_sound(SOUND_CHANGE_BAT);
    break;
  case STAMP_ID_NEO_HOPPER:
    play_sound(SOUND_STAMP_HOPPER);
    break;
  default:
    play_sound(SOUND_STAMP_DEFAULT);
  }
}

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

void setup(){
  M5.begin();

  Serial.begin(115200);
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_IR_RX_TX_FOR_STAMP, INPUT);
  // G25と共用で使用しないG36をフローティングにする
  gpio_pulldown_dis(GPIO_NUM_36);
  gpio_pullup_dis(GPIO_NUM_36);

  // 液晶画面は省電力化のためOFFにする
  M5.Axp.ScreenBreath(0); // Max 12

  //---------- MP3プレイヤー ----------
  hs_mp3_player.begin(9600, SERIAL_8N1, PIN_MP3_RX, PIN_MP3_TX);

  if(!mp3_player.begin(hs_mp3_player)) {
    Serial.println(F("Unable to begin music_player:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while(true);
  }
  Serial.println(F("mp3_player online."));
  delay(100);
  mp3_player.volume(SOUND_VOLUME_DEFAULT); 

  // 動作状態確認用LEDオン
  digitalWrite(PIN_LED, LOW); // LED点灯
}

void loop(){

  // 基本的にバイスタンプからの信号を受信モードで待つ
  pinMode(PIN_IR_RX_TX_FOR_STAMP, INPUT);

  reset_start_ms = millis();
  while(analogRead(PIN_IR_RX_TX_FOR_STAMP) < IR_THRESHOLD_FOR_STAMP){
    // 受信待ち待機
    if(state != STATE_INIT && millis() - reset_start_ms > RESET_MS){
      state = STATE_INIT;
    }
  }

  // 閾値を超えた(ONになった)ので、5500μs待つ
  delayMicroseconds(5500);

  // 送受信データは丁度1byteなので、1byteの変数に受信結果を収める
  uint8_t recv_data = 0;
  if(analogRead(PIN_IR_RX_TX_FOR_STAMP) >= IR_THRESHOLD_FOR_STAMP){
    // ここでも閾値を超えていれば、スタンプから信号が来たとみなし、およそ2000μsごとの読み取りを開始する
    for(uint8_t i=0; i<LEN_IR_DATA; i++){
      delayMicroseconds(READ_INTERVAL_MICROS);
      if(analogRead(PIN_IR_RX_TX_FOR_STAMP) < IR_THRESHOLD_FOR_STAMP){
        // ON:1500μs, OFF: 500μs ... 0
        // ON: 500μs, OFF:1500μs ... 1
        // なので、2000μ秒ごとのサイクルで1000μsの位置がONなら0、OFFなら1となる
        recv_data = recv_data | (1 << (LEN_IR_DATA-1-i));
      }
    }

    // スタンプから受け取った信号を表示する
    Serial.print("Stamp -> Adapter: ");
    print_data(recv_data);

    uint8_t send_data = 0;
    if(recv_data < STAMP_ID_MAX){// 100未満は全てバイスタンプのIDとみなす
       if(state == STATE_SET){
         send_data = 140; // シーケンス開始要求
       }else{
         send_data = 101; // ドライバーセット待機
         stamp_id = recv_data;
         state = STATE_SET;
       }
    }else{
      switch(recv_data){
      case 129: // バイスタンプ発光終了通知
        send_data = 130; // バイスタンプ発光終了応答
        break;
      case 131: // ドライバーセット後の押印全押し操作要求
        //send_data = 141; // 何もしないときの応答
        send_data = ready_stamp();
        break;
      case 132: // ドライバーセット後の押印半押し操作要求
        //send_data = 142; // 何もしないときの応答
        send_data = ready_stamp();
        break;
      case 133: // ドライバーセット後の天面ボタン操作要求
        //send_data = 143; // 何もしないときの応答
        send_data = ready_top();
        break;
      case 157: // ドライバー発光終了要求
        send_data = 207; // ドライバー発光終了応答
        break;
      case 183: // ドライバー点滅発光要求
        send_data = 233; // ドライバー点滅発光応答
        break;
      case 224: // バイスタンプ点滅発光(遅)応答
        play_sound_top();
        send_data = 127; // スタンプセット継続要求
        break;
      case 225: // バイスタンプ点滅発光(速)応答
        switch(state){
        case STATE_TOP:   play_sound_top();   break;
        case STATE_STAMP: play_sound_stamp(); break;
        default: ;
        }
        send_data = 127; // スタンプセット継続要求
        break;
      case 236: // バイスタンプ多色発光応答
        play_sound_stamp();
        send_data = 127; // スタンプセット継続要求
        break;
      default:
        if(is_blinking && millis() - blink_start_time_ms > blink_duration_ms){
          // 点滅発光(速)または多色発光を要求したときは、一定時間後に消灯要求を送る
          send_data = 199;
          is_blinking = false;
        }else{
          send_data = 127; // スタンプセット継続要求
        }
      }
    }

    delay(SEND_IR_DELAY_MS);

    // 読み取ったデータに対するレスポンスをバイスタンプへ伝える
    send_ir(PIN_IR_RX_TX_FOR_STAMP, send_data);

    // レスポンスとしてスタンプへ伝えた結果を表示する
    Serial.print("Stamp <- Adapter: ");
    print_data(send_data);
  }
}