Compare commits

..

6 Commits

Author SHA1 Message Date
Nekojimi 4ec47df625 Fix issues with queue not playing. 2022-06-19 23:25:20 +01:00
Nekojimi 9a6520f426 Merge commit '594ffa9ba9bb36e2505a6cf25423817a2f3f0bf3' 2022-06-19 20:23:34 +01:00
Nekojimi 594ffa9ba9 Move song queue to new QueueManager class. 2022-06-19 19:40:43 +01:00
Nekojimi d7c48b38fa Add debug method. 2022-06-19 00:00:45 +01:00
Nekojimi 795e31aa62 Fix playback issues caused by new track player.
Submitted ByteBuffer needed to be rewound, and provide method should return true until there are zero bytes left in the buffer.
2022-06-19 00:00:15 +01:00
Nekojimi f64ed119a7 WIP: work on new track player system. 2022-04-19 23:44:35 +01:00
9 changed files with 469 additions and 124 deletions

View File

@ -40,6 +40,7 @@ public class Main extends ListenerAdapter
private MusicHandler musicHandler; private MusicHandler musicHandler;
private final Downloader downloader; private final Downloader downloader;
private final Searcher searcher; private final Searcher searcher;
private final QueueManager queueManager;
private JDA jda; private JDA jda;
private final Map<String, Command> commands = new HashMap<>(); private final Map<String, Command> commands = new HashMap<>();
@ -131,6 +132,10 @@ public class Main extends ListenerAdapter
downloader.setMessageHandler(downloaderMessageHandler); downloader.setMessageHandler(downloaderMessageHandler);
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml")); searcher = MetaSearcher.loadYAML(new File("searchproviders.yml"));
// init queue manager
queueManager = new QueueManager();
// init commands
addCommand(new JoinCommand(this)); addCommand(new JoinCommand(this));
addCommand(new LeaveCommand(this)); addCommand(new LeaveCommand(this));
playCommand = new PlayCommand(this); playCommand = new PlayCommand(this);
@ -244,7 +249,7 @@ public class Main extends ListenerAdapter
song.setNumber(trackNumber); song.setNumber(trackNumber);
trackNumber++; trackNumber++;
request.setSong(song); request.setSong(song);
downloader.accept(new Downloader.DownloadTask(request, musicHandler)); downloader.accept(new Downloader.DownloadTask(request, queueManager));
request.respond("Request pending..."); request.respond("Request pending...");
return song; return song;
} }
@ -290,6 +295,7 @@ public class Main extends ListenerAdapter
// Get an audio manager for this guild, this will be created upon first use for each guild // Get an audio manager for this guild, this will be created upon first use for each guild
AudioManager audioManager = guild.getAudioManager(); AudioManager audioManager = guild.getAudioManager();
musicHandler = new MusicHandler(); musicHandler = new MusicHandler();
queueManager.setHandler(musicHandler);
// Create our Send/Receive handler for the audio connection // Create our Send/Receive handler for the audio connection
// EchoHandler handler = new EchoHandler(); // EchoHandler handler = new EchoHandler();
@ -347,6 +353,10 @@ public class Main extends ListenerAdapter
{ {
return currentVoiceChannel; return currentVoiceChannel;
} }
public QueueManager getQueueManager()
{
return queueManager;
}
public int getTrackNumber() public int getTrackNumber()
{ {

View File

@ -5,18 +5,12 @@
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.io.Closeable; import java.io.*;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.sound.sampled.AudioFormat; import javax.sound.sampled.*;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import net.dv8tion.jda.api.audio.AudioSendHandler; import net.dv8tion.jda.api.audio.AudioSendHandler;
import org.apache.commons.io.input.buffer.CircularByteBuffer; import org.apache.commons.io.input.buffer.CircularByteBuffer;
@ -24,17 +18,14 @@ import org.apache.commons.io.input.buffer.CircularByteBuffer;
* *
* @author jimj316 * @author jimj316
*/ */
public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Song> public class MusicHandler implements AudioSendHandler, Closeable
{ {
private static final int DESIRED_BUFFER_SIZE = 3840 * 500; private QueueManager queueManager;
// private final LinkedList<Song> songQueue = new LinkedList<>();
private final LinkedList<Song> songQueue = new LinkedList<>();
private Song currentSong;
private AudioInputStream din = null;
// private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>(); // private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024); private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
private boolean playing = true; private boolean shouldPlay = true;
private int byteCount; private int byteCount;
private boolean arrayErr = false; private boolean arrayErr = false;
@ -45,42 +36,44 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Song>
this.nowPlayingConsumer = nowPlayingConsumer; this.nowPlayingConsumer = nowPlayingConsumer;
} }
private Song currentSong;
private TrackPlayer player;
private File debugOutFile;
private BufferedOutputStream debugOut;
public MusicHandler() public MusicHandler()
{
}
public void addSong(Song song)
{
System.out.println("Song added to queue: " + song.getLocation().getAbsolutePath());
songQueue.add(song);
if (!canProvide() && playing)
nextSong();
}
public boolean removeSong(int i)
{ {
try try
{ {
songQueue.remove(i); debugOutFile = new File("debug.wav");
return true; if (debugOutFile.exists())
} catch (ArrayIndexOutOfBoundsException ex) debugOutFile.delete();
debugOutFile.createNewFile();
debugOut = new BufferedOutputStream(new FileOutputStream(debugOutFile));
} catch (IOException ex)
{ {
return false; Logger.getLogger(MusicHandler.class.getName()).log(Level.SEVERE, null, ex);
} }
} }
public boolean removeSong(Song song) void setQueueManager(QueueManager manager)
{ {
return songQueue.remove(song); queueManager = manager;
} }
public boolean restartSong() public void playNext()
{ {
songQueue.addFirst(currentSong); nextSong(true);
currentSong = null;
return nextSong(true);
} }
// public boolean restartSong()
// {
//// songQueue.addFirst(currentSong);
// currentSong = null;
// return nextSong(true);
// }
private boolean nextSong() private boolean nextSong()
{ {
return nextSong(false); return nextSong(false);
@ -89,35 +82,33 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Song>
public boolean nextSong(boolean immediate) public boolean nextSong(boolean immediate)
{ {
if (immediate) if (immediate)
{
System.out.println("Immediate next - clearing buffer");
audioBuffer.clear(); audioBuffer.clear();
}
AudioInputStream in = null;
try try
{ {
if (din != null)
{
din.close();
din = null;
}
if (currentSong != null) if (currentSong != null)
{ {
if (!currentSong.isKept()) if (!currentSong.isKept())
currentSong.delete(); currentSong.delete();
currentSong = null; currentSong = null;
} }
currentSong = songQueue.poll(); currentSong = queueManager.nextSongNeeded();
if (nowPlayingConsumer != null) if (nowPlayingConsumer != null)
nowPlayingConsumer.accept(currentSong); nowPlayingConsumer.accept(currentSong);
if (currentSong == null) if (currentSong == null)
{
System.out.println("End of queue.");
debugOut.flush();
return false; return false;
}
System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath()); System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath());
arrayErr = false; arrayErr = false;
in = AudioSystem.getAudioInputStream(currentSong.getLocation());
AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT;
din = AudioSystem.getAudioInputStream(decodedFormat, in);
byteCount = 3840; byteCount = 3840;
fillBuffer(false); player = new TrackPlayer(currentSong);
System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes()); // System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes());
return true; return true;
} catch (UnsupportedAudioFileException | IOException ex) } catch (UnsupportedAudioFileException | IOException ex)
{ {
@ -130,89 +121,114 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Song>
public boolean isPlaying() public boolean isPlaying()
{ {
return playing; return player != null;
} }
public void setPlaying(boolean playing) public boolean isShouldPlay()
{ {
if (!this.playing && playing) return shouldPlay;
}
public void setShouldPlay(boolean shouldPlay)
{
if (!this.shouldPlay && shouldPlay)
nextSong(); nextSong();
this.playing = playing; this.shouldPlay = shouldPlay;
} }
@Override @Override
public boolean canProvide() public boolean canProvide()
{ {
return player != null && player.has(1);
// If we have something in our buffer we can provide it to the send system // If we have something in our buffer we can provide it to the send system
return audioBuffer.getCurrentNumberOfBytes() > byteCount && playing; // return audioBuffer.getCurrentNumberOfBytes() > byteCount && shouldPlay;
} }
@Override @Override
public ByteBuffer provide20MsAudio() public ByteBuffer provide20MsAudio()
{ {
fillBuffer(true); ByteBuffer ret = ByteBuffer.allocate(byteCount);
byte[] data = new byte[byteCount]; while (ret.position() < byteCount && player != null)
audioBuffer.read(data, 0, data.length);
// byte[] data = queue.poll();
return ByteBuffer.wrap(data); // Wrap this in a java.nio.ByteBuffer
}
private void fillBuffer(boolean canSkip)
{
// use what we have in our buffer to send audio as PCM
while (audioBuffer.getCurrentNumberOfBytes() < DESIRED_BUFFER_SIZE)
if (!readData())
if (!canSkip || !nextSong())
break;
}
private boolean readData()
{
if (din == null)
return false;
try
{ {
// if (din.available() == 0) // System.out.println("Position: " + ret.position() + " Remaining: " + ret.remaining());
// return false; try
int bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
int space = audioBuffer.getSpace();
if (din.available() > 0 && din.available() < bytesToRead)
bytesToRead = din.available();
if (bytesToRead > space)
bytesToRead = space;
if (bytesToRead == 0)
return false;
byte[] bytes = new byte[bytesToRead];
// byte[] bytes = din.readNBytes(bytesToRead);
int read = din.read(bytes);
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
if (read < 0)
return false;
// queue.add(bytes);
audioBuffer.add(bytes, 0, read);
} catch (IOException ex)
{
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
return false;
} catch (ArrayIndexOutOfBoundsException ex)
{
if (!arrayErr)
arrayErr = true;
else
{ {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); ByteBuffer read = player.read(ret.remaining());
return false; // System.out.println("SAMPLES from player: " + Util.printSamples(read));
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
System.out.println("Read: " + read.remaining());
ret.put(read);
} catch (TrackPlayer.OutOfInputException | IOException ex)
{
System.out.println("Track ended, starting next.");
boolean foundNext = nextSong();
if (!foundNext)
{
System.out.println("Out of tracks!");
break;
}
} }
} }
return true; System.out.println("Buffer filled, submitting.");
} ret.rewind(); // required apparently, if returned buf has pos > 0 you get silence
assert ret.hasArray(); // output MUST be array backed
public Queue<Song> getSongQueue() return ret;
{
return songQueue;
} }
// private void fillBuffer(boolean canSkip)
// {
// // use what we have in our buffer to send audio as PCM
// while (audioBuffer.getCurrentNumberOfBytes() < DESIRED_BUFFER_SIZE)
// if (!readData())
// if (!canSkip || !nextSong())
// break;
// }
//
// private boolean readData()
// {
// if (din == null)
// return false;
// try
// {
// // if (din.available() == 0)
// // return false;
// int bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
// int space = audioBuffer.getSpace();
// if (din.available() > 0 && din.available() < bytesToRead)
// bytesToRead = din.available();
// if (bytesToRead > space)
// bytesToRead = space;
// if (bytesToRead == 0)
// return false;
// byte[] bytes = new byte[bytesToRead];
// // byte[] bytes = din.readNBytes(bytesToRead);
// int read = din.read(bytes);
//// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
// if (read < 0)
// return false;
//// queue.add(bytes);
//
// audioBuffer.add(bytes, 0, read);
// } catch (IOException ex)
// {
// Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
// return false;
// } catch (ArrayIndexOutOfBoundsException ex)
// {
// if (!arrayErr)
// arrayErr = true;
// else
// {
// Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
// return false;
// }
// }
// return true;
// }
public Song getCurrentSong() public Song getCurrentSong()
{ {
return currentSong; return currentSong;
@ -227,14 +243,6 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Song>
@Override @Override
public void close() throws IOException public void close() throws IOException
{ {
din.close();
} }
@Override
public void accept(Song t)
{
addSong(t);
}
} }

View File

@ -61,4 +61,9 @@ public class Playlist
return songs; return songs;
} }
Song getNextSong()
{
throw new UnsupportedOperationException("Not supported yet.");
}
} }

