Writing a .NET Music Discord Bot for a Raspberry Pi Zero 2 W: Brotherman Bill
This post takes us through creating a music Discord bot using C#, Lavalink, and Victoria while running on a small Raspberry Pi Zero 2 W.
Origin Story
It's great fun to hang out with friends on Discord while gaming, and it's even better to have a soundtrack with it. The Discord server my friends and I are in, like many others, used to use Rythm - a music bot that could play music in a voice chat. However due to legal issues, Rythm as we knew it left us in September 2021.
Missing Rythm, I thought "hang on, I write software, I can probably make one!" and that's where my custom music and meme bot, Brotherman Bill (a.k.a BrothermanBill), began developing in late November 2021.
This post is written about five months on after various iterations and feature additions specific to the needs and wants of my friends. There's still more to do, and there always will be, but it's in a good enough state to talk about.
Discord.NET
If it isn't obvious so far, I'll be using .NET, specifically C# for this bot. There are a couple of .NET libraries for Discord integration and I settled on Discord.NET. It was pretty easy to get something up and running from the Your First Bot guide. Note this documentation also handily takes you through creating a token from Discord.
After getting some simple commands up and running, it was time to look up how to get the bot to output audio into the voice channel. With Discord.NET it was a bit fiddly with handling the audio stream to send up to Discord, but it worked. Though I became stumped on how to integrate into other audio services like YouTube in order to consume media.
Turns out there is an existing option for this called Lavalink and there is a very good NuGet package for this, Victoria.
Lavalink
Lavalink is a "standalone audio sending node based on Lavaplayer and JDA-Audio". Or in other terms it leverages Lavaplayer to provide a surface for playing audio from many sources including YouTube and Twitch.
A gotcha here is that Lavalink runs on Java, but it seems to be the de facto standard for anything Discord and music/audio as it has client libraries written in many different languages from Python, to Node.js, to C#.
While this means we need to have Java installed, Lavalink can just happily sit there running because the actual interaction is through Victoria, a great C# wrapper for Lavalink.
Victoria
The Victoria readme states:
Lavalink wrapper for Discord.NET which provides more options and performs better than all .NET Lavalink libraries combined.
While I haven't used any of the other ones, Victoria is really easy to use. I was able to nearly lift and shift from the following docs/classes:
And one Lo-Fi YouTube URL later, it worked. It was such a moment I had to bring the squad in to the voice channel to show it off. And for you reader, let me show off what the bot can do.
Music
The core purpose for this bot, and where most commands are centered around.
Commands
Using the !play
command without a URL queries YouTube. Using it with a URL instead will just play the specified audio file.
!np
or !nowplaying
will show what is currently playing.
!queue
brings up what's currently in the queue.
And many more commands to do with music:
Command | Description |
---|---|
Join | Adds Brotherman Bill to the calling user's audio channel. |
Leave | Disconnects Brotherman Bill to the calling user's audio channel. |
Play | Adds a YouTube search query or a YouTube video or playlist URL to the queue. |
PlayNow | Immediately plays a YouTube search query or a YouTube video or playlist URL. |
MoveToBack | Moves the currently playing track to the back of the queue. |
Pause | Pauses the current track. |
Seek | Seeks with a given time. Formats include "ss", "mm:ss", "h:mm:ss". Can be negative. |
SeekTo | Seeks to a given time. Formats include "ss", "mm:ss", "h:mm:ss". |
Resume | Resumes the current track. |
Stop | Stops playing the current track and clears the queue. |
Skip | Skips the currently playing track. |
NowPlaying (np) | Displays information about the currently playing track. |
Queue | Displays the current queue. Use "full" after the command for the entire queue. |
ClearQueue | Clears the queue. |
Memes
While YouTube has little meme snippets, often when you want a soundbyte it won't be the first result from a search and instead you'd get a long meme video. Thankfully though the website MyInstants exists, and you can search and get a decent enough array of results - especially for memes. And it has an API.
Commands
!meme
will play a random meme from MyInstants.
This command also works with a query.
Note that there is title, description, and art as that is not supplied by MyInstants for most sounds and as such the defaults are used.
Other Features
There are a few other tidbits the bot can do.
Auto Joining on Command
While there is a !join
command to get the bot into the voice channel of the person who ran said command, most commands will allow the bot to skip this step and automatically join. The most obvious one is simply the !play
command where the bot will join and play in one go.
Actions Around Playing Unending Streams
Playing a YouTube livestream will cause special behaviour. As it is essentially counted as infinite, adding things to the queue after starting a stream will never play. So the queued item will play immediately then fall back to the stream afterwards.
Showing Now Playing
Easy to see what's currently playing my checking the status.
Leaving an Empty Voice Channel
When the party's over for the day, no on wants to be left managing the bot. So when the current voice channel is vacated by everyone, the bot will also automatically leave.
Misc. Commands
The following are a few useful misc. commands.
!uptime
to see how long the bot has been up.
!ping
for the round trip time from the bot to Discord servers.
!help
for a list of all the commands.
Raspberry Pi
This is one of my favourite parts of this project. For a few months I would run Brotherman Bill via Visual Studio on my local machine when my friends and I hung out on Discord. Sadly this meant that the bot wasn't around when I wasn't around. I looked into hosting things in the cloud but I had no interest in paying cloud costs for a service that wasn't very used but had to run 24/7 to be on standby.
I had a Raspberry Pi Zero W but it turns out it can't run .NET due to being ARM6 whereas .NET requires instructions from ARM7 to run.
However, I also had a Raspberry Pi Zero 2 W for a different project, which DID support .NET and is multi-threaded. With a little asset commandeering, I set out to understand how to get any of this running on a tiny Zero 2 W - and I documented/wrote about it too! Feel free to checkout all the steps I took to get up to this point:
- How to Run a Raspberry Pi Zero 2 W Headless
- Installing Java 16 on a Raspberry Pi Zero 2 W
- Running Lavalink on a Raspberry Pi Zero 2 W
- Installing .NET 6 on a Raspberry Pi Zero 2 W
- Creating an Autostart .NET 6 Service on a Raspberry Pi
After understanding all of the above, I couldn't believe that it just worked first time. It sure was a bit slower than running it off my Intel i7 but after a couple of seconds of initial delay, it played music from YouTube to the voice channel.
Deployment
As a developer, we like to be lazy automate tasks. I didn't want to go out of my way to push new code to the Pi as I want it to just run headless off in a corner somewhere attached to the wall (and my Wi-Fi) and forget about it. Thankfully I've setup CI/CD for two workplaces with Azure DevOps so while it might feel like overkill, it was fun to flex that knowledge for my own projects to automatically deploy after new code is checked in. You can check that out in my Continuous Deployment From Github to a Raspberry Pi Zero 2 W With Azure DevOps post.
Physical Setup
The bot itself lives in the official Raspberry Pi Zero case, with a little label printed from a an HP Sprocket.
With the bot being so small, I've taken to run it just hanging off a very short USB cable. It literally hangs out.
The Things I Learned
With anything, there's a good amount of learning. The following is a quickfire bullet point list that skims through some of it.
- First time using the newer .NET Secret Manager to handle storing the Discord token.
- Graceful closing is always nicer than just killing a process, and I learned that there is a hook for this with the
AppDomain.CurrentDomain.ProcessExit
event. - Using
record
for the first time. - Understanding that
async
isn't needed all the time even thoughTask
is involved - such as when returningTask.CompletedTask
. - Remembering that Expression Body Members are a thing, or using
=>
for one line functions. - That most emojis can just be pasted into strings as literals - without resorting to their code.
- How to write services for a Linux OS.
- Edit: For the architecture, it turns out I had to use some altered
.jar
files from Cog-Creators/Lavalink-Jars.
What Next?
As great as it is, there are some issues:
- Fix the memory bloat somewhere. While both .NET and Java are managed, after a few days the page swapping for memory becomes so frequent the Pi becomes unusable and completely unresponsive. There is also the Azure DevOps agent running too. When moving into this slow state I noticed the non-music commands tend to be fine, maybe then it's the fault of Lavalink/Java? Perhaps a faster SD card will band aid the problem?
- There's always something on the to-do list
- I want to creating a voice command subsystem or something like that where we don't need to alt-tab to run a command. Maybe integrate into Azure's Cognitive Services for more complex tasks <3
- There are some assumptions that the bot will only run on one server. Some shared resources will run into contention if another server is involved - though I'm not sure I'm compelled to fix this. An example of this is the status of the bot being set to what's currently playing - this wouldn't scale with >1 voice channels, let alone servers.
- I'd like to create a self-contained and trimmed app.
To Conclude
Thank for you reading through. Feel free to check out the source code yourself, take inspiration for your own bot, or even fork it! It was a fun project over a few months of on and off to make and while it still has a few problems, it was a riveting experience to write.