【Discord.js V14】Discord BOTにVCに音楽を流す機能を実装してみる

皆さん初めまして。Morichanと申します。
下記の※の記述を理解したうえで、記事を読んでください。

※DiscordBot開発初心者の書いた記事です。
（また、プログラミング自体、２年ほどのブランクあり）

BOTをVCに参加させるまでの流れに関しては、[別の記事](https://qiita.com/Mori-chan/items/2df4ad15adc79cf2a167)にまとめてあります。

関連記事：
* [【Windows11】Discord BOTを開発するための開発環境を整える。](https://qiita.com/Mori-chan/items/add6f63b04f70ff53732)
* [【Discord.js V14】Discord BOTにスラッシュコマンドを実装してみる](https://qiita.com/Mori-chan/items/2e869c0e2060b78f0380)
* [【Discord.js V14】Discord BOTをVCに参加させてみる](https://qiita.com/Mori-chan/items/2df4ad15adc79cf2a167)
* [【Discord.js V14】２つのBOTを別々のVCに参加させてみる](https://qiita.com/Mori-chan/items/7d598933753d0abe1b91)
* [【Discord.js V14】スラッシュコマンドのオプションで、既に選択済みの選択肢を２つ目の引数のリストから消す。](https://qiita.com/Mori-chan/items/89ad3245443f1e8ace07)

## 目次
1. [開発環境](#開発環境)
2. [VCに音楽を流す機能を実装してみる](#vcに音楽を流す機能を実装してみる)
3. [関連記事](#関連記事)


## 開発環境
OS (Windows11)
discord.js (14.7.1)
node.js (18.13.0)
npm (8.19.3)
@discordjs/voice (^0.14.0)



## VCに音楽を流す機能を実装してみる
Speaker-botを使って、VCに音楽を流してみます。

* `index.js`内部のBOT(今回はSpeaker-bot)の記述を改変する。
```
// GatewayIntentBits.GuildVoiceStatesを追加しないと、VCに入ってから音を取得したり流したりなどの処理ができない。
const client2 = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
```

* `join.js`から、`return connection2;`でVCの情報を受け渡すコードを追記。
```
			await interaction.reply('VCに参加しました！');
			return connection2;
		}
		else {
			await interaction.reply('BOTを参加させるVCを指定してください！');
		}
	},
};
```
* `index.js`でコマンドの判定を行い、commandの値が、
joinの時、`connection = await command.execute(interaction, client1, client2);`で受け取り、
playの時、`await command.execute(interaction, connection);`で`play.js`に値を受け渡す。

```
let connection = null;

---（以下略）---

	try {
        // コマンドを実行
		if (interaction.commandName === 'join') {
			connection = await command.execute(interaction, client1, client2);
		}
		else if (interaction.commandName === 'play') {
			await command.execute(interaction, connection);
		}
		else {
			await command.execute(interaction);
		}
	} catch (error) {
		console.error(error);
		await interaction.reply({ content: 'コマンドを実行中にエラーが発生しました。', ephemeral: true });
	}
});

---（以下略）---
```

* VSCordのターミナルで、[参考資料](https://scrapbox.io/discordjs-japan/@discordjs%2Fvoice%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E9%9F%B3%E5%A3%B0%E3%82%92%E5%86%8D%E7%94%9F%E3%81%99%E3%82%8B)をもとに、
`npm i discord.js @discordjs/voice @discordjs/opus sodium`または、`npm i discord.js @discordjs/voice @discordjs/opus tweetnacl`を実行。
※Windowsの場合、前者だとエラーが出る恐れがあるらしいので、その際は後者を実行。

* 新しく`play.js`を作成。[参考資料](https://scrapbox.io/discordjs-japan/@discordjs%2Fvoice%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E9%9F%B3%E5%A3%B0%E3%82%92%E5%86%8D%E7%94%9F%E3%81%99%E3%82%8B)をもとに、中身を記述。
正直、下のほうの記述は、`player.play(resource);`と`connection.subscribe(player);`さえあれば動きます。
```
const { createAudioPlayer, NoSubscriberBehavior, createAudioResource, StreamType, AudioPlayerStatus, VoiceConnectionStatus, entersState } = require('@discordjs/voice');
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
	data: new SlashCommandBuilder()
        // コマンドの名前
		.setName('play')
        // コマンドの説明文
		.setDescription('VCで音楽を流します。'),
	async execute(interaction, connection) {
		const resource = createAudioResource(
			'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
			{
                // OPUS型で渡す時はArbitrary
				inputType: StreamType.Arbitrary,
			},
		);
		const player = createAudioPlayer({
			behaviors: {
				noSubscriber: NoSubscriberBehavior.Pause,
			},
		});
		player.play(resource);
        // (以下)エラーが出て音声が無事に流れなかった場合、動いているかの確認を取りやすくするために記述。
		const status = ['●Loading Sounds...', '●Connecting to VC...'];
		const p = interaction.reply(status.join('\n'));
		const promises = [];
		promises.push(entersState(player, AudioPlayerStatus.AutoPaused, 1000 * 10).then(() => status[0] += 'Done!'));
		promises.push(entersState(connection, VoiceConnectionStatus.Ready, 1000 * 10).then(() => status[1] += 'Done!'));
		await Promise.race(promises);
		await p;
		await Promise.all([...promises, interaction.editReply(status.join('\n'))]);
		connection.subscribe(player);
		await entersState(player, AudioPlayerStatus.Playing, 100);
		await interaction.editReply('Playing'); // これは必須。
		await entersState(player, AudioPlayerStatus.Idle, 2 ** 31 - 1);
		await interaction.editReply('End');
	},
};
```
* `node deploy-commands.js`、`node index.js`を行い、Discordでコマンド`/join`と`/play`を入力。
ヘッドホンをつけてSpeaker-botのいるVCに
無事音楽が流れました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3109638/773e98e8-cab5-a7a2-2c40-ad68a498f3f8.png)

* ルートディレクトリに再生したいmp3ファイルがある場合は、
`'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'`の部分を
`join(__dirname, '../001.mp3')`のように書き換える。

## 関連記事
* [【Windows11】Discord BOTを開発するための開発環境を整える。](https://qiita.com/Mori-chan/items/add6f63b04f70ff53732)
* [【Discord.js V14】Discord BOTにスラッシュコマンドを実装してみる](https://qiita.com/Mori-chan/items/2e869c0e2060b78f0380)
* [【Discord.js V14】Discord BOTをVCに参加させてみる](https://qiita.com/Mori-chan/items/2df4ad15adc79cf2a167)
* [【Discord.js V14】２つのBOTを別々のVCに参加させてみる](https://qiita.com/Mori-chan/items/7d598933753d0abe1b91)
* [【Discord.js V14】スラッシュコマンドのオプションで、既に選択済みの選択肢を２つ目の引数のリストから消す。](https://qiita.com/Mori-chan/items/89ad3245443f1e8ace07)
* 【Discord.js V14】Discord BOTにVCに音楽を流す機能を実装してみる
* [【Discord.js V14】Discord BOTにVCから１人分の音声を取得して、録音する機能を実装してみる](https://qiita.com/Mori-chan/items/8fcb1770c4d0020a8284)
* [【Discord.js V14】VC１からVC２に音声をStreaming（中継）するBOTを作成する。](https://qiita.com/Mori-chan/items/e9ced87c0d68a9eb2a9a)
* [【Discord.js V14】取得するVCの音声をロールによって判別する](https://qiita.com/Mori-chan/items/41cd914bb9a93d930bff)