View File

@ -0,0 +1,141 @@
/*
* Copyright (C) 2022 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package moe.nekojimi.chords;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer;
/**
*
* @author jimj316
*/
public class QueueManager implements Consumer<Song>
{
private Mode mode;
private final Queue<Song> jukeboxQueue;
private Playlist playlist;
private MusicHandler handler;
public QueueManager()
{
jukeboxQueue = new LinkedList<>();
}
@Override
public void accept(Song t)
{
jukeboxQueue.add(t);
if (!handler.isPlaying())
handler.playNext();
}
/**
* Called by the music handler when the current song has ended, or if
* playNext is called with nothing playing.
*
* @return the next track to play, or null to stop playing.
*/
public Song nextSongNeeded()
{
// if there's anything in the queue, play that first
if (!jukeboxQueue.isEmpty())
{
return jukeboxQueue.poll();
}
// otherwise if there's a playlist, shuffle from that
else if (playlist != null)
{
return playlist.getNextSong();
}
// otherwise stop playing
else
return null;
}
public MusicHandler getHandler()
{
return handler;
}
public void addSong(Song song)
{
System.out.println("Song added to queue: " + song.getLocation().getAbsolutePath());
jukeboxQueue.add(song);
}
public boolean removeSong(int i)
{
try
{
return jukeboxQueue.remove((Song) jukeboxQueue.toArray()[i]);
} catch (ArrayIndexOutOfBoundsException ex)
{
return false;
}
}
public boolean removeSong(Song song)
{
return jukeboxQueue.remove(song);
}
public void setHandler(MusicHandler handler)
{
this.handler = handler;
handler.setQueueManager(this);
}
public Queue<Song> getJukeboxQueue()
{
return jukeboxQueue;
}
public Playlist getPlaylist()
{
return playlist;
}
public void setPlaylist(Playlist playlist)
{
this.playlist = playlist;
}
public Mode getMode()
{
return mode;
}
public void setMode(Mode mode)
{
this.mode = mode;
}
public boolean restartSong()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public enum Mode
{
JUKEBOX,
PLAYLIST;
}
}

