こんにちは。この記事は音声認識機能の導入:Discordボット開発【その③】の続きの記事になります。
1. Discord Botの音声再生管理:自動ミュート機能の実装
Discordサーバーで音楽を再生する際、背景音楽としてのみ使用したい場合があります。そんな時、音楽再生中にユーザーが話すことを防ぐため、ボイスチャンネル内のメンバーを自動的にミュートし、再生が終了したらミュートを解除する機能が役立ちます。本記事では、Javaを使用してDiscord Botにこの機能を実装する方法を紹介します。
2. 使用技術
- Java: 実装言語
- JDA (Java Discord API): Discord Bot開発用のライブラリ
- LavaPlayer: Java用の高機能なオーディオライブラリ
3. クラスの概要
TrackScheduler
クラスは、AudioEventAdapter
を継承しており、LavaPlayerのイベントリスニング機能を利用して音声トラックの再生状態を監視します。このクラスは、音声トラックの再生開始時と終了時に実行される特定のアクションを定義します。
主な機能
-
再生開始時の自動ミュート:
onTrackStart
メソッドにより、音声トラックの再生が開始されたときにボイスチャンネル内の全メンバーをミュートします。 -
再生終了時のミュート解除:
onTrackEnd
メソッドにより、トラックの再生が終了したときにミュートを解除します。
コードの説明
package jp.livlog.cotogoto.api.discord;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
* Discordのボイスチャンネルにおける音声トラックの再生状態に基づいて、
* チャンネル内のメンバーを自動的にミュートまたはミュート解除するクラスです。
*/
public class TrackScheduler extends AudioEventAdapter {
private final Guild guild; // Discordのギルド
private final VoiceChannel channel; // ボイスチャンネル
/**
* TrackSchedulerのコンストラクタ。
*
* @param guild 対象のDiscordギルド
* @param channel 対象のボイスチャンネル
*/
public TrackScheduler(final Guild guild, final VoiceChannel channel) {
this.guild = guild;
this.channel = channel;
}
/**
* 音声トラックの再生が開始されたときに呼び出されます。
* ボイスチャンネル内のすべてのユーザーをミュートします。
*
* @param player 再生を行うAudioPlayer
* @param track 再生が開始されたAudioTrack
*/
@Override
public void onTrackStart(final AudioPlayer player, final AudioTrack track) {
this.muteAllMembersInChannel(true); // すべてのユーザーをミュート
}
/**
* 音声トラックの再生が終了したときに呼び出されます。
* ボイスチャンネル内のすべてのユーザーのミュートを解除します。
*
* @param player 再生を行っていたAudioPlayer
* @param track 再生が終了したAudioTrack
* @param endReason 再生終了の理由
*/
@Override
public void onTrackEnd(final AudioPlayer player, final AudioTrack track, final AudioTrackEndReason endReason) {
if (endReason.mayStartNext) {
this.muteAllMembersInChannel(false); // ミュート解除
}
}
/**
* 指定された状態に基づいてボイスチャンネル内のメンバーをミュートまたはミュート解除します。
* ボットはこの処理の対象外です。
*
* @param mute ミュートする場合はtrue、ミュート解除する場合はfalse
*/
private void muteAllMembersInChannel(final boolean mute) {
for (final Member member : this.channel.getMembers()) {
if (!member.getUser().isBot()) { // ボットを除外
this.guild.mute(member, mute).queue(); // メンバーをミュートまたはミュート解除
}
}
}
}
このクラスでは、AudioPlayer
、Guild
(サーバー)、およびVoiceChannel
(ボイスチャンネル)のインスタンスを管理します。これにより、特定のボイスチャンネル内での音声再生とユーザーのミュート管理を可能にします。
また、呼び出し元は以下のようになります。
package jp.livlog.cotogoto.api.discord;
import java.nio.ByteBuffer;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.audio.AudioReceiveHandler;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import net.dv8tion.jda.api.audio.UserAudio;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
* NobyAudioHandlerは、Discordのオーディオデータの送受信と、音声再生の管理を行うクラスです。
* オーディオプレイヤーの作成、オーディオトラックの再生、ユーザー音声の受信処理などを担当します。
*/
@Slf4j
public class NobyAudioHandler implements AudioReceiveHandler, AudioSendHandler {
private final AudioProcessor audioProcessor; // オーディオ処理を担当するプロセッサ
private final SharedAudioData sharedAudioData; // 共有オーディオデータ
private final AudioPlayerManager playerManager; // オーディオプレイヤーの管理
private final AudioPlayer audioPlayer; // オーディオプレイヤー
private final Guild guild; // Discordギルド
private final VoiceChannel channel; // ボイスチャンネル
private AudioFrame lastFrame; // 最後のオーディオフレーム
/**
* NobyAudioHandlerのコンストラクタ。
*
* @param audioProcessor オーディオ処理を担当するプロセッサ
* @param sharedAudioData 共有オーディオデータ
* @param playerManager オーディオプレイヤーの管理を行うマネージャ
* @param guild 対象のDiscordギルド
* @param channel 対象のボイスチャンネル
*/
public NobyAudioHandler(
final AudioProcessor audioProcessor,
final SharedAudioData sharedAudioData,
final AudioPlayerManager playerManager,
final Guild guild,
final VoiceChannel channel) {
this.audioProcessor = audioProcessor;
this.sharedAudioData = sharedAudioData;
this.playerManager = playerManager;
this.audioPlayer = playerManager.createPlayer();
this.guild = guild;
this.channel = channel;
this.audioPlayer.addListener(new TrackScheduler(guild, channel));
}
@Override
public boolean canReceiveCombined() {
// 合成されたオーディオは受け取らないため、falseを返します。
return false;
}
@Override
public boolean canReceiveUser() {
// ユーザーオーディオは受け取るため、trueを返します。
return true;
}
@Override
public void handleUserAudio(final UserAudio userAudio) {
// ユーザーから受け取ったオーディオデータを処理します。
final var receiveData = userAudio.getAudioData(1.0); // バイト配列としてオーディオデータを取得
this.sharedAudioData.addAudioData(userAudio.getUser().getId(), receiveData);
}
@Override
public boolean canProvide() {
// オ
ーディオデータが提供可能かどうかを判定します。
try {
final var audioData = this.sharedAudioData.takeAudioData();
if (audioData != null) {
final var replyData = this.audioProcessor.processAudio(audioData.getId(), audioData.getData());
if (replyData != null) {
final var base64String = DataUriSchemeGenerator.encodeToBase64WithMimeType(replyData);
this.loadAndPlayTrack(base64String);
}
}
} catch (final Exception e) {
NobyAudioHandler.log.error(e.getMessage(), e);
}
this.lastFrame = this.audioPlayer.provide();
return this.lastFrame != null;
}
/**
* 指定されたトラックをロードし、再生を開始します。
*
* @param trackString 再生するトラックのURLまたは識別子
*/
private void loadAndPlayTrack(final String trackString) {
this.playerManager.loadItem(trackString, new AudioLoadResultHandler() {
@Override
public void trackLoaded(final AudioTrack track) {
// ロードされたトラックを再生します。
NobyAudioHandler.this.audioPlayer.playTrack(track);
}
@Override
public void playlistLoaded(final AudioPlaylist playlist) {
// プレイリストがロードされた場合の処理をここに実装します。
}
@Override
public void noMatches() {
// トラックが見つからない場合の処理をここに実装します。
}
@Override
public void loadFailed(final FriendlyException e) {
// トラックのロードに失敗した場合のエラー処理をここに実装します。
NobyAudioHandler.log.error(e.getMessage(), e);
}
});
}
@Override
public ByteBuffer provide20MsAudio() {
// 20ミリ秒分のオーディオデータを提供します。
return ByteBuffer.wrap(this.lastFrame.getData());
}
@Override
public boolean isOpus() {
// 提供されるオーディオデータがOpus形式であるかどうかを返します。
return true;
}
}
NobyAudioHandler
クラスは以前までの実装を省略して書いていますが、全体を通すことで、Botのプログラムとして仕上がってきます。
ミュートとミュート解除の実装
muteAllMembersInChannel
メソッドは、ボイスチャンネル内の全メンバーに対してミュートまたはミュート解除を行います。この処理は、ボット以外のユーザーに対して適用されます。
4. 実装のポイント
この機能を実装する際には、次の点に注意してください。
- ユーザーの権限: Botがミュートを行うためには、適切な権限が必要です。
- ユーザーエクスペリエンス: 自動ミュート機能を適用する前に、ユーザーに通知することで、混乱を避けることができます。
5. CotoGotoとの連携について
最後に、この技術メモは、CotoGotoの機能拡張の一環としてDiscordを連携するためのものです。CotoGoto(コトゴト)は、人工知能を搭載した会話型アプリで、日常的な会話を通じて作業内容を分析し、タスク管理やスケジュール管理をサポートします。Discordボットの導入により、CotoGotoはより多くのユーザーとのインタラクションを実現し、日々の生活や作業に役立つ情報を提供できるようになります。
詳細は、以下をご覧ください。
6. まとめ
TrackScheduler
クラスを使用して、Discord Botに音声再生中の自動ミュート機能を実装する方法を紹介しました。この機能は、音楽をバックグラウンドで再生する際など、さまざまなシナリオで役立つでしょう。実装に当たっては、適切な権限の設定とユーザーへの明確なコミュニケーションが重要です。