Discord で音声通話ができるボットを作る

現在作りながら書いております

はじめに

使用する言語は C# ライブラリは Discord.Net です。初期設定の部分は DiscordのBotをC#で作ってみる - Qiita が参考になります。接続やログインの部分は公式サンプル Discord.Net/Program.cs at dev · discord-net/Discord.Net がそのまま使えます。

このライブラリのドキュメントは Home | Discord.Net Documentation にありますが、多くの場合リポジトリ discord-net/Discord.Net: An unofficial .Net wrapper for the Discord API (http://discordapp.com) のコードを見る方が参考になります。Discord 側の公式ドキュメント Discord Developer Portal — Documentation — Intro も参照する必要があるでしょう。

Tips

ボイスチャンネルを使うときのセットアップ

Discord は音声周りで libsodium と Opus を使っているため Sending Voice | Discord.Net Documentation にしたがって、ライブラリを導入する必要があります。これによってセキュアで低遅延で高品質な音声通話ができているんですね。

サーバー(ギルド)を取得する

Discord ではサーバーは内部的には Guild と呼ばれます。かっこいい。client は Ready になった DiscordSocketClient として以下のようにギルドを取得出来ます。なお<guild-id> は開発者 UI にしたあと、サーバー名を右クリックでコピーできます。

client.GetGuild(<guild-id>);

ボイスチャンネルを取得する

<guild-id> は開発者 UI にしたあと、チャンネル名を右クリックでコピーできます。

guild.GetChannel(<channel-id>) as SocketVoiceChannel;

もしくは guild.VoiceChannels から Enumerator<SocketVoiceChannel> で取得できるので、それを使うのが良いでしょう。

ボイスチャンネルのストリームを取得する

自分がしゃべるほうは ConnectAsync してから、CreateXXXStream() を呼べば取得出来ます。

using(var audioClient = await voiceChannel.ConnectAsync())
using(var speakStream = audioClient.CreatePCMStream(AudioApplication.Voice))
{
// しゃべります
}
await voiceChannel.DisconnectAsync();

(未確認です)Stream には種類がありますが Direct のほうは内部でタイマーを持たないそうなので、おそらく書いた物がそのまま送られてしまい、まとめて書き込むと相手側のバッファーがあふれて捨てられて音が切れる結果になってしまうでしょう。逆にタイマー付きということは、定期的にバッファーの中から少しずつデータを送っていくのだと想像します。たとえば、マイクからの入力を入れたりする場合だと Direct でよいと思いますが、音楽を流す等の場合には、タイマー付きのストリームを使うのが良いのではないかと思います。

聞く方に関しては、実体は AudioClient が持っているのですが、ここからではアクセスできません。SocketGuildUser 経由で取得します。

using(var audioClient = await voiceChannel.ConnectAsync())
{
var listenStream = voiceChannel.Users.First().AudioStream;
// 聞きます
}
await voiceChannel.DisconnectAsync();

ボイスのフォーマット

Discord Developer Portal — Documentation — Voice Connections によると

Voice data sent to discord should be encoded with Opus, using two channels (stereo) and a sample rate of 48kHz.

だそうです。48kHz のステレオのデータを Opus でエンコードして送ってあげる必要があります。

Discord.Net のドキュメント Sending Voice | Discord.Net Documentation に、FFmpeg を使ったときのサンプルがあり、それによると Discord.Net 側は PCM signed 16-bit little-endian を前提にしているようです。

話すのと録音のサンプル

exe のところに置かれた Speech.wav という Wave ファイルを再生し、その後5秒間特定のユーザーの発言を録音するサンプルです。

必要な NuGet パッケージ

  • Discord.Net

  • CSCore

  • CSCore.Ffmpeg (プレリリースにしないとでてきません)

話す音声ファイル

Speech.wav というファイルを exe とおなじところに置いてください。Visual Studio のプロジェクトに入れて、出力ディレクトリにコピーするようにすると楽です。libsodium.dll と Opus.dll も同じようにすると楽です。

その他の注意

Sending Voice | Discord.Net Documentation にしたがって、ライブラリを導入する必要があります。その際、64bit 向けの dll のため、C# のプロジェクトを 64bit 向けにする必要があります。また、.Net Core (Standard) には CSCore.Ffmpeg が対応していないため、.Net Framework を使う必要があります。

using CSCore;
using CSCore.Codecs.WAV;
using CSCore.Ffmpeg;
using Discord;
using Discord.Audio;
using Discord.WebSocket;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Ipubot
{
class Program
{
const long GuildId = <your-guild-id>;
const long ChannelId = <your-channel-id>;
const long UserId = <your-user-id>;
const string BotToken = "<your-bot-token>";
readonly WaveFormat discordFormat = new WaveFormat(48000, 16, 2, AudioEncoding.Pcm);
readonly DiscordSocketClient client;
static async Task Main() => await new Program().MainAsync();
public Program()
{
client = new DiscordSocketClient();
client.Log += LogAsync;
client.Ready += ReadyAsync;
}
public async Task MainAsync()
{
await client.LoginAsync(TokenType.Bot, BotToken);
await client.StartAsync();
await Task.Delay(-1);
}
async Task ReadyAsync()
{
Console.WriteLine($"{client.CurrentUser} is connected!");
var guild = client.GetGuild(GuildId);
var voiceChannel = (SocketVoiceChannel)guild.GetChannel(ChannelId);
var user = guild.GetUser(UserId);
try
{
using (var audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false))
{
await Speak(audioClient).ConfigureAwait(false);
await ListenTo(user, TimeSpan.FromSeconds(5f);
}
}
finally
{
await voiceChannel.DisconnectAsync().ConfigureAwait(false);
}
}
async Task Speak(IAudioClient audioClient)
{
using (var speakStream = audioClient.CreatePCMStream(AudioApplication.Voice))
using (var decoder = new FfmpegDecoder(File.OpenRead("Speech.wav")))
using (var source = decoder.ChangeSampleRate(discordFormat.SampleRate)
.ToSampleSource()
.ToStereo()
.ToWaveSource(discordFormat.BitsPerSample))
{
var bytesToSpeak = new byte[source.Length];
source.Read(bytesToSpeak, 0, (int)source.Length);
speakStream.Write(bytesToSpeak, 0, bytesToSpeak.Length);
await speakStream.FlushAsync().ConfigureAwait(false);
}
}
async Task ListenTo(SocketGuildUser user, TimeSpan listeningDuration)
{
var buffer = new byte[discordFormat.BytesPerSecond];
using (var writer = new WaveWriter("Recorded.wav", discordFormat))
{
var startedAt = DateTime.Now;
while (DateTime.Now < startedAt + listeningDuration)
{
var bytesRead = await user.AudioStream.ReadAsync(buffer, 0, buffer.Length);
writer.Write(buffer, 0, bytesRead);
}
}
}
Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
}
}