View File

@ -0,0 +1,144 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package moe.nekojimi.chords;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import org.apache.commons.io.input.buffer.CircularByteBuffer;
/**
*
* @author jimj316
*/
public class TrackPlayer implements Closeable
{
private static final int DESIRED_BUFFER_SIZE = 3840 * 500;
private static final int MAX_READ_FAILS = 3;
private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
private final AudioInputStream input;
private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam
public TrackPlayer(Song song) throws UnsupportedAudioFileException, IOException
{
AudioInputStream in = AudioSystem.getAudioInputStream(song.getLocation());
AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT;
input = AudioSystem.getAudioInputStream(decodedFormat, in);
fillBuffer(false);
}
public TrackPlayer(AudioInputStream input) throws IOException
{
this.input = input;
fillBuffer(false);
}
public boolean has(int byteCount)
{
// return true;
return audioBuffer.getCurrentNumberOfBytes() >= byteCount;
}
public ByteBuffer read(int length) throws IOException
{
boolean filled = fillBuffer(true);
// if (!filled)
// throw new OutOfInputException();
int toRead = Math.min(length, audioBuffer.getCurrentNumberOfBytes());
System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes());
if (toRead <= 0)
throw new OutOfInputException();
byte[] data = new byte[toRead];
audioBuffer.read(data, 0, data.length);
// byte[] data = queue.poll();
return ByteBuffer.wrap(data); // Wrap this in a java.nio.ByteBuffer
}
/**
*
* @param canSkip
* @return true if the buffer is not empty; false otherwise
*/
private boolean fillBuffer(boolean canSkip) throws IOException
{
int fails = 0;
// use what we have in our buffer to send audio as PCM
while (audioBuffer.getCurrentNumberOfBytes() < DESIRED_BUFFER_SIZE)
{
boolean read = readData();
if (!read && !canSkip)
return false;
else if (fails < MAX_READ_FAILS)
fails++;
else
return false;
}
return true;
}
/**
*
* @return true if any data was read; false otherwise
*/
private boolean readData() throws IOException
{
if (input == null)
return false;
try
{
// if (din.available() == 0)
// return false;
int bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
int space = audioBuffer.getSpace();
if (input.available() > 0 && input.available() < bytesToRead)
bytesToRead = input.available();
if (bytesToRead > space)
bytesToRead = space;
if (bytesToRead == 0)
return false;
byte[] bytes = new byte[bytesToRead];
// byte[] bytes = din.readNBytes(bytesToRead);
int read = input.read(bytes);
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
if (read < 0)
return false;
// queue.add(bytes);
audioBuffer.add(bytes, 0, read);
// System.out.println("SAMPLES player buff: " + Util.printSamples(audioBuffer));
} catch (ArrayIndexOutOfBoundsException ex)
{
if (!arrayErr)
arrayErr = true;
else
throw ex;
}
return true;
}
public void close() throws IOException
{
input.close();
}
public static class OutOfInputException extends RuntimeException
{
}
}

