This article is a continuation of the previous Discord Bot with recording function starting with Python: (3) Cooperation with Database.
This time, we will add a jukebox function that plays music prepared in advance on the server with commands. If you use discord.py, you can play audio with a very simple process. Here, we will add a function that allows continuous playback by implementing a song cue instead of just playing it.
We plan to write 7 times in total, and have finished writing up to 5 articles.
In discord.py, it is a part that is not directly tampered with, so there is not much to be aware of it, but in addition to the REST API used when sending posts with HTTP requests and getting server information, it is bidirectional. There are three methods of sending and receiving information: WebSocket communication for communication and RTP communication for sending and receiving voice.
In the case of discord.py, it is a very intuitive mechanism to get an instance of the voice channel from Context etc. and connect to the voice channel by executing the connect
coroutine of that instance. What is returned by this connect
is an instance of the VoiceClient
class.
In VoiceClient
, the above WebSocket communication and other encrypted communication are covered. Therefore, if you want to manipulate the voice channel information for each server, you can perform various operations on this VoiceClient
instance.
When considering processing for voice channels from multiple servers, a mechanism to connect this instance with the server (ID) is required. Here, we operate VoiceClient using a dictionary array for management like Official implementation in discord.py repository Then try to play the music locally (on the server running the bot).
As mentioned above, entering and exiting the Voice Channel is very easy. Here, we will create a Cog called Voice
, enter the room with $ join
, and leave with $ leave
. An example of implementation is as follows.
python:./src/app/dbot/cogs/Voice.py
import discord
from discord.ext import commands
from typing import Dict
from dbot.core.bot import DBot
class Voice(commands.Cog):
def __init__(self, bot: DBot):
self.bot = bot
self.voice_clients: Dict[int, discord.VoiceClient] = {}
@commands.command()
async def join(self, ctx: commands.Context):
#Not participating in Voice Channel
if not ctx.author.voice or not ctx.author.voice.channel:
return await ctx.send('Join the voice channel first')
vc = await ctx.author.voice.channel.connect()
self.voice_clients[ctx.guild.id] = vc
@commands.command()
async def leave(self, ctx: commands.Context):
vc = self.voice_clients.get(ctx.guild.id)
if vc is None:
return await ctx.send('I haven't joined the voice channel yet')
await vc.disconnect()
del self.voice_clients[ctx.guild.id]
def setup(bot):
return bot.add_cog(Voice(bot))
The property voice
of discord.Member
returns an instance of the VoiceState class if the member is participating in a voice channel. This VoiceState has a property called channel
in addition to the mute state of the member, and by referring to this, an instance of the voice channel class (discord.VoiceChannel) currently in the member is obtained. can do.
Calling the connect
coroutine of the VoiceChannel
returns a VoiceClient
and the bot joins the voice channel. The VoiceClient
is stored in the dictionary using the server ID as a key. On the contrary, $ leave
searches for VoiceClient
from the server ID and calls the disconnect
coroutine. The entry / exit process is now complete.
First, prepare the music to play and save it under the ./src/app/music/
folder.
Give it an appropriate name to implement it so that it searches based on the name. For the time being, play with $ play song title
and stop with $ stop
.
python:./src/app/dbot/cogs/Voice.py
import discord
from glob import glob
import os
from discord.ext import commands
from typing import Dict
from dbot.core.bot import DBot
class Voice(commands.Cog):
#Abbreviation
@commands.command()
async def play(self, ctx: commands.Context, *, title: str = ''):
vc: discord.VoiceClient = self.voice_clients.get(ctx.guild.id)
if vc is None:
await ctx.invoke(self.join)
vc = self.voice_clients[ctx.guild.id]
music_pathes = glob('./music/**.mp3')
music_titles = [
os.path.basename(path).rstrip('.mp3')
for path in music_pathes
]
if not title in music_titles:
return await ctx.send('There is no specified song.')
idx = music_titles.index(title)
src = discord.FFmpegPCMAudio(music_pathes[idx])
vc.play(src)
await ctx.send(f'{title}To play')
@commands.command()
async def stop(self, ctx: commands.Context):
vc: discord.VoiceClient = self.voice_clients.get(ctx.guild.id)
if vc is None:
return await ctx.send('Bot hasn't joined the voice channel yet')
if not vc.is_playing:
return await ctx.send('Already stopped')
await vc.stop()
await ctx.send('Stopped')
#Abbreviation
To play music using duscird.py, you need to pass the AudioSource to the VoiceClient's Play coroutine. Ffmpeg is required to create AudioSource. If you have a ffmpeg environment, you can play music very easily just by giving the path to the file.
Similarly, when stopping playback, call the vc.stop coroutine.
In the current implementation, the music will switch as soon as you call $ play
. Change this to a specification that adds music to the queue when $ play
is performed and plays the next music when the previous music has finished playing (so-called playlist).
Use ʻasyncio.Queue and ʻasyncio.Event
to implement playlists. They are often used for implementations that wait until an element is added to the queue, and for implementations that wait until a flag is set elsewhere.
python:./src/app/dbot/cogs/Voice.py
import discord
from glob import glob
import os
import asyncio
from discord.ext import commands
from typing import Dict
from dbot.core.bot import DBot
class AudioQueue(asyncio.Queue):
def __init__(self):
super().__init__(100)
def __getitem__(self, idx):
return self._queue[idx]
def to_list(self):
return list(self._queue)
def reset(self):
self._queue.clear()
class AudioStatus:
def __init__(self, ctx: commands.Context, vc: discord.VoiceClient):
self.vc: discord.VoiceClient = vc
self.ctx: commands.Context = ctx
self.queue = AudioQueue()
self.playing = asyncio.Event()
asyncio.create_task(self.playing_task())
async def add_audio(self, title, path):
await self.queue.put([title, path])
def get_list(self):
return self.queue.to_list()
async def playing_task(self):
while True:
self.playing.clear()
try:
title, path = await asyncio.wait_for(self.queue.get(), timeout=180)
except asyncio.TimeoutError:
asyncio.create_task(self.leave())
src = discord.FFmpegPCMAudio(path)
self.vc.play(src, after=self.play_next)
await self.ctx.send(f'{title}To play...')
await self.playing.wait()
def play_next(self, err=None):
self.playing.set()
async def leave(self):
self.queue.reset()
if self.vc:
await self.vc.disconnect()
self.vc = None
@property
def is_playing(self):
return self.vc.is_playing()
def stop(self):
self.vc.stop()
class Voice(commands.Cog):
def __init__(self, bot: DBot):
self.bot = bot
self.audio_statuses: Dict[int, AudioStatus] = {}
@commands.command()
async def join(self, ctx: commands.Context):
#Not participating in Voice Channel
if not ctx.author.voice or not ctx.author.voice.channel:
return await ctx.send('Join the voice channel first')
vc = await ctx.author.voice.channel.connect()
self.audio_statuses[ctx.guild.id] = AudioStatus(ctx, vc)
@commands.command()
async def play(self, ctx: commands.Context, *, title: str = ''):
status = self.audio_statuses.get(ctx.guild.id)
if status is None:
await ctx.invoke(self.join)
status = self.audio_statuses[ctx.guild.id]
music_pathes = glob('./music/**.mp3')
music_titles = [
os.path.basename(path).rstrip('.mp3')
for path in music_pathes
]
if not title in music_titles:
return await ctx.send('There is no specified song.')
idx = music_titles.index(title)
await status.add_audio(title, music_pathes[idx])
await ctx.send(f'{title}Was added to the playlist')
@commands.command()
async def stop(self, ctx: commands.Context):
status = self.audio_statuses.get(ctx.guild.id)
if status is None:
return await ctx.send('Bot hasn't joined the voice channel yet')
if not status.is_playing:
return await ctx.send('Already stopped')
await status.stop()
await ctx.send('Stopped')
@commands.command()
async def leave(self, ctx: commands.Context):
status = self.audio_statuses.get(ctx.guild.id)
if status is None:
return await ctx.send('I haven't joined the voice channel yet')
await status.leave()
del self.audio_statuses[ctx.guild.id]
@commands.command()
async def queue(self, ctx: commands.Context):
status = self.audio_statuses.get(ctx.guild.id)
if status is None:
return await ctx.send('Join the voice channel first')
queue = status.get_list()
songs = ""
for i, (title, _) in enumerate(queue):
songs += f"{i+1}. {title}\n"
await ctx.send(songs)
def setup(bot):
return bot.add_cog(Voice(bot))
Create a new class called ʻAudioStatus` to register VoiceClient and server information together, and save the instance.
ʻAudioStatus calls a function called ʻasyncio.create_task
at initialization. As the name suggests, the task of playing music is created from coroutines. By doing this, asynchronous access such as responding to commands from other servers while performing other processing on the task side is possible. The property playing
is ʻasyncio.Event, which is used when you want to set a flag when a certain condition is met and wait without doing anything until the flag is set.
playing_task calls
clearand
wait, and
play_nextcalls
set. They call" set the flag to
False", "wait until the flag becomes
True ", and" It is a process of "setting the flag to
True".
play_next is passed to the after argument of
vc.play, which gives the argument what you want to do when the playback of one song is finished. By setting the flag for playing the next song at the end of the song, the
playing_task` loop will start again.
The queue
property of this ʻAudioStatus inherits ʻasyncio.Queue
and makes it easy to get the song list, but this ʻasyncio.Queue has no value to return when calling
getWaits there for new elements to be added on the fly. This makes it possible to set a waiting time for song input. And if there is no input for 3 minutes, it calls the
leave` coroutine and automatically leaves the voice channel.
This makes it possible to add a music playback function with a song cue.
The music playback function is now implemented! I'm glad it was easy! The voice recording function is just as easy, isn't it? ?? ??
It seems that it can not be said, so as a preparation for implementing the recording function, I will implement it without using discord.py by taking a closer look at how Discord's API function is handled by directly touching it. ..
Recommended Posts