0. 들어가기 앞서
노래 자체를 디스코드 봇에 재생하는 것은 당연히 내 수준으로는 불가능하다
그러므로 다른 사람이 구현해놓은 코드를 통해서 재생하는 방법으로 갈 것인데
아무래도 계속해서 버전 업데이트가 이루어지다보니 라이브러리를 업데이트 해주지 않으면 어느순간 노래가 안나오거나 오류가 발생하게 된다. 그럼에도 불구하고 노래 재생까지 구현해보았다는 것에 의의를 두었다.
+) 버전은 이 게시물 작성의 가장 최신 버전을 기준으로 구현해보았다.
아래 강좌를 참고하여 만들었다.
https://www.youtube.com/watch?v=9mAPldz8ACI
1. 라이브러리 추가
우선 lavaplayer 라이브러리를 사용하기 위해
build.gradle.kts 파일을 찾아 의존성을 추가해주어야한다.
이전 환경설정 편에서 작성한 JDA 버전을 올려주면서 lavaplayer와 youtube-source를 추가해줄 것이다.
1. JDA 깃허브 주소 : https://github.com/discord-jda/JDA
GitHub - discord-jda/JDA: Java wrapper for the popular chat & VOIP service: Discord https://discord.com
Java wrapper for the popular chat & VOIP service: Discord https://discord.com - discord-jda/JDA
github.com
2. lavaplayer 깃허브 주소 : https://github.com/lavalink-devs/lavaplayer
GitHub - lavalink-devs/lavaplayer: Lavaplayer fork maintained by Lavalink
Lavaplayer fork maintained by Lavalink. Contribute to lavalink-devs/lavaplayer development by creating an account on GitHub.
github.com
3.youtube-source 깃허브 주소 : https://github.com/lavalink-devs/youtube-source
GitHub - lavalink-devs/youtube-source: A rewritten YouTube source manager for Lavaplayer.
A rewritten YouTube source manager for Lavaplayer. - lavalink-devs/youtube-source
github.com
해당 깃허브들을 들어가면
의존성 추가 방법을 찾을 수 있다.
이를 통해 의존성을 추가한 코드를 작성해보았다.
plugins {
id("java")
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
maven {
setUrl("https://maven.lavalink.dev/releases")
}
maven {
setUrl("https://jitpack.io")
}
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.9.1"))
testImplementation("org.junit.jupiter:junit-jupiter")
// 새로 추가/업데이트 한 라이브러리
implementation("net.dv8tion:JDA:5.2.2")
implementation("dev.arbjerg:lavaplayer:2.2.2")
implementation("dev.lavalink.youtube:common:1.11.2")
// 로그 기록
implementation("org.slf4j:slf4j-api:2.0.9")
implementation("ch.qos.logback:logback-classic:1.5.6")
}
tasks.test {
useJUnitPlatform()
}
이후 Gradle을 다시 로드해주면 적용이 된다.
Intellij 기준으로 External Libraries에서
해당 네 개의 라이브러리가 있다면 정상적으로 빌드된 것이다.
2. Class 생성
우선 music 폴더를 따로 만들어서 관리하도록 만들것이다.
이후 생성한 music 폴더에
AudioPlayerSendHandler.class
GuildMusicManager.class
PlayerManager.class
TrackScheduler.class
각 클래스들을 생성해주었다.
+) 각 클래스의 용도가 궁금하여 GPT에게 물어보았더니 아래와 같이 답변해주었다.
AudioPlayerSendHandler : Discord로 오디오 데이터를 전송하기 위한 인터페이스를 구현
GuildMusicManager : 서버(길드)별로 플레이어와 트랙 관리를 담당
PlayerManager : 서버(길드)별 GuildMusicManager를 중앙에서 관리
TrackScheduler : 음악 재생 대기열(Queue) 관리
2-1. AudioPlayerSendHandler.class
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import java.nio.Buffer;
import java.nio.ByteBuffer;
public class AudioPlayerSendHandler implements AudioSendHandler {
private final AudioPlayer audioPlayer;
private final ByteBuffer buffer;
private final MutableAudioFrame frame;
public AudioPlayerSendHandler(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
this.buffer = ByteBuffer.allocate(1024);
this.frame = new MutableAudioFrame();
this.frame.setBuffer(buffer);
}
@Override
public boolean canProvide() {
return this.audioPlayer.provide(this.frame);
}
@Override
public ByteBuffer provide20MsAudio() {
final Buffer buffer = ((Buffer) this.buffer).flip();
return (ByteBuffer) buffer;
}
@Override
public boolean isOpus() {
return true;
}
}
2-2. GuildMusicManager.class
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
public class GuildMusicManager {
public final AudioPlayer audioPlayer;
public final TrackScheduler scheduler;
private final AudioPlayerSendHandler sendHandler;
public GuildMusicManager(AudioPlayerManager manager) {
this.audioPlayer = manager.createPlayer();
this.scheduler = new TrackScheduler(this.audioPlayer);
this.audioPlayer.addListener(this.scheduler);
this.sendHandler = new AudioPlayerSendHandler(this.audioPlayer);
}
public AudioPlayerSendHandler getSendHandler() {
return this.sendHandler;
}
}
2-3. TrackScheduler.class
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 java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class TrackScheduler extends AudioEventAdapter {
private final AudioPlayer audioPlayer;
private final BlockingQueue<AudioTrack> queue;
public TrackScheduler(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
this.queue = new LinkedBlockingQueue<>();
}
public void queue(AudioTrack track) {
if(!this.audioPlayer.startTrack(track, true)) {
this.queue.offer(track);
}
}
@Override
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
if(endReason.mayStartNext) {
nextTrack();
}
}
public void nextTrack() {
this.audioPlayer.startTrack(this.queue.poll(), false);
}
}
2-4. PlayerManager.class
import com.sedmelluq.discord.lavaplayer.player.AudioConfiguration;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import java.util.HashMap;
import java.util.Map;
public class PlayerManager {
private static PlayerManager INSTANCE;
private final Map<Long, GuildMusicManager> musicManagers;
private final AudioPlayerManager audioPlayerManager;
private PlayerManager() {
this.musicManagers = new HashMap<>();
this.audioPlayerManager = new DefaultAudioPlayerManager();
audioPlayerManager.getConfiguration().setOpusEncodingQuality(AudioConfiguration.OPUS_QUALITY_MAX);
audioPlayerManager.getConfiguration().setResamplingQuality(AudioConfiguration.ResamplingQuality.HIGH);
audioPlayerManager.getConfiguration().setFilterHotSwapEnabled(true);
YoutubeAudioSourceManager youtube = new YoutubeAudioSourceManager(true);
this.audioPlayerManager.registerSourceManager(youtube);
AudioSourceManagers.registerRemoteSources(this.audioPlayerManager);
AudioSourceManagers.registerLocalSource(this.audioPlayerManager);
}
public static PlayerManager getINSTANCE() {
if(INSTANCE == null) {
INSTANCE = new PlayerManager();
}
return INSTANCE;
}
public GuildMusicManager getMusicManager(Guild guild) {
return this.musicManagers.computeIfAbsent(guild.getIdLong(), (guildId) -> {
final GuildMusicManager guildMusicManager = new GuildMusicManager(this.audioPlayerManager);
guild.getAudioManager().setSendingHandler(guildMusicManager.getSendHandler());
return guildMusicManager;
});
}
public void loadAndPlay(TextChannel textChannel, String trackURL, Member client) {
final GuildMusicManager musicManager = this.getMusicManager(textChannel.getGuild());
this.audioPlayerManager.loadItemOrdered(musicManager, trackURL, new AudioLoadResultHandler() {
@Override
public void trackLoaded(AudioTrack audioTrack) {
// 트랙 대기열 추가
musicManager.scheduler.queue(audioTrack);
textChannel.sendMessageFormat("재생 중인 곡: `%s` (by `%s`)",
audioTrack.getInfo().title,
audioTrack.getInfo().author
).queue();
}
@Override
public void playlistLoaded(AudioPlaylist audioPlaylist) {
// 플레이리스트 처리
AudioTrack firstTrack = audioPlaylist.getSelectedTrack() != null
? audioPlaylist.getSelectedTrack()
: audioPlaylist.getTracks().get(0);
musicManager.scheduler.queue(firstTrack);
textChannel.sendMessageFormat(
"재생 중인 곡: `%s` (by `%s`)",
firstTrack.getInfo().title,
firstTrack.getInfo().author
).queue();
}
@Override
public void noMatches() {
textChannel.sendMessage("일치하는 결과가 없습니다. " + trackURL).queue();
}
@Override
public void loadFailed(FriendlyException e) {
textChannel.sendMessage("재생할 수 없습니다. " + e.getMessage()).queue();
}
});
}
}
3. 노래 명령어 등록
이제 노래 명령어를 등록하고 해당 노래에 관련하여 정보를 찾았을 때 노래를 재생하는 코드를 작성할 것이다.
우선 채팅반응 에서 만들었던 ChattingReaction 클래스에서 play 명령어를 추가해본다.
우선 playMusic 함수를 만들고 "노래" 또는 "play"로 시작할 때 해당 함수를 실행하도록 작성하였다
playMusic 함수에는
1.해당 명령어를 실행하는 유저가 보이스 채널에 접속해 있는지 확인
2. 봇이 보이스 채널에 접속해 있는지 확인
3. 입력받은 메시지를 링크화후 재생
위 로직을 따라 구현하였다.
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.managers.AudioManager;
import org.github.constmine.bot.music.PlayerManager;
public class ChattingReaction extends ListenerAdapter {
@Override
public void onMessageReceived(MessageReceivedEvent event) {
String msg = event.getMessage().getContentRaw();
String[] parts = msg.split(" ", 2);
switch(parts[0]) {
case "ping" :
case "핑" :
event.getChannel().sendMessage("Pong!").queue();
break;
case "대답" :
case "reply" :
event.getMessage().reply("Reply!").queue();
break;
case "노래" :
case "play" :
playMusic(event, parts[1]);
break;
}
}
public void playMusic(MessageReceivedEvent event, String text) {
if(!event.getMember().getVoiceState().inAudioChannel()) {
event.getChannel().sendMessage("소속해 있는 보이스 채널이 없습니다.").queue();
return;
}
if(!event.getGuild().getSelfMember().getVoiceState().inAudioChannel()) {
final AudioManager audioManager = event.getGuild().getAudioManager();
final VoiceChannel memberChannel = (VoiceChannel) event.getMember().getVoiceState().getChannel();
audioManager.openAudioConnection(memberChannel);
}
String link = "ytsearch: " + text + " 노래";
PlayerManager.getINSTANCE().loadAndPlay(event.getChannel().asTextChannel(), link, event.getMember());
}
}
4. 테스트
출력도 잘 되고 노래도 잘 나오는 것을 확인 할 수 있었다.
+) 만약 노래가 잘 나오지 않는다면 로그를 확인해보고 원인을 파악해보자. 라이브러리 추가할때 로그 파악을 위한 라이브러리를 추가하였으므로 오류를 찾을 수 있다.
+) 대부분의 오류가 lavaplayer에서 발생했으므로 lavaplayer 깃허브에서 issue에 들어가보면 다양한 에러에 대한 해결책을 찾을 수 있을 것이다.
'java > DiscordBot' 카테고리의 다른 글
[DiscordBot] Java로 디스코드봇 만들기 6. 슬래쉬 명령어 제작 (0) | 2025.01.13 |
---|---|
[DiscordBot] Java로 디스코드봇 만들기 5. 채팅에 반응하기 (0) | 2024.02.03 |
[DiscordBot] Java로 디스코드봇 만들기 4. 상태 메시지 설정 (0) | 2024.02.02 |
[DiscordBot] Java로 디스코드봇 만들기 3. Github와 연동 (0) | 2024.02.02 |
[DiscordBot] Java로 디스코드봇 만들기 2. 토큰 관리 (0) | 2024.02.01 |