OGIMOノート~家族のためのモノづくり~

OGIMOテックノート ~家族のためのモノづくり~

重度障害の息子を持つ父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具/ロボット関係の製作記録、思った事を残していきます

M5StickCを使ったタンバリン型スイッチを作ってみた

はじめに

Nintendo Switchコントローラを改造して、手の不自由な息子も一緒に家族でゲームを楽しめるための取り組み!

ogimotokin.hatenablog.com

前記事にて物理スイッチで操作できるゲームコントローラを製作しました。
これにより、様々なスタイルでNintendo Switchのゲームを楽しむ事が出来ます!

息子の操作サポートのために作ったコントローラですが、
単なる福祉用途としてだけでなく、きょうだい達も思わず使いたくなる面白い遊び方の可能性を秘めたコントローラにしていきたいと思い、どんな面白い拡張が出来るかを実験しています。

今回のターゲットは、上の娘がはまりつつある太鼓の達人
最初は体験版で遊んでたらすっかりハマり、普通のコントローラで楽しく遊んでいます。

今年のクリスマスプレゼントは、『太鼓の達人』をサンタさんにお願いしているハマり具合!


その姿を見ているうちに
「太鼓コントローラは実際に音は鳴らない。
 もしこれが、コントローラでなく実際に音の鳴る楽器で遊べたら、、、
 楽しいかも!」


太鼓の達人」は少ないボタンで遊べるので、肢体不自由な息子でも遊びやすいゲームやと思います。
そして、タンバリンは大きな面なので手を当てやすいので、息子の興味を引く事が出来るかもしれない!
楽器演奏がうまく出来ない息子でも、きょうだいで楽しく演奏する体験して欲しい!


という訳で、

Nintendo Switch太鼓の達人」に対応した
タンバリン型スイッチ

を製作してみました。

[要求仕様]
・タンバリンを鳴らすと、それに同期して実際の『太鼓の達人』ゲームに判定が入る事
 またLEDを光らせる等の簡易フィードバックもある事
・他のスイッチ機器でも使える事
・タンバリンのどこを叩いても反応する事
(力の弱い息子でも使える事が望ましい)

製作物

上記要求仕様を踏まえた結果、一番お手軽な実装方法として、
「M5StickCのマイク機能を使った音量反応型スイッチ」
という方針で、スイッチデバイスを開発しました。

f:id:motokiinfinity8:20191224224948j:plain

マイクの入力音量に応じてスイッチON/OFF制御を行うというシンプル構造。
タンバリン音源とチューニング(入力判定閾値の調整)のみで、しっかりとタンバリン音だけ反応してくれます。
また、音検出した際のフィードバックとして、M5StickCのLEDと「どんちゃん(赤い太鼓のキャラクタ)」っぽいイラストも表示してみた(笑)

なお、これを応用すれば、タンバリンだけでなく、例えば「呼び鈴」などもコントローラに出来ます(笑)


非常に広い応用が期待できそう!

ハードウェア

ハードウェア構成は非常にシンプルです。

f:id:motokiinfinity8:20191224235242j:plain

M5StickC内部にマイクが搭載されているおかげで、外部回路としてはスイッチON/OFFのためのフォトカプラがあればOK。
ただし、M5StickCはご存じの様にバッテリー容量が80mAHと少なく、子供が遊びたい時に充電切れてて遊べないのは致命傷になりかねないので、電池パックを取り付けれる様に配慮しました。


上記の様に、3cm×7cmの基板上でM5StickC含めてすべて収まります。

音量反応型のスイッチの場合、当然ながら他の雑音の影響を受けやすいのが欠点なのですが、
 ・タンバリンのすぐ近くにM5Stickを配置
 ・スイッチOFF→ONの音量判定閾値を高めに設定する

工夫により、多少の雑音ノイズの影響を抑える事ができました!

ソフトウェア

実は、ソフトウェアもそこまで開発工数を使いませんでした。
というのも、コードの多くは 以下のサイトにもある様な、M5StickCのマイク入力のサンプルコードを流用しているからです。

lang-ship.com


後は、音量入力の閾値決めになるのですが、ある程度は実験値を使いつつ、合わせて「ON→OFF時の判断音量閾値」と「OFF→ON時の判断音量閾値」を分ける(=ヒステリシス性を持たせる)事で、より精度の上げた判定条件にしています。

#include <M5StickC.h>
#include <driver/i2s.h>
#define PIN_CLK  0
#define PIN_DATA 34
#define READ_LEN (2 * 256)
#define GAIN_FACTOR 1
uint8_t BUFFER[READ_LEN] = {0};
uint16_t oldy[160];
int16_t *adcBuffer = NULL;

#define SWON_MIC_THMAX   2000
#define SWON_MIC_THMIN  -5000
#define SWOFF_MIC_THMAX   0
#define SWOFF_MIC_THMIN  -3000
#define MIC_AVRAGE   -1500
#define PIN_SWOUT 26

bool g_sw = 0;
bool g_sw_old = 0;

