Discord音声送受信+AI Server構想の変更
結論から申し上げますとDiscordの音声の受信がうまくいきません。音声送信は問題なくできます。Discord.netは受信すらできなかったのですがDsharpplusに変更してから受信はできるようになりました。ただし正しく受信ができないと言った方がいいんでしょうかロボット音声というレベルではないですね。もしかしたらwav変換でしくじっているのかと思い、pcm生データ+Audacityで再生を試みるもwavと同じ音でした^^;
Discord Botが受信した筆者の声
この件を攻略するのは、かなり時間がかかるか諦めるかのどちらかになりそうです。また、Whisperの幻聴と処理速度の問題も重なりAI Server構想を暫定案で進めることにしました。
Discordを使う前提であれば音声文字入力が使えることがわかりました。外出先ではiPhoneの音声コントロールを、自宅ではChrome+Voice Inを使って音声を文字に変えて入力します。これが思った以上に精度とレスポンスがいいことがわかりました。ただし送信ボタンを押す時にiPhoneなら「ENTERキーを押す」、Voice Inなら「確定」と言わないといけないことが少し気になります。まあ、暫定ということで・・・といいながら慣れてこのまま行きそうな予感。
Discordへ文字チャットで送信して、VoiceチャンネルがアクティブならAIが音声で返答してくるプログラムはすでに完成しており、現在テスト中です。
その前に、Discordの音声送受信のプログラムを載せておきますので、もしこの記事を見て解決策をお持ちの方はぜひコメントで教えてください。m(_ _)m
筆者のプログラミングの能力は、ここまでです^^;
テスト用でコードは汚いです。動かしたい方は修正してくださいね。
Nuget情報
Discord送受信テストプログラム概要
Discordの音声チャンネルにユーザーが入るとBotが参加し、TestDataというディレクトリにあるtest01.wavを送信する。
ユーザーが、話すとTestDataというディレクトリにSSRCの名前でwavファイルとpcm生データを作成する。
注意:音声受信はファイアウォールが邪魔しますので「ファイアウォールによるアプリケーションの許可」に当プログラムを登録してあげてください。ルーターは問題なさげです(テザリングのテストも同じ結果だったため)。
※ffmpegを使っています。パスが通った場所に置くかEXEと同じ場所にffmpeg.exeとlibsodium.dllを置いてください。
ffmpegダウンロード⇒ffmpeg公式
using DSharpPlus;
using DSharpPlus.EventArgs;
using DSharpPlus.VoiceNext;
using DSharpPlus.VoiceNext.EventArgs;
using System.Diagnostics;
using System.Collections.Concurrent;
namespace VoiceNextBot
{
class Program
{
private static ConcurrentDictionary<uint, Process>? ffmpegs;
private static FileStream fs = File.Create(Environment.GetEnvironmentVariable("TESTDATA", System.EnvironmentVariableTarget.User) + "recivevoice.pcm");
static async Task Main(string[] args)
{
var discord = new DiscordClient(new DiscordConfiguration
{
Token = Environment.GetEnvironmentVariable("DISCORD_TOKEN", System.EnvironmentVariableTarget.User),
TokenType = TokenType.Bot,
Intents = DiscordIntents.AllUnprivileged | DiscordIntents.GuildVoiceStates
});
var vnext = discord.UseVoiceNext(new VoiceNextConfiguration
{
EnableIncoming = true
});
discord.Ready += OnClientReady;
discord.GuildAvailable += OnGuildAvailable;
discord.VoiceStateUpdated += OnVoiceStateUpdated;
await discord.ConnectAsync();
await Task.Delay(-1);
}
private static Task OnClientReady(DiscordClient client, ReadyEventArgs e)
{
Console.WriteLine("Bot is ready!");
return Task.CompletedTask;
}
private static Task OnGuildAvailable(DiscordClient client, GuildCreateEventArgs e)
{
Console.WriteLine($"Guild available: {e.Guild.Name}");
return Task.CompletedTask;
}
private static async Task OnVoiceStateUpdated(DiscordClient client, VoiceStateUpdateEventArgs e)
{
if (e.After.Channel != null && e.After.User != client.CurrentUser)
{
Console.WriteLine($"{e.After.User.Username} joined voice channel {e.After.Channel.Name}");
var vnext = client.GetVoiceNext();
var connection = await vnext.ConnectAsync(e.After.Channel);
await SendAsync(connection, Environment.GetEnvironmentVariable("TESTDATA", System.EnvironmentVariableTarget.User) + "test01.wav");
ffmpegs = new ConcurrentDictionary<uint, Process>();
connection.VoiceReceived += OnVoiceReceived;
}
}
public static async Task OnVoiceReceived(VoiceNextConnection vnc, VoiceReceiveEventArgs e)
{
if (e.User is null) return;
if (e.User.IsBot) return;
string TestDir = Environment.GetEnvironmentVariable("TESTDATA", System.EnvironmentVariableTarget.User);
if (!ffmpegs.ContainsKey(e.SSRC))
{
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-f s16le -ar 48000 -i pipe:0 -ar 44100 {TestDir}{e.SSRC}.wav",
RedirectStandardInput = true
};
ffmpegs.TryAdd(e.SSRC, Process.Start(psi));
}
var ffmpeg = ffmpegs[e.SSRC];
await ffmpeg.StandardInput.BaseStream.WriteAsync(e.PcmData.ToArray(), 0, e.PcmData.Length);
await ffmpeg.StandardInput.BaseStream.FlushAsync();
fs.Write(e.PcmData.ToArray(),0,e.PcmData.Length);
}
private static async Task SendAsync(VoiceNextConnection connection, string path)
{
var transmit = connection.GetTransmitSink();
var pcm = ConvertAudioToPcm(path);
await pcm.CopyToAsync(transmit);
await pcm.DisposeAsync();
}
private static Stream ConvertAudioToPcm(string filePath)
{
var ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1",
RedirectStandardOutput = true,
UseShellExecute = false
});
return ffmpeg.StandardOutput.BaseStream;
}
}
}