Discord Bot with recording function starting with Python: (4) Play music files

Introduction

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.

  1. Discord Bot with recording function starting with Python: (1) Introductory discord.py
  2. Discord Bot with recording function starting with Python: (2) Convenient function (Bot extension, Cog, Embed)
  3. Discord Bot with recording function starting with Python: (3) Cooperation with Database
  4. Discord Bot with recording function starting with Python: (4) Play music files
  5. Discord Bot with recording function starting with Python: (5) Directly operate Discord API

Discord voice communication

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).

Operate VoiceChannel

Until you enter the Voice Channel

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.

Play audio

First, prepare the music to play and save it under the ./src/app/music/ folder.

Image from Gyazo

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.

Perform continuous playback

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 clearandwait, and play_nextcallsset. 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 theleave` coroutine and automatically leaves the voice channel.

Image from Gyazo

This makes it possible to add a music playback function with a song cue.

At the end

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

Discord Bot with recording function starting with Python: (4) Play music files
Discord Bot with recording function starting with Python: (3) Cooperation with Database
Discord Bot with recording function starting with Python: (1) Introduction discord.py
Play audio files from Python with interrupts
Discord bot with python raspberry pi zero with [Notes]
GRPC starting with Python
How to operate Discord API with Python (bot registration)
Create a Mastodon bot with a function to automatically reply with Python
Python beginner launches Discord Bot
Reinforcement learning starting with Python
Sorting image files with Python (2)
Sort huge files with python
Sorting image files with Python (3)
[Python] Play with Discord's Webhook.
Sorting image files with Python
Integrate PDF files with Python
Reading .txt files with Python
Python starting with Hello world!
Recursively unzip zip files with python
[Python] POST wav files with requests [POST]
Decrypt files encrypted with OpenSSL with Python 3
Let's play with Excel with Python [Beginner]
Handle Excel CSV files with Python
Read files in parallel with Python
Data analysis starting with python (data visualization 1)
Data analysis starting with python (data visualization 2)
System trading starting with Python3: long-term investment
Play video with sound with python !! (tkinter / imageio)
[AWS] Using ini files with Lambda [Python]
Create a Python function decorator with Class
Play handwritten numbers with python Part 2 (identify)
"System trade starting with Python3" reading memo
Fractal to make and play with Python
Business efficiency starting from scratch with Python
Launch the Discord Python bot for 24 hours.
Decrypt files encrypted with openssl from python with openssl
I want to play with aws with python
Download files on the web with Python
Learn Python! Comparison with Java (basic function)
[Easy Python] Reading Excel files with openpyxl
Data analysis starting with python (data preprocessing-machine learning)
Convert HEIC files to PNG files with Python
"First Elasticsearch" starting with a python client
[Easy Python] Reading Excel files with pandas
Play audio files with interrupts using PyAudio
Let's make a Twitter Bot with Python!
If you want to make a discord bot with python, let's use a framework