discord允許我們提供音檔來讓機器人播放聲音
如果音檔為音樂,則可以實現播放音樂的功能

環境準備

播放音樂以及抓取音檔需要額外的套件,請一樣依照自己的電腦環境來替換開頭:

1
pip install yt-dlp discord.py[voice] python-ffmpeg

yt-dlp是提取Youtube影片資料的
discord.py[voice]是discord.py的語音擴充
python-ffmpeg可以把音檔轉換成二進制檔案供Discord使用

屬性介紹

voice屬性

之前我們講過使用者有許多屬性,其中voice沒有提到,今天簡單介紹我們需要用到的部分
user.voice屬性本身屬於discord.VoiceState類別,代表目前該使用者的語音狀態,如果沒有在語音狀態則是None
如果存在的話會有以下常用屬性(非全部)

  • deaf: 是否拒聽
  • mute: 是否靜音
  • channel: 連線到的頻道
  • self_stream: 是不是正在直播

我們今天會需要他的channel屬性

voice_clients

這個屬性是bot所有的語音連線狀態
與使用者不同,一個機器人可以同時在不同伺服器連線到語音頻道(不過一個伺服器一樣限制一個頻道)
bot.voice_clients列出了所有機器人連線到的語音用戶端(可以想成一個專屬語音的虛擬使用者),屬於discord.VoiceClient類別
channelguild(頻道/伺服器)屬性可以給我們取用,還能進行其他播放的操作

discord.utils.get()

這是一個特殊的函數,是discord提供的方便函數
其用法為

1
result = discord.utils.get(一個列表, 屬性=值)

意思是在一個列表裡面一一尋找,直到找到某一個項目的屬性符合值,舉例來說

1
result = discord.utils.get(社團列表, name="電算社")

這時候result就會找到一個物件,其name屬性為"電算社"的並返回給result
這時候我們就可以拿result來用
但如果沒有找到的話result會是None

如果搭配voice_clients就可以尋找機器人所有語音用戶端內,是當前這個伺服器的那個項目

加入與退出

加入

我們將請使用者先加入一個語音頻道,然後使機器人也加入該頻道
這時候就需要考慮到使用者如果還沒進去一個頻道,就要請他進去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@bot.tree.command()
async def join(interaction: discord.Interaction):

# 正常運行時,使用者應該連線而機器人不應該已經連線
v = interaction.user.voice # 使用者的連線
vc = discord.utils.get(bot.voice_clients, guild=interaction.guild) # 機器人的連線

if v and not vc: # 如果判斷的條件不是布林值,則改判斷存不存在,或者是不是空的

# 直接對一個頻道進行connect()就可以讓機器人連線
await v.channel.connect() # 文字頻道沒辦法connect(),可是這邊已經100%確定這是語音頻道
await interaction.response.send_message(f"已經連線至 {v.channel.mention}")
elif not v: # 如果是使用者沒有連線
await interaction.response.send_message("請先加入一個語音頻道")
else: # 機器人已經連線過了
await interaction.response.send_message(f"機器人已經連線了")

退出

退出時一樣要考慮機器人是不是真的在一個頻道內

1
2
3
4
5
6
7
8
9
10
@bot.tree.command(description="讓機器人離開語音頻道")
async def leave(interaction: discord.Interaction):
# 從機器人的語音用戶端找到guild是當前伺服器的那一項
vc = discord.utils.get(bot.voice_clients, guild=interaction.guild)

if vc: # 如果這個用戶端存在
await vc.disconnect(force=True)
await interaction.response.send_message("已經離線")
else: # 用戶端不存在 => 機器人沒有在這個伺服器連線到語音
await interaction.response.send_message("機器人不在語音頻道內")

播放本地音訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@bot.tree.command(description="播放音訊")
async def play(interaction: discord.Interaction, name: str):
await interaction.response.defer() # 取得音訊和轉換需要時間,建議先進思考

# 這裡採用另一種寫法,如果不符合條件就直接跳出函數,不用另外包一層else
vc = discord.utils.get(bot.voice_clients, guild=interaction.guild)
if not vc:
await interaction.response.send_message("機器人不在語音頻道內")
return

audio = discord.FFmpegPCMAudio(f"./{name}") # 建立一個音訊二進制檔案(discord會自動處理,只需提供路徑)

vc.play(audio)
await interaction.followup.send(f"正在播放 {name}")

暫停、繼續和停止

暫停

這裡還需要多考慮一件事:機器人有沒有在播音樂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@bot.tree.command(name="pause", description="暫停播放音樂")
async def pause(interaction: discord.Interaction):


vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild)
if not vc:
await interaction.response.send_message("機器人不在語音頻道內")
return

# 檢查機器人確實有在播放
if vc.is_playing():
vc.pause() # 這個函數可以暫停
await interaction.response.send_message("音樂已暫停")
else:
await interaction.response.send_message("目前沒有音樂在播放")

繼續

同理,繼續之前需要檢查機器人是不是暫停狀態

1
2
3
4
5
6
7
8
9
10
11
12
@bot.tree.command(name="resume", description="繼續播放音樂")
async def resume(interaction: discord.Interaction):
vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild)
if not vc:
await interaction.response.send_message("機器人不在語音頻道內")
return

if vc.is_paused():
vc.resume() # 這個函數可以繼續
await interaction.response.send_message("音樂已繼續播放")
else:
await interaction.response.send_message("音樂未被暫停")

停止

1
2
3
4
5
6
7
8
9
10
11
12
@bot.tree.command(name="stop", description="停止播放音樂")
async def stop(interaction: discord.Interaction):
vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild)
if not vc:
await interaction.response.send_message("機器人不在語音頻道內")
return

if vc.is_playing():
vc.stop() # 這個函數可以停止
await interaction.response.send_message("已經停止")
else:
await interaction.response.send_message("沒有在播音樂")