Discordのミュートスイッチを作った

## 動機
コロナもあって、しばらくDiscordのボイスチャットに一日中いるのが日常になりましたが、
離席するときや、帰ってきたときなど、ミュートし忘れたり、逆にミュートを解除し忘れて話していたりすることが多いし、
毎回デスクトップの奥底からDiscordのウィンドウを発掘してきて、ミュートボタンを押すのは結構めんどくさいです。

対策として、まあ色々と方法はあるんですが、

- ショートカットの設定
    - 既存のショートカットにかぶらないようにすると結構押すのがめんどくさい感じになった
- マイク側でミュート
    - 私のマイクはボリュームしかないので0にするか戻すかっていう感じになる
        - その場合、戻すときにせっかく探求した最適なゲインがずれてしまうのがあまりうれしくないのです。
    - あと、Discordでミュート表示になっていないのに声が入っていないのもなんか嫌
        - ミュート表示は離席サインを兼ねていると思っている

というわけで（自分にとって）完璧な方法はあんまりなかったんですよね。

そこで、机に**トグルスイッチを設置する**ことにしました。

## トグルスイッチとは
これ
![](https://s3-ap-northeast-1.amazonaws.com/toolbox-prod/uploads/2013/06/15090019/c45c196cfef5b2d106b0d5ee6e4a591c.jpg)

飛行機のコックピットや船のデッキ、ラリーカーのダッシュボードとかについているイメージがあって、これを「カチッ」とするのは結構楽しくてなんかかっこいいロマンのあるやつです。

## どうやって作るか

### 構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108489/c21e747c-3415-82c5-de66-8e81e8b50668.png)
- PC
    - Discordが入っているPC
- Discord
    - デスクトップアプリ
- Node.jsスクリプト
    - Discord-RPCのライブラリを利用してミュート状態を取得・制御する。
        - DiscordのミュートON/OFF切り替えがあれば、Raspberry Pi Picoに送信。
        - Picoからトグルスイッチの切り替えを受信すれば、Discordのミュートを切り替え。
- トグルスイッチ
    - PicoにGPIOで配線されている。
- Raspberry Pi Pico
    - トグルスイッチの入力をGPIOで取得し、USBシリアル通信でPCに送信する


## 実装
### Node.jsのスクリプト
discord-rpcを使ってミュートの操作を行う部分については、以前Qiitaにまとめています。
https://qiita.com/rkunkunr/items/7a679b9cb5674cbd074f
この記事はWebサーバーとして起動し、特定のリクエストを受けると切り替えるものでしたが、今回はその部分がシリアル通信になります。
動作としては、
- シリアル通信でスイッチの変化を受信すれば、discord-rpcでスイッチに合わせてミュート状態を変更する
- discord-rpc側からミュート状態の変化イベントを受け取れば、そのミュート状態をシリアル通信で送信
というシンプルなものです。

#### スクリプト(抜粋)
```javascript
import RPC from "discord-rpc";
import { auth } from "./auth";
import type { RPCEvents } from "discord-rpc";
import { SerialPort } from "serialport";
import { readSerialPortPath } from "./util";
import { existsSync } from "fs";

const client = new RPC.Client({ transport: "ipc" });

// read and check serial path
let serialPath = "";
try {
  serialPath = readSerialPortPath();
} catch {
  throw new Error("serial.json not found");
}
if (!existsSync(serialPath)) throw new Error("serial port doesn't exists");

// init SerialPort
const serialPort = new SerialPort({
  path: serialPath,
  baudRate: 9600,
});

// When data from device
serialPort.on("data", async (d) => {
  const buffer = d as Buffer;
  const num = buffer.readUInt8();
  const char = String.fromCharCode(num);
  console.debug(`${char} = ${num}`);

  const vc = await client.getVoiceSettings();
  let mute = false;

  if (num === 49) mute = true;

  console.debug("set to ", mute);

  await client.setVoiceSettings({
    ...vc,
    mute,
  });

  console.debug("finished");
});

const VOICE_SETTINGS_UPDATE: RPCEvents = "VOICE_SETTINGS_UPDATE";

client.on(VOICE_SETTINGS_UPDATE, (data) => {
  console.debug(data.mute);
  if (data.mute) {
    serialPort.write("1");
  } else {
    serialPort.write("0");
  }
});

client.on("ready", async () => {
  // console.debug("Logged in as ", client.application.name);
  // console.debug("Authed for user ", client.user.username);
  console.debug("Ready");
  client.setActivity({
    details: "Discordミュートスイッチ稼働中",
    state: "稼働中",
    startTimestamp: new Date(),
  });

  const isMute = (await client.getVoiceSettings()).mute;
  serialPort.write(isMute ? "1" : "0");
});

async function start() {
  const accessToken = await auth(
    "{DISCORD_CLIENT_ID}",
    "{DISCORD_CLIENT_SECRET}"
  );

  await client.login({
    clientId: "{DISCORD_CLIENT_ID}",
    scopes: ["identity", "rpc", "rpc.api"],
    accessToken: accessToken,
  });

  client.subscribe("VOICE_SETTINGS_UPDATE", () => {});
}

start();

```

### Picoのスクリプト
![IMG_20220503_232116.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108489/0d0ec796-f228-1406-d589-f15f5039c1a6.jpeg)
プルダウン抵抗の付け方が結構ヤクザなのは許してください。多分ピンヘッダをつけてブレッドボードや基板に刺すほうがいいですw
今回は小さくしたかったのと、使い回す予定がなかったのでこのような形にしました。
使い回すとしても、半田を溶かせば結構ピンヘッダもはんだ付けできるものです。

プログラムはCで記述しました。
CMakeLists.txtで設定することで、標準入出力をUSBシリアルにできるため、`printf()`や`scanf()`関数で簡単にシリアル通信ができます。

組み込み系だとほぼArduinoのスケッチしか書いたことがなかったのですが、GPIOの入力をみてシリアル通信で送受信するぐらいであれば簡単に実現できました。

動作としては、
- シリアル通信で現在のミュート状態を受け取ったら、変数に格納しておく。
- スイッチ状態を監視し、ミュート状態と異なっていれば、スイッチの状態をシリアル通信で送信する。
というものです。

#### スクリプト
```c
#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"

int main() {
  stdio_init_all();

  const uint LED_PIN = PICO_DEFAULT_LED_PIN;
  const uint SWITCH_PIN = 20;
  gpio_init(LED_PIN);
  gpio_init(SWITCH_PIN);
  gpio_set_dir(LED_PIN, GPIO_OUT);
  gpio_set_dir(SWITCH_PIN, GPIO_IN);

  // true or 1 -> mute
  bool mute_state = false;
  bool switch_state = false;

  while(true) {
    if(tud_cdc_available() > 0) {
      char get_chars[1] = {'0'};
      tud_cdc_read(get_chars, 1);
      mute_state = get_chars[0] == '1';
    }

    // Set Indicator LED
    gpio_put(LED_PIN, mute_state ? 0 : 1);

    /**
     * Check switch state
     * PIN20 --> GND: switch_state == true
     * PIN20 --> +  : switch_state == false
     */
    switch_state = !gpio_get(SWITCH_PIN);
    if(switch_state != mute_state) {
      // Need to send packet
      printf("%c", switch_state ? '1' : '0');
    }
    sleep_ms(100);
  }
  return 0;
}
```

### スイッチ部分
ここは簡単で、[3Pのトグルスイッチ](https://akizukidenshi.com/catalog/g/gP-03774/)にはんだ付けで配線をして、7mm穴のトグルスイッチステーにねじ込んだだけです。
これを机の裏面にビスで固定してしまえばOKです。
（実はミュート状態を示すパイロットランプをスイッチの上につけようと思ってたんですが、ちょうどいいLEDが家になかったので保留してあります。また気が向いたときに追加したいと思います。）
![IMG_20220503_232111.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108489/3fd0cdb2-7756-7c84-3c26-36014ab2ed77.jpeg)


## 動作について
Discordのミュートは、もちろんDiscordのデスクトップアプリから操作することができます。
トグルスイッチを追加すると、操作するIFが2つになるわけですが、それぞれで同期を取る方法を考えないといけません。
つまり、トグルスイッチでミュートにしたあと、アプリからミュートを解除しようとした際の挙動を考えないとならないのです。

今回は、アプリ側のボタンを殺すことにしました。
つまり、アプリ側でミュートを解除しようとしても、トグルスイッチがミュート解除の方に倒されない限りは変更されないというわけです。

具体的には、
1. アプリ側でミュート解除
1. スクリプトでミュート状態変化イベントが発火し、シリアル通信で`0`を送信
1. Pico側で受信し、格納。
1. スイッチ状態は`1`なので、シリアル通信で`1`を送信
1. スクリプトは`1`を受信して、Discordをミュートにする

という流れですね。
トグルスイッチによってミュート状態がひと目で分かるというのもメリットだと考えているので、スイッチの状態に嘘をつかせたくなかったという意図があります。

## 完成
<iframe width="560" height="315" src="https://www.youtube.com/embed/2nEkVvBfbH8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


１週間ほど使っている感想ですが、結構便利ですね。
やはりDiscordのアプリをデスクトップの奥底から発掘するのは大変なので、作ってよかったと思っています。