LoginSignup
2
6

More than 1 year has passed since last update.

【Discord.js V14】VC1からVC2に音声をStreaming(中継)するBOTを作成する。

Last updated at Posted at 2023-01-31

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

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

BOTでVCに音楽を流す機能BOTでVCを録音する機能までの流れに関しては、別の記事にまとめてあります。

関連記事:

目次

  1. 開発環境
  2. 今回このBOTを作るに至った経緯
  3. 完成イメージ
  4. VCから1人分の音声を取得して、別のVCに流してみる
  5. 取得した音声をPCM形式にデコードして、別のVCに流してみる
  6. 複数人の音声を取得して、audio-mixerでミキシング後、別のVCに流してみる
  7. 最後に
  8. 関連記事

開発環境

OS (Windows11)
discord.js (14.7.1)
node.js (18.13.0)
npm (8.19.3)
@discordjs/voice (^0.14.0)

今回このBOTを作るに至った経緯

最近、友達とAmong Usをやる機会があり、
「会議の音声を霊界チャットに中継して聞くことができれば…」
と考え、検索してみたところ、Qiitaで以下の記事を発見。

一応2つの記事で作成されているBOTを自分の環境に入れてみたところ、
shoshoiさんのDiscordVoiceTransfer.tsに関しては、環境構築で挫折し、断念。
suzukeyさんのdiscord_transferに関しては、動きはするものの、BOTがコマンドを認識してくれず、断念。

よって、数日の間暇だったこともあり、自分で開発してみることに。

【参考にしたBOT】
suzukeyさんのdiscord_transfer
shoshoiさんのDiscordVoiceTransfer.ts

完成イメージ

image.png
※画像はsuzukeyさんの記事から引用しています。

VCから1人分の音声を取得して、別のVCに流してみる

