Webアプリを使った音楽の自動演奏をYouTubeでライブストリーミング配信したい（自動制御編）

### 概要

[Webアプリを使った音楽の自動演奏をYouTubeでライブストリーミング配信したい](https://qiita.com/tadfmac/items/e52ffb9da320151de3b1)の続きです。     

前回の記事では下記のような構成で、Webアプリによる自動演奏のYouTubeライブストリーミング環境を構築しました。    

![streamingSystem.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/b0ff27eb-16be-e778-c2a8-98d38122698e.png)

前回までの課題として、ユーザー操作しないと手動演奏ができない、という制約がありました。    
> いや、それ当たり前でしょ。という話ではあるんですが

この記事では、[Seeed XIAO RP2040](https://www.switch-science.com/products/7634) というRaspberry Pi Pico互換のマイコンを利用してUSBキーボト兼シリアルデバイスを構築することで、下記のようにRPA的な制御を行うことで「手動演奏」を実現します。    

> いや、だからそれ「手動演奏」じゃねーだろ！というのはひとまず置いといてw    

#### 追加する制御

- [前提条件として]ブラウザが常にキーボードの制御対象となるアプリケーションとなるようにしておく。（ブラウザ起動後Windowをクリックして前面に持ってくる等）
- シリアル通信でコマンドを受け取ると、ランダムな回数のキーイベント（キーは`a`で良い）を送出する USB HIDデバイスをXIAO RP2040で作る。
- 定期的に（1分に1度くらいのインターバルで）XIAO RP2040へシリアルコマンドを送出する node.js アプリケーションを作る。
- ブラウザにキーイベントが届き、バーチャル缶たたき機の「たたく」動作を行う。（[バーチャル缶たたき機](https://capp.mz4u.net/vc/)では、キーイベントに反応して「たたく」ボタンをクリックしたのと同じ動作をさせることができるため）

システム構成は下記のようになります。    

![streamingSystem2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/e399448c-6caa-24e1-29c1-08aa3bd09b6c.png)

ということで、今回は下記3ステップで進めます。

1. Seeed XIAO RP2040のプログラミング
2. node.js のプログラミング
3. ライブストリーミングへの反映

それでは、順に見ていきましょう。    

### 1. Seeed XIAO RP2040のプログラミング

#### USBコンポジットデバイスを作成する

今回の用途では、1本のUSBケーブルでのPCとの接続で、シリアル通信（USB-CDC）と、キーボード（USB-HID）の2種類のUSB通信が行えるデバイスを作成する必要があります。   
 
これは AVRのArduino等でも作れそうですが、USB機能が貧弱なマイコンの場合、USB-CDCとUSB-HIDが同時に利用可能なコンポジットデバイスの作成に苦労する可能性があります。    
今回利用する RP2040ベースの Seeed XIAO RP2040では、Arduino 環境でもこうしたコンポジットデバイスが簡単に作成可能です。

#### Arduino IDE

今回は Arduino 環境でプログラミングを行います。    
あらかじめ [Arduino IDE](https://www.arduino.cc/en/software) をインストールしておいてください。    

筆者はまだ2.0環境へ移行していないので、Arduino IDE 1.8.19 でテストを実施しています。   

> 現在も https://www.arduino.cc/en/software の [Legacy IDE (1.8.X)] からダウンロード可能です。 
> ![arduino.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/c001105e-a60f-62de-e65a-5c5541fa28b0.png)

#### RP2040用のArduino Coreを導入

Arduino IDEのインストールが済んだら、次に Boards Managerから RP2040用のコアを導入します。    
XIAO RP2040は Arduino IDEの標準環境ではサポートされていませんが、オープンソースで開発が進められている Arduino Coreを追加導入することで開発が可能になります。    

まず、Arduino > Tools > Board から、`Boards Manager...` を選んで Boards Managerを起動します。

![boardmanager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/17f8df42-6639-e57d-2e17-b71848a731d6.png)

表示された画面で `RP2040` と入力します。    

![boardmanager2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/173a2cce-c205-5cd3-f9a2-33cdf11ea287.png)

いくつか候補が表示されますが、`Raspberry Pi Pico/RP2040 by Earle F. Philhower, III` を選択してインストールしてください。

> 未インストールの場合、ここで `install` ボタンがアクティベートされているはずです。    
> 私は既に導入済みでしたので、スクリーンショットでは install ボタンが無効化されています。    

Boards Manager で RP2040用のCoreのインストールが終わると、Arduino > Tools > Board から Seeed XIAO RP2040が選択できるようになっています。    

![coreselection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/88d3f43e-dbac-bb1f-b7fc-ef7d517c39d4.png)

XIAO RP2040の書き込み用の設定は以下の通りです。     

![rp2040settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/fbf55d67-b5ea-b935-326a-273e282506e6.png)

#### サンプルプログラムを書き込んでみる。

今回作成したいプログラムに非常に近いサンプルプログラムが用意されています。    
こちらを書き換えて目的のプログラムに書き換えるのが早いと思いますので、まずは使ってみましょう。    

Arduino > File > Examples > Examples for Seeed XIAO RP2040 > Keyboard > Serial

を開きましょう。

すると、下記内容のスケッチが開かれます (コメント部分は省略)

```serial.ino /.c
#include "Keyboard.h"

void setup() {
  // open the serial port:
  Serial.begin(9600);
  // initialize control over the keyboard:
  Keyboard.begin();
}

void loop() {
  // check for incoming serial data:
  if (Serial.available() > 0) {
    // read incoming serial data:
    char inChar = Serial.read();
    // Type the next ASCII value from what you received:
    Keyboard.write(inChar + 1);
  }
}
```

##### RP2040を書き込みモードにする。

XIAO RP2040の Bootスイッチ（`B`と刻印のあるタクトスイッチ）を押しながら、PCとUSBケーブルで接続すると、RP2040はマスストレージモードで起動します。

この状態で、Arduinoの`Upload`を実行すると、プログラムを書き込んだのち、自動的にリセットがかかって書き込んだスケッチの機能で起動します。

と、文字で書くのは簡単なのですが、**Bootスイッチ（`B`と刻印のあるタクトスイッチ）を押しながら、PCとUSBケーブルで接続** 
というのが、以外に難しい操作です。やってみればわかりますが、なんか手が2本だと足りない感覚になります。。。。。    

スイッチ付きのハブがあればかなり操作が楽になりますので、**導入を強くおススメします！**（USBケーブルを抜き差しではなくハブのスイッチのON/OFFだけでできるよになる）

![hub2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/6730f3ae-fc8c-73b1-387d-c811790af0da.png)

##### サンプルプログラムの動作確認

XIAO RP2040へサンプルプログラムの書き込みができたので、Ardiono IDEのシリアルモニタを使って動作確認してみましょう。
最初に、XIAO RP2040のシリアルポートを選びます。

Arduino > Tools > Port で、XIAO RP2040のポートを選びます。

![port.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/294d1a06-dfb7-4f40-b688-3b3cab2e1ead.png)

次に、Arduino IDEの Window の上部アイコンが並んでいる中の一番右の虫眼鏡のようなアイコンをクリックするとシリアルモニターが起動します。

![monitor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/437e06a2-4548-c24c-a276-6747f0b21c28.png)

このコンソールの上部の入力ボックスに、`a`と入力後、`enter`キーを押すと、そのまま`a`が `b`に変わります。
`enter`を押し続けると、`c`→`d`→`e` と、どんどん文字が変わっていく様子がわかります。

どうやらうまく動いているようです。

サンプルプログラムは、下記のような動作となっています。

1. `if (Serial.available() > 0) {` で、シリアル入力を監視している。`0`以上になった時のみ下記処理を実施する。
2. `char inChar = Serial.read();` で 送信された文字コードをASCIIコードとして読み取っている。
3. `Keyboard.write(inChar + 1);` で、ASCIIコードとして読み取った文字コードに1を加えたコードをキーボードのキーPUSHとして送信している。

ASCIIコードに足したりは必要ありませんが、要するにシリアルからデータを受け取った時にキーイベントを送信するプログラムはこのコードで既に実現できています。

#### サンプルコードを少し書き換える

仕様は下記ですので、その内容にスケッチを書き換えます。

仕様：    

- シリアルイベントを受け取ると、ランダムな回数 `a` を送出する。ただし、1度のシリアルイベントで何文字送られて来ても、キーボードの送信処理トリガーは1回のみとする。
- シリアル通信のボーレートは 115200bps とする

```serial.ino / .c
#include "Keyboard.h"

void setup() {
  // open the serial port:
  Serial.begin(115200);
  // initialize control over the keyboard:
  Keyboard.begin();

  randomSeed(analogRead(0));
}

void loop() {
  // check for incoming serial data:
  if (Serial.available() > 0) {
    // read incoming serial data:
    char inChar = Serial.read();

    int times = (int)random(4, 32);
    int cnt;

    for(cnt=0;cnt<times;cnt++){
      Keyboard.write('a');
      int duration = (int)random(1,11);

      delay(30*duration);
    }
    while(Serial.available() > 0){
      char inChar = Serial.read();
    }
  }
}
```

上記スケッチをXIAO RP2040に書き込んだ後に、シリアルモニタから動かしてみてください。
文字を送信すると、ランダムなたくさんの `a` が、多少タイミンングも揺れながら表示されると思います。

### 2. node.js のプログラミング

続けて node.js 側で、定期的（プログラムの起動後最初の送信を1分後とし、その後1分間隔で1回ずつ）に、シリアルポートに`a`を送信するプログラムを作成してみましょう。

#### node.js のインストール

node.js を ubuntu 22.04 で作成した配信用PCにインストールします。

下記手順でインストールしました。

[How to install Node.js via binary archive on Linux?](https://github.com/nodejs/help/wiki/Installation#how-to-install-nodejs-via-binary-archive-on-linux)

特に難しい点はなかったので、リンクのみで失礼します。    

#### node.js を sudo で実行可能にする。

ubuntu 22.04ではシリアルポートへの書き込みは root権限が必要なので、sudo 付きで node.js を実行できるように設定します。

下記サイトで詳しく実施方法が解説されていますので、いずれかの方法で対応します。

https://penpen-dev.com/blog/sudo-node-commandnotfound/

#### XIAO RP2040の VID/PIDを調べておく

配信用PC（Ubuntu 22.04）のUSBポートにXIAO RP2040を接続して VID/PIDを調べておきます。    

USBポートへXIAO RP2040を接続後、下記コマンドを入力します。

```
lsusb
```

すると、下記のように結果が表示されますので、VID/PIDをメモしておきます。

![vidpid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/5e8c5dff-a78b-27ec-7544-f8b7b92146b1.png)

#### アプリケーションを作成して実行する。

適当にアプリケーション用のフォルダを作成します。    

```
cd ~
mkdir serialkey
cd serialkey
```

今回は `serialkey` というフォルダを作成しました。

続けて `~/serialkey` 配下に下記2つのファイルを作成してください。

```app.mjs / .js
import { SerialPort } from 'serialport';
import { ByteLengthParser } from 'serialport';

const TARGET_VID = "2e8a";
const TARGET_PID = "800a";
const BAUDRATE = 115200;
const INTERVAL_TIME = 30*1000;

async function selectPort(){
  let idx = null;
  let port = null;
  let ports = await SerialPort.list();
  for(let cnt=0;cnt<ports.length;cnt ++){
    console.log("device["+cnt+"] vid="+ports[cnt].vendorId+" pid="+ports[cnt].productId);
    if((ports[cnt].vendorId == undefined)||(ports[cnt].productId == undefined)){
      continue;
    }
    if((ports[cnt].vendorId.toLowerCase() == TARGET_VID)&&
       (ports[cnt].productId.toLowerCase() == TARGET_PID)){
      idx = cnt;
      console.log("device found="+cnt+" path="+ports[cnt].path);
      break;
    }
  }
  if(idx == null){
    console.log("device not found.");
  }else{
    console.log("selectPort OK");
    const _path = ports[idx].path;
    port = new SerialPort({path:_path, baudRate:BAUDRATE, autoOpen:false});
  }
  return port;
}

setInterval(async ()=>{
  let port = await selectPort();
  if(port != null){
    await sendCommand(port, 'a');
  }
},INTERVAL_TIME);

function sendCommand(port, command){
  return new Promise((resolve)=>{
    if(port != null){
      port.open((err)=>{
        if(err){
          console.log("sendCommand Open Error = "+err.message);
          resolve();
        }else{
          console.log("sendCommand open OK.");
          port.write(command,async (err)=>{
            if(err){
              console.log("sendCommand send error = "+err.message);
            }else{
              console.log("sendCommand send completed. command="+command;
            }
            await port.close();
            resolve();
          });
        }
      });
    }else{
      console.log("sendCommand() port not found");
      resolve();
    }
  });
}
```

```package.json
{
  "name": "serialkey",
  "version": "1.0.0",
  "description": "serialkey server",
  "type": "module",
  "main": "app.mjs",
  "scripts": {
    "start": "node ./app.mjs"
  },
  "author": "tadfmac",
  "license": "MIT",
  "dependencies": {
    "@serialport/list": "^12.0.0",
    "serialport": "^12.0.0"
  }
}
```

`app.mjs`の下記部分は さきほど`lsusb`で調べた VID/PIDの値に書き換えておいてください。

```.js
const TARGET_VID = "2e8a"; // <- 書き換える
const TARGET_PID = "800a"; // <- 書き換える
```

上記2つのファイルが作成できたら、下記コマンドで実行します。

```
npm i
sudo npm start
```

定期的に `device not found.` というメッセージがコンソールに表示されたら動作してそうです。    

##### プログラムの解説

少しプログラムの解説をしておきます。   
このプログラムでは、[serialport](https://serialport.io/docs/api-serialport) というライブラリを使ってシリアルポートの制御を実施します。    

`selectPort()`関数では、システムのシリアルポートの一覧を取得し、USB VIDが `TARGET_VID` かつ USB PIDが `TARGET_PID` の組み合わせのポートを探します。これは XIAO RP2040 の VID/PID です。     

`setInterval()`で定期的に `selectPort()`を呼び出して、XIAO RP2040が見つかると、`sendCommand()` 関数でシリアルポートにコマンドを発行してます。

先ほど作成した XIAO RP2040はシリアルポートになんらかのデータが届くとキーイベントを発行する仕組みとなっていますので、これで定期的にキーイベントが発行できる、という仕組みとなります。

#### ブラウザを起動して動作確認する。

ブラウザを起動して、[バーチャル缶たたき機のサイト（https://capp.mz4u.net/vc）](https://capp.mz4u.net/vc) にアクセスします。   
その状態（ブラウザのWindowにフォーカスが当たっている状態）で、USBポートに先ほどプログラムを書き込んだ XIAO RP2040を接続します。

しばらくして、下記のように「たたく」を押した時と同じような画面に変わり、缶の音が鳴れば成功です！

![tataku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31758/19944958-8a28-72e4-bb01-bc63d3b83ea7.png)

テストがうまくいったので、一旦 USBポートから XIAO RP2040を外しておきましょう。（この時も紹介したスイッチ付きハブを利用すると便利です）

### 3. ライブストリーミングへの反映

OBS Studio を起動して、新たなストリーミング配信を開始します。
前回からブラウザのWindowサイズ等を変更している際は、配信領域の位置合わせを忘れずに。

実施方法は [前回記事](https://qiita.com/tadfmac/items/e52ffb9da320151de3b1) を参照ください。    

配信開始後、もういちど USBポートに XIAO RP2040を接続すれば、自動化された「手動演奏」付きのライブ配信になります。

[配信の様子](https://www.youtube.com/watch?v=FVOEEsTCcHw)

### まとめ

Arduino 環境で簡単にUSB-CDCとUSB-HID(キーボード)のコンポジットデバイスが作成できる Seeed XIAO RP2040 と node.js を使ったシンプルなRPAの方法を紹介しました。

みなさまの何かのヒントになれば幸いです！    

##### オマケ

- [筆者が使ってるスイッチつきUSBハブ](https://www.amazon.co.jp/Sabrent-USB-%E3%83%96%E3%83%AB%E3%83%BCLED%E3%83%8F%E3%83%96-4%E3%83%9D%E3%83%BC%E3%83%88%E3%80%81%E5%80%8B%E5%88%A5%E3%82%B9%E3%82%A4%E3%83%83%E3%83%81%E4%BB%98%E3%81%8D-HB-UM43/dp/B00JX1ZS5O/ref=asc_df_B00JX1ZS5O/) (写真に写したUSB2.0仕様のものとは別のモノですが、こちらも使ってます。ケーブルが長くてUSB3対応)