Skip to content

A speaking bot

tostc edited this page May 28, 2020 · 1 revision

Introduction

In the previous tutorial we have created a bot which can process a user command to write Hello World! into the chat. Now maybe you think "Well that's cool, but I want to create a music bot." or maybe you would like to make a speaking bot or something else. And that's where this tutorial is for. libDiscordBot has an interface to help you to stream music. Also there is a interface which helps you to create a music queue where you can add, remove, play, skip and pause music.

Overview

This chapter will be extend the previous one. First we create an audio interface which can stream audio to a voice channel. After that we create a music queue to create a basic music bot.

Disclaimer: This chapter will use a pseudo api to load audio data. But you can implement the loading routine with any library you want.

Let's get started

AudioSource

First we need to create a new class which derives from IAudioSource. This interface helps us to stream audio to Discord. First things first Discord supports only a sample rate of 48000 Hz with two channels! So you need to convert your audio on the fly or you must convert your audio before you begin to stream.

AudioSource.hpp

#ifndef AUDIOSOURCE_HPP
#define AUDIOSOURCE_HPP

#include <controller/IAudioSource.hpp>
#include <string>

using namespace DiscordBot;

class CAudioSource : public IAudioSource
{
    public:
        CAudioSource() 
        {
            //Creates a pseudo WaveLoader object.
            WavLoader loader("audio.wav");

            //The buffer size for the audio data is channel count times samples count.
            //Discord supports only 16 bit pcm 2 channel data. Maybe you need to convert your audio first.
            //Also the audio samplerate must 48000 Hz.
            m_BufferSize = loader.GetSampleCount() * loader.GetChannels();
            m_AudioData = new uint16_t[m_BufferSize];

            //Load whole audio into the buffer.
            loader.LoadAs16Bit(m_AudioData, m_BufferSize);       

            m_ReadPos = 0;     
        }

        uint32_t OnRead(uint16_t *Buf, uint32_t Samples) override
        {
            size_t CountToRead = Samples * 2;   //2 because we have 2 channels.

            if(CountToRead + m_ReadPos >= m_BufferSize)
                CountToRead = m_BufferSize - m_ReadPos;

            memset(Buf, 0, Samples * 2 * sizeof(uint16_t)); //To avoid crappy audio if the buffer is not completely filled.
            memcpy(Buf, m_AudioData + m_ReadPos, CountToRead * sizeof(uint16_t));

            return CountToRead;  
        }

        ~CAudioSource() 
        {
            delete[] m_AudioData;
        }

    private:
        uint16_t *m_AudioData;
        size_t m_BufferSize;
        size_t m_ReadPos;       //Position of data which will be next read.
};

#endif

As you see you don't need much code to create a audio source, which streams data to Discord. Inside the constructor we load the whole audio file into memory. (This is very ineffective, but it's just a simple example.) Next we have the OnRead method, which is a callback which is called from the library to obtain audio data to stream. The first parameter is a preallocated buffer which can held 2 times Samples data. The second parameter Samples is the size of the buffer in samples per channel. (e.g. if your buffer is 2048 bytes big you have 1024 bytes per channel, because Discord uses 2 channels!) Next we can add a new command to play this audio source. I will only write the command callback in this example and not the whole class. If you don't know how to create a command please visit the previous chapter.

VoiceCommands.hpp

    ...

    void PlaySound(CommandContext ctx)
    {
        if((ctx->Msg->Member && !ctx->Msg->Member->State) || !ctx->Msg->Member)
            m_Client->SendMessage(ctx->Msg->ChannelRef, "Sorry you must connected to a channel to use this command!");
        else
            m_Client->StartSpeaking(ctx->Msg->Member->State->ChannelRef, AudioSource(new CAudioSource()));
    }

    ...

That's it! One line of code and you can stream music. Let's move forward. "But wait! What does these lines do?", you may think. Ok I will explain. The first if statement checks if the user is connected to a voice chat. It also checks if the message is written to a server text channel or as dm. If the user is not connected to a server or has written the command as dm we write a message into the chat. Inside the else statement we have following line m_Client->StartSpeaking(ctx->Msg->Member->State->ChannelRef, AudioSource(new CAudioSource())). This is the command which starts streaming the audio source and joins to the audio channel of the user, if this it not already the case. Please don't be confused by the second parameter with the value AudioSource(new CAudioSource()). The AudioSource is a typename of a std::shared_ptr<IAudioSource> which is provided by the library and CAudioSource is our implemented class. There are also other commands to manipulate the audio source for example to pause the audio and resume it later. Please visit the API Documentation or the IDiscordClient.hpp for more informations. They are all called *Speaking.

MusicQueue

Above we have created an audio source which loads an audio file from your hard drive and plays it with a play command. That's cool but maybe you think "Ok but I want to play a playlist, not just one song." I had the same thoughts, because I've written a music bot for me and my friends and I had find out it is very hard to write a good queue which is thread safe and don't crash my application. (But maybe I'm just dumb. :D) So I've written an interface which do all the work for you. Things like add this song to the queue, remove this song from the queue or play the next song after the current one finishes.

MusicQueue.hpp

#ifndef MUSICQUEUE_HPP
#define MUSICQUEUE_HPP

#include <controller/IMusicQueue.hpp>
#include "QueueAudioSource.hpp"

using namespace DiscordBot;

class CMusicQueue : public IMusicQueue
{
    public:
        CMusicQueue() {}
        ~CMusicQueue() {}

    protected:
        AudioSource OnNext(SongInfo Info) override
        {
            return AudioSource(new CQueueAudioSource(Info->Path));
        }
};

#endif

I know this tutorial sounds like a self praise or a commercial, but I will repeat me. One line of code and you have a music queue. Ok not exact one line because you need to implement the loading and so on, but for the playlist itself you need one line. The OnNext method is a callback which is called by the queue itself to load the next song. The queue do the rest for you. There are many other callbacks under the protected sections inside the IMusicQueue.hpp file. You can also visit the API Documentation for more informations. Next we need to register the queue, like the controller. We go into our main.cpp and add following line before the Run() call.

Note: CQueueAudioSource is the same class above but it gets the path to the file as parameter.

main.cpp

    ...
    client->RegisterMusicQueue<CMusicQueue>();
    ...

Now we have registered our queue. This queue will now automatically create for every server were the bot is connected to.(Only if the bot is inside a voice channel.) Optional you can pass the constructor parameter to the function. These will pass to constructor if the queue is create.

Now you can add songs to the queue with the void AddToQueue(Guild guild, SongInfo Info) method of the client. Were SongInfo is an object which can contains the Name, Duration and Path to the song. The only attribute which is used by the queue is Name. The rest is ignored. The Name is used to remove a song by it's name. To start playing a queue just call bool StartSpeaking(Channel channel).

Congratulations

You have made it to the end of the second tutorial. You know now how to stream music and create a basic music bot with the help of the music queue api.

Hopefully this tutorial was understandable, if not please open an issue so I can correct it.