Spresenseで赤外線リモコン楽器を作ってみる

先週作成したリモコンデコードプログラムを応用してリモコン楽器を作って遊んでみたいと思います。

![gakki.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2374160/940a147f-e8f1-1627-6158-42d16964fdd1.png)


# 必要な材料

## リモコンデコード機材

以下のページにある機材を用意します。
[Spresenseで赤外線リモコンをデコードする](https://qiita.com/cmm614/items/7e9a0b906896045ed727)

## ソニーのリモコン

ソニー製の”赤外線”テレビリモコンであれば何でもよいです。

![_20221218_211432_mini.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2374160/2c54dbd6-0833-8c14-dc8d-9d45666417b9.jpeg)

最近のモデルはBluetooth経由で信号を飛ばしていたりするので、その場合は赤外線モードに切り替える必要があります。
設定の切り替え方法は
 1. 設定を開く
 1. `リモコンとアクセサリ` を選択
 1. `Bluetooth設定` を選択
 1. Bluetoothを無効にする
Bluetooth通信には5. で有効にすることで戻すことができます。


## ヘッドフォン

音が鳴れば何でもよいです。

# プログラムの作成

## 音階テーブル

今回は、リモコンコードから、リモコンボタンを判定し、ボタンに対応する音階を選択する必要があるので、音階テーブルを作成します。

まず、リモコンコードとリモコンボタンの関係は以下のようになります。
(実際にリモコンのボタンを押して確認しました。)

|リモコンボタン |リモコンコード |
|--------------|--------------|
|1             |0x0010        |
|2             |0x0810        |
|3             |0x0410        |
|4             |0x0C10        |
|5             |0x0210        |
|6             |0x0A10        |
|7             |0x0610        |
|8             |0x0E10        |
|9             |0x0110        |
|10            |0x0910        |
|11            |0x0510        |
|12            |0x0D10        |

次に、音階と周波数の関係は以下のようになります。
(音階周波数テーブルは [音階周波数](https://tomari.org/main/java/oto.html) のサイトを参考に作成しています。)

|音階 |周波数 |
|-----|------|
|ド   |   523|
|レ   |   587|
|ミ   |   659|
|ファ |   698|
|ソ   |   784|
|ラ   |   880|
|シ   |   988|
|ド   |  1047|
|レ   |  1175|
|ミ   |  1319|
|ファ |  1397|
|ソ   |  1568|

以上のテーブルにより、以下のようなリモコンコードと周波数のテーブルを用意すればOKです。
(例. 1が押されれば、0x0010がデコードされ、この値をテーブルから探し出せばドの周波数523が得られる寸法。)

```c++: 音階テーブル
// 周波数対応テーブル用構造体
struct score {
  uint32_t code;
  int fs;
};

// リモコンコードとBeep周波数テーブル
struct score scores[] = {
  // {<リモコンコード>, <周波数>}
     {0x0010,          523},  //  1: ド
     {0x0810,          587},  //  2: レ
     {0x0410,          659},  //  3: ミ
     {0x0C10,          698},  //  4: ファ
     {0x0210,          784},  //  5: ソ
     {0x0A10,          880},  //  6: ラ
     {0x0610,          988},  //  7: シ
     {0x0E10,         1047},  //  8: ド
     {0x0110,         1175},  //  9: レ
     {0x0910,         1319},  // 10: ミ
     {0x0510,         1397},  // 11: ファ
     {0x0D10,         1568}   // 12: ソ
};
```

上記の音階テーブルは次のように利用して、リモコンコードから周波数に変換します。

```c++: リモコンコードから周波数への変換方法
// 再生周波数(Hz, 0の場合は無音)
int beep_fz  = 0;

// デコードされたリモコンコード ir_code から再生音階を判定し状態を更新
void set_beep_fs(uint32_t ir_code) {
  int fs = 0;

  // 与えられたir_codeと一致するリモコンコードをscoresテーブルから検索
  for (int n = 0; n < sizeof(scores) / sizeof(struct score); n++) {
    if (scores[n].code == ir_code) {
      fs = scores[n].fs;
      break;
    }
  }

  // 検索結果の周波数を設定(検索できなかった場合は0=無音)
  beep_fz = fs;
}
```

## リリース判定

前回は、リモコンのボタンをデコードするタイミングを簡易化するために通信フォーマットのLeader部分(2400のHjgh)を判定したタイミングで行っていました。”押された”タイミングはわかっても、”離された”タイミングを計るようにプログラムを作成していませんでした。

[SONYフォーマット](http://elm-chan.org/docs/ir_format.html) によると、リモコンで送信される赤外線信号は45msec周期で送信されているので、”45msec以上の時間リモコンコードが送られてこない"場合リモコンが離されたと解釈することができます。(ピッタリ45msecではタイミングによってはリモコンコードを読み込む前に話したと判定されてしまうので、少し余裕を取ります。)

なので、以下のようにリモコンのLeaderを判定したタイミングでタイマーを仕掛け、それから90msecリモコンコードが送られてこなかった場合に、リリースと判定します。

```c++: リリース判定
#include <signal.h>

// ボタンの状態変数(押されている = 1, 離されている = 0)
int key_pressed = 0;

// タイマー変数
timer_t tid;

// タイマー詳細情報変数
struct itimerspec itval;

// 指定時間リモコンが押されなかった場合に呼び出される関数
// ここでは、ボタンの状態、周波数、リモコンコードをリセット
void key_released_handler(int signum) {
  if (key_pressed == 1) {
    Serial.println("Key released.\n");
  }

  key_pressed = 0;
}

// リモコンのボタンが離されたと判定されるるまでのタイマーを開始
void key_release_timer_start() {
  if(timer_settime(tid, 0, &itval, NULL) < 0) {
    Serial.println("timer_settime failed.\n");
  }
}

// タイマーの条件を設定
void set_key_release_timer() {
  struct sigaction act, oldact;

  memset(&act, 0, sizeof(struct sigaction));
  memset(&oldact, 0, sizeof(struct sigaction));

  // タイマーが発動した時に呼び出される関数の指定
  act.sa_handler = key_released_handler;
  act.sa_flags = SA_RESTART;
  if(sigaction(SIGALRM, &act, &oldact) < 0) {
    Serial.println("sigaction failed.\n");
    return;
  }

  // タイマー発動までの時間を設定
  // (SONYのリモコンは45msec周期で送信しているので、倍の時間90msecリモコンコードが来なければ離したと判定)
  itval.it_value.tv_sec = 0;
  itval.it_value.tv_nsec = 90 * 1000 * 1000;
  itval.it_interval.tv_sec = 0;
  itval.it_interval.tv_nsec = 90 * 1000 * 1000;
  
  if(timer_create(CLOCK_REALTIME, NULL, &tid) < 0) {
    Serial.println("timer_create failed.\n");
  }
}

void do_decoding(int status, int microsec) {
  // 誤差を±100usecとしてどのパターンで割り込みが来たかを判定
  if (status == 0 && 2300 < microsec && microsec < 2500) {
    // Status 0(発光)期間が4Tの場合 -> 新規コード開始の合図

    // 前回のデコード結果をここで表示する。
    if (ir_code != 0x0000) {
      ...
      
      // ボタンリリースタイマーを開始
      key_release_timer_start();
    }
  ...
}

void setup() {
  ...

  // ボタンリリースタイマーの設定
  set_key_release_timer();

  ...
}

```

## 音階再生(Beep)

音階の再生にSpresense Audioライブラリの beep 機能を使います。
今回はloop()関数で継続的にBeep音の設定を行うことでBeep音を発生させます。
リモコンボタンを押しているときは指定された周波数のBeep音を、ボタンから話した時はBeep音を消去します。

```c++: Beep
#include <Audio.h>

// ボタンの状態変数(押されている = 1, 離されている = 0)
int key_pressed = 0;

// 再生周波数(Hz, 0の場合は無音)
int beep_fz  = 0;

void setup() {
  ...

  // Audioインスタンスを取得
  theAudio = AudioClass::getInstance();

  // Audio機能を初期化
  theAudio->begin();
  puts("initialization Audio Library");

  // 出力先を設定(スピーカーモード)
  theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, 0, 0);
}

void loop() {
  // ボタンが押されているときはBeep音を鳴らし、離されている場合は無音
  if (key_pressed ==1 && beep_fz != 0) {
    // Beep音を再生
    theAudio->setBeep(1, -20, beep_fz);
  } else {
    // Beep音を消去
    theAudio->setBeep(0, 0, 0);
  } 
  usleep(100 * 1000);
}
```

## リモコン楽器プログラム

以上の追加実装内容を含めて実装すると以下のようになります。

```c++: Sony_Remote_Keyboard.ino
#include <signal.h>
#include <sys/time.h>
#include <Audio.h>

AudioClass *theAudio;

// 周波数対応テーブル用構造体
struct score {
  uint32_t code;
  int fs;
};

// 入力ピン
const int IR_PIN = 7;

// 時間測定用変数
struct timespec start_time;
struct timespec end_time;

// デコードされた値を保持
volatile uint32_t ir_code = 0x0000;

// リモコンコードとBeep周波数テーブル
struct score scores[] = {
  // {<リモコンコード>, <周波数>}
     {0x0010,          523},  //  1: ド
     {0x0810,          587},  //  2: レ
     {0x0410,          659},  //  3: ミ
     {0x0C10,          698},  //  4: ファ
     {0x0210,          784},  //  5: ソ
     {0x0A10,          880},  //  6: ラ
     {0x0610,          988},  //  7: シ
     {0x0E10,         1047},  //  8: ド
     {0x0110,         1175},  //  9: レ
     {0x0910,         1319},  // 10: ミ
     {0x0510,         1397},  // 11: ファ
     {0x0D10,         1568}   // 12: ソ
};

// ボタンの状態変数(押されている = 1, 離されている = 0)
int key_pressed = 0;

// 再生周波数(Hz, 0の場合は無音)
int beep_fz  = 0;

// タイマー変数
timer_t tid;

// タイマー詳細情報変数
struct itimerspec itval;

// 指定時間リモコンが押されなかった場合に呼び出される関数
// ここでは、ボタンの状態、周波数、リモコンコードをリセット
void key_released_handler(int signum) {
  if (key_pressed == 1) {
    Serial.println("Key released.\n");
  }

  key_pressed = 0;
  beep_fz     = 0;
  ir_code     = 0;
}

// リモコンのボタンが離されたと判定されるるまでのタイマーを開始
void key_release_timer_start() {
  if(timer_settime(tid, 0, &itval, NULL) < 0) {
    Serial.println("timer_settime failed.\n");
  }
}

// タイマーの条件を設定
void set_key_release_timer() {
  struct sigaction act, oldact;

  memset(&act, 0, sizeof(struct sigaction));
  memset(&oldact, 0, sizeof(struct sigaction));

  // タイマーが発動した時に呼び出される関数の指定
  act.sa_handler = key_released_handler;
  act.sa_flags = SA_RESTART;
  if(sigaction(SIGALRM, &act, &oldact) < 0) {
    Serial.println("sigaction failed.\n");
    return;
  }

  // タイマー発動までの時間を設定
  // (SONYのリモコンは45msec周期で送信しているので、倍の時間90msecリモコンコードが来なければ離したと判定)
  itval.it_value.tv_sec = 0;
  itval.it_value.tv_nsec = 90 * 1000 * 1000;
  itval.it_interval.tv_sec = 0;
  itval.it_interval.tv_nsec = 90 * 1000 * 1000;
  
  if(timer_create(CLOCK_REALTIME, NULL, &tid) < 0) {
    Serial.println("timer_create failed.\n");
  }
}

// デコードされたリモコンコードから再生音階を判定し状態を更新
void set_beep_fs(uint32_t ir_code) {
  int fs = 0;

  // 与えられたir_codeと一致するリモコンコードをscoresテーブルから検索
  for (int n = 0; n < sizeof(scores) / sizeof(struct score); n++) {
    if (scores[n].code == ir_code) {
      fs = scores[n].fs;
      break;
    }
  }

  // 検索結果の周波数を設定(検索できなかった場合は0=無音)
  beep_fz = fs;

  if (key_pressed == 0) {
    Serial.println("Key pressed.\n");
  }

  // ボタンが押されているので状態を更新する
  key_pressed = 1;
}

void do_decoding(int status, int microsec) {
  // 誤差を±100usecとしてどのパターンで割り込みが来たかを判定
  if (status == 0 && 2300 < microsec && microsec < 2500) {
    // Status 0(発光)期間が4Tの場合 -> 新規コード開始の合図

    // 前回のデコード結果をここで表示する。
    if (ir_code != 0x0000) {
      // 再生音階を変更
      set_beep_fs(ir_code);
      
      Serial.print("CODE: ");
      Serial.print(ir_code, HEX);
      Serial.println();
      
      // ボタンリリースタイマーを開始
      key_release_timer_start();
    }
    
    // 次のデコードに備え値を初期化
    ir_code = 0x0000;
  } else if (status == 1 && 500 < microsec && microsec < 700) {
    // Status 1(消灯)期間が1Tの場合 -> コード送信中消灯時間はこの時間のみ。特にここでやることはない。
  } else if (status == 0 && 500 < microsec && microsec < 700) {
    // Status 0(発光)期間が1Tの場合 -> 1bit の 0 が送られた合図

    // ir_codeを1bit左にシフトして今回のbit = 0を加える
    ir_code = (ir_code << 1) + 0;
  } else if (status == 0 && 1100 < microsec && microsec < 1300) {
    // Status 0(発光)期間が2Tの場合 -> 1bit の 1 が送られた合図

    // ir_codeを1bit左にシフトして今回のbit = 1を加える
    ir_code = (ir_code << 1) + 1;
  }
}

int get_microtime(struct timespec st_t, struct timespec ed_t) {
  long nanotime;

  // nanotimeの差分を計算(今回は1秒未満の時間測定なので秒は無視)
  nanotime = (ed_t.tv_nsec - st_t.tv_nsec);

  // 秒が桁上がりした場合逆転してしまうので補正
  if (nanotime < 0) {
    nanotime += 1000000000;
  }

  return (int) nanotime / 1000;
}

void isr_handler() {
  int status;
  int microtime;
  
  // 割り込みが入った時刻を取得
  clock_gettime(CLOCK_REALTIME, &end_time);

  // 割り込み信号の状態を確認
  status = digitalRead(IR_PIN);

  // 時間計測
  microtime = get_microtime(start_time, end_time);

  // デコード
  do_decoding(status, microtime);
  
  start_time = end_time;
}

void setup() {
  // デバッグポートのボーレートを設定(ログ出力の時間で時間測定がズレてしまうので高速に設定。)
  Serial.begin(2000000);
  
  // 赤外線回路から入力した信号ピンの入力設定。
  pinMode(IR_PIN, INPUT_PULLDOWN);

  // 割り込みハンドラの設定。GPIOのHigh/Lowが変更した場合ハンドラが呼ばれる。
  attachInterrupt(IR_PIN, isr_handler, CHANGE);

  // ダミーの開始時間を取得
  clock_gettime(CLOCK_REALTIME, &start_time);

  // ボタンリリースタイマーの設定
  set_key_release_timer();

  // Audio機能を設定
  theAudio = AudioClass::getInstance();
  theAudio->begin();
  puts("initialization Audio Library");
  theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, 0, 0);
}

void loop() {
  // ボタンが押されているときはBeep音を鳴らし、離されている場合は無音
  if (key_pressed ==1 && beep_fz != 0) {
    // Beep音を再生
    theAudio->setBeep(1, -20, beep_fz);
  } else {
    // Beep音を消去
    theAudio->setBeep(0, 0, 0);
  } 
  usleep(100 * 1000);
}
```

このプログラムを実行してみると以下のように音楽を楽しむことができます。

https://twitter.com/i/status/1604153026851135488

# 最後に

今回は、リモコンをデコードし音階を再生するプログラムを作成しました。
SpresenseはAudio機能も備わっており、非常に簡単に楽器を作成することができました。
数字キーで音を鳴らすだけのプログラムでしたが、ほかのボタンを音量などほかの機能として追加すればもっと面白い楽器が作成できるかもしれません。
ぜひチャレンジしていただけたら嬉しいです。

今度はリモコンを送信するプログラムを作成してみたいと思います！