View File

@ -0,0 +1,37 @@
package moe.nekojimi.chords;
import java.nio.ByteBuffer;
/*
* Copyright (C) 2022 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*
* @author jimj316
*/
public class Util
{
public static String printSamples(ByteBuffer buf)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < buf.limit(); i += 128)
{
sb.append(String.format("%x ", buf.get(i)));
}
return sb.toString();
}
}

View File

@ -43,7 +43,7 @@ public class QueueCommand extends Command
else else
message += ":mute: **Not playing anything right now.**\n"; message += ":mute: **Not playing anything right now.**\n";
final Queue<Song> songQueue = bot.getMusicHandler().getSongQueue(); final Queue<Song> songQueue = bot.getQueueManager().getJukeboxQueue();
if (!songQueue.isEmpty()) if (!songQueue.isEmpty())
{ {
message += "__Ready to play:__\n"; message += "__Ready to play:__\n";

View File

@ -34,8 +34,8 @@ public class RemoveCommand extends Command
try try
{ {
int i = Integer.parseInt(arg.get(0)); int i = Integer.parseInt(arg.get(0));
boolean removed = bot.getMusicHandler().removeSong(i - 1); boolean removed = bot.getQueueManager().removeSong(i - 1);
final int size = bot.getMusicHandler().getSongQueue().size(); final int size = bot.getQueueManager().getJukeboxQueue().size();
if (removed) if (removed)
event.getChannel().sendMessage("Song removed.").queue(); event.getChannel().sendMessage("Song removed.").queue();
else if (size > 1) else if (size > 1)

View File

@ -32,7 +32,7 @@ public class RestartCommand extends Command
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(GuildMessageReceivedEvent event, List<String> arg)
{ {
// TODO: this needs to clear the current data queue // TODO: this needs to clear the current data queue
boolean ok = bot.getMusicHandler().restartSong(); boolean ok = bot.getQueueManager().restartSong();
if (ok) if (ok)
event.getChannel().sendMessage("Restarted current song!").queue(); event.getChannel().sendMessage("Restarted current song!").queue();
else else