Adding Speech Output to Discord using C# - #1


Since i spent several days trying to figure out on how to get Speech- and Musicoutput to work on a discord bot using c# i decided to sum up my findings in a short Blog-Post. The Post is rather long therefore i split it in 2 parts.

The discord library can very easily be imported from nuget.

>dotnet add package Discord.Net.Commands
>dotnet add package Discord.Net.WebSocket

In my case this was Version 2.2.0 of the library.

Commands work very nicely but i decided to go for voice. Connecting to a voice channel is also very easy

        var server =
            _discordSocketClient.Guilds.FirstOrDefault(x => x.Name?.Contains("MyServerName") ?? false);
        if (server != null)
        {
            var speechChannel =
                server.Channels.FirstOrDefault(x => (x is IVoiceChannel && x.Name == "Allgemein"));
            if (speechChannel is IVoiceChannel voiceChannel)
            {
                _logger.LogInformation($"Sending msg to {server.Name} - {speechChannel.Name}");

Works also nicely. But from ther on it gets awkward:

                using (IAudioClient audioClient = await voiceChannel.ConnectAsync())
                {
                    await audioClient.SetSpeakingAsync(true);

Boom - what then was:

Unable to load DLL 'libsodium.dll': The specified module could not be found. (Exception from HRESULT: 0x8007007E)

This was fixabel by using Google and i used the solution from StackOverflow, downloaded opus.dll and libsodium.dll from the links in the article (Remember: You have to rename libopus.dll to opus.dll). I put them into nativelibs/win32 and nativelibs/win64.

Adding that code int the csproj moves it to the correct location at buildtime:

  <ItemGroup>
    <None Update="nativelibs\win*\*.dll" CopyToPublishDirectory="Always">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

and implementing a short helper made it possible to select the correct bitness:

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace DiscordBotExample
{
    public class LibOpusLoader
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr LoadLibrary(string libname);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool FreeLibrary(IntPtr hModule);

        private static IntPtr _opusHandle = IntPtr.Zero;
        private static IntPtr _libSodiumHandle = IntPtr.Zero;

        public static void Init()
        {
            // the dll-loading is now available only for Windows-Plattform. 
            // For Linux/MAC you have to make sure that libsodium and opus codecs are installed on the system.
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
                return;
            }

            string bitness = "win32";
            if (Environment.Is64BitProcess)
            {
                bitness = "win64";
            }
            string opusDirPAth = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "nativelibs", bitness);
            if (_opusHandle == IntPtr.Zero)
            {
                _opusHandle = LoadLibrary(Path.Combine(opusDirPAth, "opus.dll"));
            }

            if (_libSodiumHandle == IntPtr.Zero)
            {
                _libSodiumHandle = LoadLibrary(Path.Combine(opusDirPAth, "libsodium.dll"));
            }
        }

        public static void Dispose()
        {
            if (_opusHandle != IntPtr.Zero)
            {
                FreeLibrary(_opusHandle);
            }
            if (_libSodiumHandle != IntPtr.Zero)
            {
                FreeLibrary(_libSodiumHandle);
            }
        }

    }
}

Yeah - we can open an AudioStream now ….

                    using (var audioStream = audioClient.CreatePCMStream(AudioApplication.Music, voiceChannel.Bitrate)) {
                        // try to write something to the stream
                    }

Continue on Part2