void i2sInit()
{
   i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
    .sample_rate =  44100,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
    .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 2,
    .dma_buf_len = 128,
   };

   i2s_pin_config_t pin_config;
   pin_config.bck_io_num   = I2S_PIN_NO_CHANGE;
   pin_config.ws_io_num    = PIN_CLK;
   pin_config.data_out_num = I2S_PIN_NO_CHANGE;
   pin_config.data_in_num  = PIN_DATA;
   
   i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
   i2s_set_pin(I2S_NUM_0, &pin_config);
   i2s_set_clk(I2S_NUM_0, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
}



void mic_record_task (void* arg)
{   
  size_t bytesread;
  while(1){
    //50ms分のバッファを読み出す
    i2s_read(I2S_NUM_0,(char*) BUFFER, READ_LEN, &bytesread, (50 / portTICK_RATE_MS));
    adcBuffer = (int16_t *)BUFFER;
    showSignal();
    vTaskDelay(50 / portTICK_RATE_MS);
  }
}

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(8);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(TFT_BLACK);

  // LED ON(GPIO_NUM_10 or M5_LED)
  pinMode(GPIO_NUM_10, OUTPUT);
  pinMode(PIN_SWOUT, OUTPUT);
  digitalWrite(GPIO_NUM_10, HIGH);  //LED OFF
  digitalWrite(PIN_SWOUT, LOW);  //SW出力 OFF

  i2sInit();
  xTaskCreate(mic_record_task, "mic_record_task", 2048, NULL, 1, NULL);
}

void loop() {
  int g_sw_now = g_sw;

  //OFF→ON判定時は「どんちゃん」を表示
  if(g_sw_old == 0 && g_sw_now == 1){
    M5.Lcd.fillCircle(100, 40, 36, 0xFFF0);  //クリーム色
    M5.Lcd.fillCircle(100, 40, 30, 0xF800);  //赤
    M5.Lcd.fillCircle(83,  35,  7, BLACK);  //右目
    M5.Lcd.fillCircle(117, 35,  7, BLACK);  //左目
    M5.Lcd.fillTriangle(90, 45, 110, 45, 100, 65, BLACK);
    M5.Lcd.fillTriangle(93, 48, 107, 48, 100, 62, 0x7800);  //ダークレッド
  }
  if(g_sw_old == 1 && g_sw_now == 0){
    M5.Lcd.fillScreen(TFT_BLACK);
  }

    // バッテリ電圧表示
  float vbat      = M5.Axp.GetVbatData() * 1.1 / 1000;
  //バッテリ残量[%]
  M5.Lcd.fillRect(0, 0, 60, 20, BLACK);
  M5.Lcd.setCursor(6, 0, 2);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.printf("%.2f V",vbat);

  g_sw_old = g_sw_now;
  vTaskDelay(100 / portTICK_RATE_MS); // otherwise the main task wastes half of the cpu cycles
}


void showSignal(){
  int y;
  int s_max = INT16_MIN;
  int s_min = INT16_MAX;
  for (int n = 0; n < 160; n++){
    y = adcBuffer[n] * GAIN_FACTOR;
    // 最大,最小更新
    if(y >= s_max) s_max = y;
    if(y <= s_min) s_min = y;
    
    y = map(y, INT16_MIN, INT16_MAX, 10, 70);
    oldy[n] = y;
  }

  //スイッチ判定
  //OFF→ON判定
  if(g_sw==0 && s_max >= SWON_MIC_THMAX  && s_min <= SWON_MIC_THMIN){
    g_sw = 1;
    digitalWrite(GPIO_NUM_10, LOW);   //LED ON
    digitalWrite(PIN_SWOUT, HIGH);  //SW出力 ON
  }
  //ON→OFF判定
  if(g_sw==1 && s_max <= SWOFF_MIC_THMAX  && s_min >= SWOFF_MIC_THMIN){
    g_sw = 0;
    digitalWrite(GPIO_NUM_10, HIGH);  //LED OFF
    digitalWrite(PIN_SWOUT, LOW);  //SW出力 OFF   
  }

  Serial.print("s_max:");
  Serial.print(s_max);

  Serial.print("  s_min:");
  Serial.println(s_min);
}

終わりに

タンバリンや呼び鈴など、実際の楽器の音を『太鼓の達人』をコントローラにしてしまうアイデアを具現化してみました。
無改造な何のヘンテツもないタンバリンなのに、それがゲームコントローラになる体験は新鮮で楽しかった!

ただ、一番の課題は「入力の遅延」
音が鳴るタイミング=入力判定となるで、少し早めに叩かないといけないので、そこがコツがいりました。(なので、本気の音ゲー用途にはむいておらず、子供の興味を引くため体験用と割り切る必要はありそうです)

ただ、ある意味、楽器演奏という観点としては
【音が鳴るタイミングに拍を合わせるため早めに叩くのが正しい姿】なので、
ある意味 本質的な音ゲーコントローラなのかもしれませんね。

ゲーム以外の用途としても音検出型スイッチとして応用できないか、他にも試してみたいと思います!