diff --git a/src/main/java/moe/nekojimi/chords/Main.java b/src/main/java/moe/nekojimi/chords/Main.java index 6d53596..273a5c0 100644 --- a/src/main/java/moe/nekojimi/chords/Main.java +++ b/src/main/java/moe/nekojimi/chords/Main.java @@ -40,6 +40,7 @@ public class Main extends ListenerAdapter private MusicHandler musicHandler; private final Downloader downloader; private final Searcher searcher; + private final QueueManager queueManager; private JDA jda; private final Map commands = new HashMap<>(); @@ -131,6 +132,10 @@ public class Main extends ListenerAdapter downloader.setMessageHandler(downloaderMessageHandler); searcher = MetaSearcher.loadYAML(new File("searchproviders.yml")); + // init queue manager + queueManager = new QueueManager(); + + // init commands addCommand(new JoinCommand(this)); addCommand(new LeaveCommand(this)); playCommand = new PlayCommand(this); @@ -244,7 +249,7 @@ public class Main extends ListenerAdapter song.setNumber(trackNumber); trackNumber++; request.setSong(song); - downloader.accept(new Downloader.DownloadTask(request, musicHandler)); + downloader.accept(new Downloader.DownloadTask(request, queueManager)); request.respond("Request pending..."); return song; } @@ -347,6 +352,10 @@ public class Main extends ListenerAdapter { return currentVoiceChannel; } + public QueueManager getQueueManager() + { + return queueManager; + } public int getTrackNumber() { diff --git a/src/main/java/moe/nekojimi/chords/MusicHandler.java b/src/main/java/moe/nekojimi/chords/MusicHandler.java index 8ba294f..5b447fc 100644 --- a/src/main/java/moe/nekojimi/chords/MusicHandler.java +++ b/src/main/java/moe/nekojimi/chords/MusicHandler.java @@ -5,18 +5,12 @@ */ package moe.nekojimi.chords; -import java.io.Closeable; -import java.io.IOException; +import java.io.*; import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.Queue; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.UnsupportedAudioFileException; +import javax.sound.sampled.*; import net.dv8tion.jda.api.audio.AudioSendHandler; import org.apache.commons.io.input.buffer.CircularByteBuffer; @@ -24,17 +18,14 @@ import org.apache.commons.io.input.buffer.CircularByteBuffer; * * @author jimj316 */ -public class MusicHandler implements AudioSendHandler, Closeable, Consumer +public class MusicHandler implements AudioSendHandler, Closeable { - private static final int DESIRED_BUFFER_SIZE = 3840 * 500; - - private final LinkedList songQueue = new LinkedList<>(); - private Song currentSong; - private AudioInputStream din = null; + private QueueManager queueManager; +// private final LinkedList songQueue = new LinkedList<>(); // private final Queue queue = new ConcurrentLinkedQueue<>(); private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024); - private boolean playing = true; + private boolean playing = true; private int byteCount; private boolean arrayErr = false; @@ -45,42 +36,44 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer this.nowPlayingConsumer = nowPlayingConsumer; } - public MusicHandler() - { - } + private Song currentSong; + private TrackPlayer player; - public void addSong(Song song) - { - System.out.println("Song added to queue: " + song.getLocation().getAbsolutePath()); - songQueue.add(song); - if (!canProvide() && playing) - nextSong(); - } + private File debugOutFile; + private BufferedOutputStream debugOut; - public boolean removeSong(int i) + public MusicHandler() { try { - songQueue.remove(i); - return true; - } catch (ArrayIndexOutOfBoundsException ex) + debugOutFile = new File("debug.wav"); + if (debugOutFile.exists()) + 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); - currentSong = null; - return nextSong(true); + nextSong(true); } +// public boolean restartSong() +// { +//// songQueue.addFirst(currentSong); +// currentSong = null; +// return nextSong(true); +// } + private boolean nextSong() { return nextSong(false); @@ -89,35 +82,33 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer public boolean nextSong(boolean immediate) { if (immediate) + { + System.out.println("Immediate next - clearing buffer"); audioBuffer.clear(); + } - AudioInputStream in = null; try { - if (din != null) - { - din.close(); - din = null; - } if (currentSong != null) { if (!currentSong.isKept()) currentSong.delete(); currentSong = null; } - currentSong = songQueue.poll(); + currentSong = queueManager.nextSongNeeded(); if (nowPlayingConsumer != null) nowPlayingConsumer.accept(currentSong); if (currentSong == null) + { + System.out.println("End of queue."); + debugOut.flush(); return false; + } System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath()); arrayErr = false; - in = AudioSystem.getAudioInputStream(currentSong.getLocation()); - AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT; - din = AudioSystem.getAudioInputStream(decodedFormat, in); byteCount = 3840; - fillBuffer(false); - System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes()); + player = new TrackPlayer(currentSong); +// System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes()); return true; } catch (UnsupportedAudioFileException | IOException ex) { @@ -143,76 +134,96 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer @Override public boolean canProvide() { + return player != null && player.has(1); // If we have something in our buffer we can provide it to the send system - return audioBuffer.getCurrentNumberOfBytes() > byteCount && playing; +// return audioBuffer.getCurrentNumberOfBytes() > byteCount && playing; } @Override public ByteBuffer provide20MsAudio() { - fillBuffer(true); - byte[] data = new byte[byteCount]; - 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 + ByteBuffer ret = ByteBuffer.allocate(byteCount); + while (ret.position() < byteCount && player != null) { - // 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("Position: " + ret.position() + " Remaining: " + ret.remaining()); + try + { + ByteBuffer read = player.read(ret.remaining()); + // System.out.println("SAMPLES from player: " + Util.printSamples(read)); // 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 + System.out.println("Read: " + read.remaining()); + ret.put(read); + } catch (TrackPlayer.OutOfInputException | IOException ex) { - Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); - return false; - } - } - return true; - } + System.out.println("Track ended, starting next."); + boolean foundNext = nextSong(); - public Queue getSongQueue() - { - return songQueue; - } + if (!foundNext) + { + System.out.println("Out of tracks!"); + break; + } + } + } + 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 + return ret; + + } + +// 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() { return currentSong; @@ -227,14 +238,6 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer @Override public void close() throws IOException { - din.close(); - } - - @Override - public void accept(Song t) - { - addSong(t); } - } diff --git a/src/main/java/moe/nekojimi/chords/Playlist.java b/src/main/java/moe/nekojimi/chords/Playlist.java index 0e1f335..2ba65d0 100644 --- a/src/main/java/moe/nekojimi/chords/Playlist.java +++ b/src/main/java/moe/nekojimi/chords/Playlist.java @@ -61,4 +61,9 @@ public class Playlist return songs; } + Song getNextSong() + { + throw new UnsupportedOperationException("Not supported yet."); + } + } diff --git a/src/main/java/moe/nekojimi/chords/QueueManager.java b/src/main/java/moe/nekojimi/chords/QueueManager.java new file mode 100644 index 0000000..2382ca1 --- /dev/null +++ b/src/main/java/moe/nekojimi/chords/QueueManager.java @@ -0,0 +1,140 @@ +/* + * 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 . + */ +package moe.nekojimi.chords; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.function.Consumer; + +/** + * + * @author jimj316 + */ +public class QueueManager implements Consumer +{ + + private Mode mode; + private final Queue jukeboxQueue; + private Playlist playlist; + private MusicHandler handler; + + public QueueManager() + { + jukeboxQueue = new LinkedList<>(); + } + + @Override + public void accept(Song t) + { + jukeboxQueue.add(t); + + 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 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; + } +} diff --git a/src/main/java/moe/nekojimi/chords/TrackPlayer.java b/src/main/java/moe/nekojimi/chords/TrackPlayer.java new file mode 100644 index 0000000..4826ea6 --- /dev/null +++ b/src/main/java/moe/nekojimi/chords/TrackPlayer.java @@ -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 + { + + } + +} diff --git a/src/main/java/moe/nekojimi/chords/Util.java b/src/main/java/moe/nekojimi/chords/Util.java new file mode 100644 index 0000000..2c181ba --- /dev/null +++ b/src/main/java/moe/nekojimi/chords/Util.java @@ -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 . + */ + +/** + * + * @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(); + } +} diff --git a/src/main/java/moe/nekojimi/chords/commands/QueueCommand.java b/src/main/java/moe/nekojimi/chords/commands/QueueCommand.java index e01587d..e20f8f8 100644 --- a/src/main/java/moe/nekojimi/chords/commands/QueueCommand.java +++ b/src/main/java/moe/nekojimi/chords/commands/QueueCommand.java @@ -43,7 +43,7 @@ public class QueueCommand extends Command else message += ":mute: **Not playing anything right now.**\n"; - final Queue songQueue = bot.getMusicHandler().getSongQueue(); + final Queue songQueue = bot.getQueueManager().getJukeboxQueue(); if (!songQueue.isEmpty()) { message += "__Ready to play:__\n"; diff --git a/src/main/java/moe/nekojimi/chords/commands/RemoveCommand.java b/src/main/java/moe/nekojimi/chords/commands/RemoveCommand.java index 40679a0..b68b682 100644 --- a/src/main/java/moe/nekojimi/chords/commands/RemoveCommand.java +++ b/src/main/java/moe/nekojimi/chords/commands/RemoveCommand.java @@ -34,8 +34,8 @@ public class RemoveCommand extends Command try { int i = Integer.parseInt(arg.get(0)); - boolean removed = bot.getMusicHandler().removeSong(i - 1); - final int size = bot.getMusicHandler().getSongQueue().size(); + boolean removed = bot.getQueueManager().removeSong(i - 1); + final int size = bot.getQueueManager().getJukeboxQueue().size(); if (removed) event.getChannel().sendMessage("Song removed.").queue(); else if (size > 1) diff --git a/src/main/java/moe/nekojimi/chords/commands/RestartCommand.java b/src/main/java/moe/nekojimi/chords/commands/RestartCommand.java index 5cdac33..ee17125 100644 --- a/src/main/java/moe/nekojimi/chords/commands/RestartCommand.java +++ b/src/main/java/moe/nekojimi/chords/commands/RestartCommand.java @@ -32,7 +32,7 @@ public class RestartCommand extends Command public void call(GuildMessageReceivedEvent event, List arg) { // TODO: this needs to clear the current data queue - boolean ok = bot.getMusicHandler().restartSong(); + boolean ok = bot.getQueueManager().restartSong(); if (ok) event.getChannel().sendMessage("Restarted current song!").queue(); else