皆さん初めまして。Morichanと申します。
下記の※の記述を理解したうえで、記事を読んでください。
※DiscordBot開発初心者の書いた記事です。
(また、プログラミング自体、2年ほどのブランクあり)
BOTでVCに音楽を流す機能やBOTでVCを録音する機能までの流れに関しては、別の記事にまとめてあります。
関連記事:
- 【Windows11】Discord BOTを開発するための開発環境を整える。
- 【Discord.js V14】Discord BOTにスラッシュコマンドを実装してみる
- 【Discord.js V14】Discord BOTをVCに参加させてみる
- 【Discord.js V14】2つのBOTを別々のVCに参加させてみる
- 【Discord.js V14】スラッシュコマンドのオプションで、既に選択済みの選択肢を2つ目の引数のリストから消す。
- 【Discord.js V14】Discord BOTにVCに音楽を流す機能を実装してみる
- 【Discord.js V14】Discord BOTにVCから1人分の音声を取得して、録音する機能を実装してみる
目次
- 開発環境
- 今回このBOTを作るに至った経緯
- 完成イメージ
- VCから1人分の音声を取得して、別のVCに流してみる
- 取得した音声をPCM形式にデコードして、別のVCに流してみる
- 複数人の音声を取得して、audio-mixerでミキシング後、別のVCに流してみる
- 最後に
- 関連記事
開発環境
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
完成イメージ
※画像はsuzukeyさんの記事から引用しています。
VCから1人分の音声を取得して、別のVCに流してみる
これまで行ってきた、VCに音楽を流す機能、VCの音声を録音する機能をもとにして、
新たに、VCの音声をストリーミングする機能を作っていきます。
-
index.js
のコマンド呼び出し部分をjoin.js
とstream.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.js
、node 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
関連記事
- 【Windows11】Discord BOTを開発するための開発環境を整える。
- 【Discord.js V14】Discord BOTにスラッシュコマンドを実装してみる
- 【Discord.js V14】Discord BOTをVCに参加させてみる
- 【Discord.js V14】2つのBOTを別々のVCに参加させてみる
- 【Discord.js V14】スラッシュコマンドのオプションで、既に選択済みの選択肢を2つ目の引数のリストから消す。
- 【Discord.js V14】Discord BOTにVCに音楽を流す機能を実装してみる
- 【Discord.js V14】Discord BOTにVCから1人分の音声を取得して、録音する機能を実装してみる
- 【Discord.js V14】VC1からVC2に音声をStreaming(中継)するBOTを作成する。
- 【Discord.js V14】取得するVCの音声をロールによって判別する