これまで行ってきた、VCに音楽を流す機能、VCの音声を録音する機能をもとにして、
新たに、VCの音声をストリーミングする機能を作っていきます。

  • index.jsのコマンド呼び出し部分をjoin.jsstream.jsに渡す値が同じになるよう、少し変更。
	try {
        // コマンドを実行
		if (interaction.commandName === 'join' || interaction.commandName === 'stream') { // ここに追記
			connection = await command.execute(interaction, client1, client2);
		}
  • join.jsをコピーして、stream.jsを作成。
    録音機能(record.js)の音声取得部分のみをコピペ。
    音楽再生機能(play.js)の音声再生の部分のみをコピペ。
    そして改変。
const { SlashCommandBuilder, ChannelType } = require('discord.js');
const { joinVoiceChannel, createAudioPlayer, NoSubscriberBehavior, EndBehaviorType, createAudioResource, StreamType } = require('@discordjs/voice');
module.exports = {
	data: new SlashCommandBuilder()
        // コマンドの名前
		.setName('stream')
        // コマンドの説明文
		.setDescription('VCを中継。')
		// コマンドのオプションを追加
		.addChannelOption((option) =>
			option
				.setName('channel1')
				.setDescription('The channel that Listener-bot join')
				.setRequired(true)
				.addChannelTypes(ChannelType.GuildVoice),
		)
		.addChannelOption((option) =>
			option
				.setName('channel2')
				.setDescription('The channel that Speaker-bot join')
				.setRequired(true)
				.addChannelTypes(ChannelType.GuildVoice),
		),
	async execute(interaction, client1, client2) {
		const voiceChannel1 = interaction.options.getChannel('channel1');
		const voiceChannel2 = interaction.options.getChannel('channel2');
		if (voiceChannel1 && voiceChannel2) {
			if (voiceChannel1 === voiceChannel2) {
				await interaction.reply('同じVCには参加できません🥺');
				return;
			}
			// Listener-botがVCに参加する処理
			const connection1 = joinVoiceChannel({
				// なぜかはわからないが、groupの指定をしないと、先にVCに入っているBOTがVCを移動するだけになってしまうので、記述。
				group: 'listener',
				guildId: interaction.guildId,
				channelId: voiceChannel1.id,
				// どっちのBOTを動かしてあげるかの指定をしてあげる。
				adapterCreator: client1.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
				// VC参加時にマイクミュート、スピーカーミュートにするか否か
				selfMute: true,
				selfDeaf: false,
			});
			// Speaker-botがVCに参加する処理
			const connection2 = joinVoiceChannel({
				group: 'speaker',
				guildId: interaction.guildId,
				channelId: voiceChannel2.id,
				adapterCreator: client2.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
				selfMute: false,
				selfDeaf: true,
			});
			// Listener-botが参加しているVCで誰かが話し出したら実行
			connection1.receiver.speaking.on('start', (userId) => {
				// VCの音声取得機能
				const audio = connection1.receiver.subscribe(userId, {
					end: {
						behavior: EndBehaviorType.AfterSilence,
						// 100だと短過ぎるのか、エラー落ちするため1000msに設定
						duration: 1000,
					},
				});
				// 音声をVCに流す機能
				const player = createAudioPlayer({
					behaviors: {
						// 聞いている人がいなくても音声を中継してくれるように設定
						noSubscriber: NoSubscriberBehavior.play,
					},
				});
				const resource = createAudioResource(audio,
					{
						// VCから取得してきた音声はOpus型なので、Opusに設定
						inputType: StreamType.Opus,
					},
				);
				player.play(resource);
				connection2.subscribe(player);
				
			});
			await interaction.reply('VCを中継します!');
		}
		else {
			await interaction.reply('BOTを参加させるVCを指定してください!');
		}
	},
};
  • node deploy-commands.jsnode index.jsを行い、Discordでコマンド/streamを入力、実行。
    サブアカウントでSpeaker-botのいるVC、メインアカウントでListener-botのいるVCに入りしゃべると……
    (テスト用のサブアカウントは、Discord PTBにて同じPCからログインしています。)
    無事VCの音が中継されました!

取得した音声をPCM形式にデコードして、別のVCに流してみる

後述のオーディオミキサーを使う際に、
VCから取得してきたままの音声形式(Opus)ではダメなため、PCM形式にデコードします。

  • npmを使い、必要なものをインストールする。
    npm i prism-media@^1.3.4 --save

  • stream.jsの一部を改変、追記する。

const { SlashCommandBuilder, ChannelType } = require('discord.js');
const { joinVoiceChannel, createAudioPlayer, NoSubscriberBehavior, EndBehaviorType, createAudioResource, StreamType } = require('@discordjs/voice');
const Prism = require('prism-media');
const { PassThrough } = require('stream');

---(以下略)---

				// VCの音声取得機能
				const audio = connection1.receiver.subscribe(userId, {
					end: {
						behavior: EndBehaviorType.AfterSilence,
						// Opusの場合、100msだと短過ぎるのか、エラー落ちするため1000msに設定
						// Rawに変換する場合、1000msだと長過ぎるのか、エラー落ちするため100msに設定
						duration: 100,
					},
				});
				const rawStream = new PassThrough();
				audio
					.pipe(new Prism.opus.Decoder({ rate: 48000, channels: 2, frameSize: 960 }))
					.pipe(rawStream);
				// 音声をVCに流す機能
				const player = createAudioPlayer({
					behaviors: {
						// 聞いている人がいなくても音声を中継してくれるように設定
						noSubscriber: NoSubscriberBehavior.play,
					},
				});
				const resource = createAudioResource(rawStream,
					{
						// VCから取得してきた音声はOpus型なので、Opusに設定
						inputType: StreamType.Raw,
					},
---(以下略)---
  • node index.jsを行い、Discordでコマンド/streamを入力、実行。
    サブアカウントでSpeaker-botのいるVC、メインアカウントでListener-botのいるVCに入りしゃべると……
    無事VCの音が中継されました!
    Rawの時のduration: 100,と、Opusの時のduration: 1000,で設定秒数が違うのはなぜ…?
    これのせいで、めちゃくちゃ時間食った……

複数人の音声を取得して、audio-mixerでミキシング後、別のVCに流してみる

現状では、一人が話しているだけであれば、きれいに音声を中継できるものの、
複数人が同時に話すと、音声がぷつぷつになってしまい、何を言っているのか聞き取れません。
なので、複数人が同時に話した時、それぞれの音をオーディオミキサーを使い、合成して1つの音として出力します。

  • npmを使い、必要なものをインストールする。
    npm i audio-mixer --save

  • stream.jsにaudio-mixerの記述を追加していく。
    ……のだが、公式のReadmeがよくわからな過ぎたため、
    ここで冒頭で参考にしたQiita記事、shoshoiさんの記事のコードを参考に、記述を追加しました。
    これが最終的なコードです。

const { SlashCommandBuilder, ChannelType } = require('discord.js');
const { joinVoiceChannel, createAudioPlayer, NoSubscriberBehavior, EndBehaviorType, createAudioResource, StreamType } = require('@discordjs/voice');
const AudioMixer = require('audio-mixer');
const Prism = require('prism-media');
const { PassThrough } = require('stream');

module.exports = {
	data: new SlashCommandBuilder()
        // コマンドの名前
		.setName('stream')
        // コマンドの説明文
		.setDescription('VCを中継。')
		// コマンドのオプションを追加
		.addChannelOption((option) =>
			option
				.setName('channel1')
				.setDescription('The channel that Listener-bot join')
				.setRequired(true)
				.addChannelTypes(ChannelType.GuildVoice),
		)
		.addChannelOption((option) =>
			option
				.setName('channel2')
				.setDescription('The channel that Speaker-bot join')
				.setRequired(true)
				.addChannelTypes(ChannelType.GuildVoice),
		),
	async execute(interaction, client1, client2) {
		const voiceChannel1 = interaction.options.getChannel('channel1');
		const voiceChannel2 = interaction.options.getChannel('channel2');
		if (voiceChannel1 && voiceChannel2) {
			if (voiceChannel1 === voiceChannel2) {
				await interaction.reply('同じVCには参加できません🥺');
				return;
			}
			// Listener-botがVCに参加する処理
			const connection1 = joinVoiceChannel({
				// なぜかはわからないが、groupの指定をしないと、先にVCに入っているBOTがVCを移動するだけになってしまうので、記述。
				group: 'listener',
				guildId: interaction.guildId,
				channelId: voiceChannel1.id,
				// どっちのBOTを動かしてあげるかの指定をしてあげる。
				adapterCreator: client1.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
				// VC参加時にマイクミュート、スピーカーミュートにするか否か
				selfMute: true,
				selfDeaf: false,
			});
			// Speaker-botがVCに参加する処理
			const connection2 = joinVoiceChannel({
				group: 'speaker',
				guildId: interaction.guildId,
				channelId: voiceChannel2.id,
				adapterCreator: client2.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
				selfMute: false,
				selfDeaf: true,
			});
			const mixer = new AudioMixer.Mixer({
				channels: 2,
				bitDepth: 16,
				sampleRate: 48000,
				clearInterval: 250,
			});
			// Listener-botが参加しているVCで誰かが話し出したら実行
			connection1.receiver.speaking.on('start', (userId) => {
				const standaloneInput = new AudioMixer.Input({
					channels: 2,
					bitDepth: 16,
					sampleRate: 48000,
					volume: 100,
				});
				const audioMixer = mixer;
				audioMixer.addInput(standaloneInput);
				// VCの音声取得機能
				const audio = connection1.receiver.subscribe(userId, {
					end: {
						behavior: EndBehaviorType.AfterSilence,
						// Opusの場合、100msだと短過ぎるのか、エラー落ちするため1000msに設定
						// Rawに変換する場合、1000msだと長過ぎるのか、エラー落ちするため100msに設定
						duration: 100,
					},
				});
				const rawStream = new PassThrough();
				audio
					.pipe(new Prism.opus.Decoder({ rate: 48000, channels: 2, frameSize: 960 }))
					.pipe(rawStream);
				const p = rawStream.pipe(standaloneInput);
				// 音声をVCに流す機能
				const player = createAudioPlayer({
					behaviors: {
						// 聞いている人がいなくても音声を中継してくれるように設定
						noSubscriber: NoSubscriberBehavior.play,
					},
				});
				const resource = createAudioResource(mixer,
					{
						// VCから取得してきた音声はOpus型なので、Opusに設定
						inputType: StreamType.Raw,
					},
				);
				player.play(resource);
				connection2.subscribe(player);
				rawStream.on('end', () => {
					if (this.audioMixer != null) {
						this.audioMixer.removeInput(standaloneInput);
						standaloneInput.destroy();
						rawStream.destroy();
						p.destroy();
					}
				});
			});
			await interaction.reply('VCを中継します!');
		}
		else {
			await interaction.reply('BOTを参加させるVCを指定してください!');
		}
	},
};
  • node index.jsを行い、Discordでコマンド/streamを入力、実行。
    複数人でお話をしてみると、ぷつぷつ途切れずに、音がしっかりと重なって聞こえます。
    GitHubにコードを載せてあります。

最後に

暇だから何となく欲しいものを作ろうと思いやってみたのですが、ソースがほぼなく、動き出してから4日間もかかってしまいました…
AngularやSocket.IO、Dockerの勉強時よりは幾分かましでしたが、Discord.js Japan User GroupというDiscordサーバーで有識者の方に質問をできていなければ、倍以上の時間がかかっていたと思います。
質問に答えてくださった方々には感謝しかありません。

今回、初めてDiscordBotに触れて、Botのコマンドの打ち方もわからない中、開発していくうえで、いろいろと学びを得て成長していく過程は超絶楽しかったですw
この記事を見ている方も、ぜひDiscordBot開発を存分に楽しんでください。
GitHub

関連記事

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6