【@discord.js/voice】VCにmp3音声を流す【TypeScript】

## イントロダクション
本記事では、[Discord](https://discord.com/)のBotをVoice Channel（VC）に参加させ、**VC内でローカルに用意したmp3ファイルを再生する**プログラムについて記述しています。なお、Discord.jsのバージョンはv14です。

## 本記事の対象者
 * DiscordBotを動作させたことがある。
 * DiscordBotを利用してVC上に音声を再生したい。

## 筆者環境
| 環境 | Version | 備考 |
| --- | --- | --- |
| Node.js | v16.18.0 |
| TypeScript | v4.9.5 |
| discord.js | v14.7.1 | 基本パッケージ |
| dotenv | v16.0.3 | Botトークン保存用 |
| @discordjs/voice | v0.14.0 | VCを扱うパッケージ |
| ffmpeg-static | v5.1.0 | @discordjs/voiceの依存 |
| tweetnacl | v1.0.3 | @discordjs/voiceの依存 |
| Visual Studio Code | v1.75.1 | |

本記事のBotは以下のようなファイル構造を保持しているとします。
```
.
├─ build
|  ├─ sounds
|　|  └─ melody.mp3
|  └─ main.js
|
├─ node_modules
|  └─ etc.
├─ src
|  └─ main.ts
├─ .env
└─ etc.
```

## モジュールのインストール
以下の記事でdiscord.js+TypeScriptの導入方法について丁寧に説明して下さっています。

https://zenn.dev/alliana_ab2m/articles/how-to-write-discord-bot-in-typescript

なお本記事のファイル/フォルダの構成や、コマンド等については上述の記事を参考にしています。

初めに基本となる`discord.js`と、`dotenv`を導入します。
```:cmd
npm install discord.js dotenv
```

次にVCで音声を再生するために必要なパッケージを導入します。
```:cmd
npm install @discordjs/voice ffmpeg-static　tweetnacl
```

## プログラムの説明
VCのあるサーバーにおいて、任意のテキストチャンネルで
```:discord
!play
```
と送信するとBotがVCに参加し、mp3音源を再生します。再生が終了するとBotはVCから退出します。
mp3ファイルは./build/soundsフォルダに格納します。

:::note warn
`!play`を送信する前にVCには最低一人が参加している状態としてください。
本プログラムでは誰もいないVCにBotは参加できません。
:::

## プログラム本体

```typescript:main.ts
import fs from 'node:fs'
import path from 'node:path'
import { Client, Message, Events, GatewayIntentBits } from 'discord.js'
import { entersState, AudioPlayerStatus, createAudioPlayer, createAudioResource, joinVoiceChannel, StreamType, NoSubscriberBehavior } from '@discordjs/voice'
import dotenv from 'dotenv'

dotenv.config()

const soundPath: string = path.join(__dirname, 'sounds');
const soundFiles: string[] = fs.readdirSync(soundPath).filter(file => file.endsWith('.mp3'));
let soundFilePath: { [key: string]: string } = {};

for (const file of soundFiles) {
    const extractExtension: string = file.split('.')[0];
    const filePath: string = path.join(soundPath, file);
    soundFilePath[extractExtension] = filePath;
}

const client = new Client({
    intents: [  GatewayIntentBits.Guilds,
                GatewayIntentBits.GuildMessages,
                GatewayIntentBits.MessageContent,
                GatewayIntentBits.GuildVoiceStates
            ]
});

client.once(Events.ClientReady, (c: Client) => {
    console.log(`Ready! Logged in as ${c.user?.tag}`);
});

client.on(Events.MessageCreate, async (message: Message) => {
    if (message.author.bot) {
        return;
    }

    if (!message.content.startsWith('!play')) {
        return;
    }

    const channel = message.member?.voice.channel;
    if (!channel) {
        await message.reply('[ERR] Someone needs to be on the voice channel.');
        return;
    }

     const connection = joinVoiceChannel({
      adapterCreator: channel.guild.voiceAdapterCreator,
      channelId: channel.id,
      guildId: channel.guild.id,
      selfDeaf: true,
      selfMute: false,
     });
    
    const player = createAudioPlayer({
        behaviors: {
            noSubscriber: NoSubscriberBehavior.Pause,
        },
    });

    connection.subscribe(player);

    const resource = createAudioResource(soundFilePath['melody'],
        {
        inputType: StreamType.Arbitrary,
        }
    );

    player.play(resource);

    await entersState(player,AudioPlayerStatus.Playing, 10 * 1000);
    await entersState(player, AudioPlayerStatus.Idle, 24 * 60 * 60 * 1000);
    
    connection.destroy();
});

client.login(process.env.TOKEN);
```

```:.env
TOKEN='YOUR_BOT_TOKEN'
```
## Botの実行
先述の[参考記事](https://zenn.dev/alliana_ab2m/articles/how-to-write-discord-bot-in-typescript)の通りであれば、次に示す`npm run`コマンドを利用してコンパイルします。

:::note warn
ファイル名やフォルダの名前は適宜読み替えてください。
:::

```:cmd
npm run compile
```

または、

```:cmd
tsc -p .
```

コンパイルが完了したら以下のコマンドでBotを実行します。
```:cmd
npm run start
```

または

```:cmd
node ./build/main.js
```
Botを起動できたら、VCに参加して、任意のテキストチャンネルで`!play`と入力し送信してください。正しく動作していれば、Botがあなたが参加しているVCに参加し、指定した音声を再生します。再生が終わると退出します。

:::note info
音声が再生されている途中に`!play`を送信すると再生状態が上書きされます。すなわち再生される音声は同じものがまた最初から再生される動作となります。
:::

:::note warn
Botが正しく動作しない場合、BotがVCから退出しないことがあります。その場合はdiscordから手動にて退出を行ってください。PC版であれば、VCチャンネルのBotを右クリックすると退出させる操作が可能です。ただしサーバーの管理者権限が必要なことに注意してください。
:::

:::note warn
BotがVCに参加して、一瞬で退出する場合は、再生したいmp3ファイルのパス指定がうまくいっていない可能性があります。`console.log()`等を利用して`createAudioResource()`の第一引数に正しくパスが指定されているか確認してみてください。
:::

## まとめ
本記事ではDiscordBotでmp3ファイルを再生するプログラムを紹介しました。今回はローカルに用意したmp3ファイルを使用しましたが、インターネット上のファイルや、Youtubeといった動画共有サービスの動画音声を再生することも可能です。こちらについては読者に検索いただきたい。

## 参考になるリンク等
・**@discordjs/voice Documentation**

https://discord.js.org/#/docs/voice/main/general/welcome

・**Youtubeの音源を再生する方法**

https://scrapbox.io/discordjs-japan/ytdl-core_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6_YouTube_%E3%81%AE%E9%9F%B3%E6%BA%90%E3%82%92%E9%85%8D%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB