From b515b1b8b175decbb77eba8d1c0bc1cf3d7f958b Mon Sep 17 00:00:00 2001 From: ChomeNS Date: Mon, 20 Mar 2023 12:42:07 +0700 Subject: [PATCH] ADD AN EXTREMELY BROKEN MUSIC --- .../me/chayapak1/chomensbot_mabe/Bot.java | 1 + .../commands/MusicCommand.java | 174 ++++++++ .../plugins/CommandHandlerPlugin.java | 1 + .../chomensbot_mabe/plugins/CorePlugin.java | 19 + .../plugins/MusicPlayerPlugin.java | 195 +++++++++ .../plugins/SelfCarePlugin.java | 6 + .../chomensbot_mabe/song/Instrument.java | 48 +++ .../chomensbot_mabe/song/MidiConverter.java | 374 ++++++++++++++++++ .../chomensbot_mabe/song/NBSConverter.java | 203 ++++++++++ .../chayapak1/chomensbot_mabe/song/Note.java | 28 ++ .../chayapak1/chomensbot_mabe/song/Song.java | 165 ++++++++ .../song/SongLoaderException.java | 24 ++ .../song/SongLoaderThread.java | 77 ++++ .../util/DownloadUtilities.java | 61 +++ 14 files changed, 1376 insertions(+) create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/commands/MusicCommand.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/plugins/MusicPlayerPlugin.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/Instrument.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/MidiConverter.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/NBSConverter.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/Note.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/Song.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderException.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderThread.java create mode 100644 src/main/java/me/chayapak1/chomensbot_mabe/util/DownloadUtilities.java diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/Bot.java b/src/main/java/me/chayapak1/chomensbot_mabe/Bot.java index 28160f8..8f77bf0 100644 --- a/src/main/java/me/chayapak1/chomensbot_mabe/Bot.java +++ b/src/main/java/me/chayapak1/chomensbot_mabe/Bot.java @@ -40,6 +40,7 @@ public class Bot { @Getter private final CommandHandlerPlugin commandHandler = new CommandHandlerPlugin(); @Getter private final ChatCommandHandlerPlugin chatCommandHandler = new ChatCommandHandlerPlugin(this); @Getter private final HashingPlugin hashing = new HashingPlugin(this); + @Getter private final MusicPlayerPlugin music = new MusicPlayerPlugin(this); public Bot (String host, int port, int reconnectDelay, String username, List allBots) { this.host = host; diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/commands/MusicCommand.java b/src/main/java/me/chayapak1/chomensbot_mabe/commands/MusicCommand.java new file mode 100644 index 0000000..fc60cfb --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/commands/MusicCommand.java @@ -0,0 +1,174 @@ +package me.chayapak1.chomensbot_mabe.commands; + +import me.chayapak1.chomensbot_mabe.Bot; +import me.chayapak1.chomensbot_mabe.command.Command; +import me.chayapak1.chomensbot_mabe.command.CommandContext; +import me.chayapak1.chomensbot_mabe.plugins.MusicPlayerPlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MusicCommand implements Command { + private Path root; + + public String name() { return "music"; } + + public String description() { + return "Plays music"; + } + + public List usage() { + final List usages = new ArrayList<>(); + usages.add("play <{song|URL}>"); + usages.add("stop"); + usages.add("loop "); + usages.add("list [{directory}]"); + usages.add("skip"); + usages.add("nowplaying"); + usages.add("queue"); + + return usages; + } + + public List alias() { + final List aliases = new ArrayList<>(); + aliases.add("song"); + + return aliases; + } + + public int trustLevel() { + return 0; + } + + public Component execute(CommandContext context, String[] args, String[] fullArgs) { + root = Path.of(context.bot().music().SONG_DIR.getPath()); + switch (args[0]) { + case "play" -> { + return play(context, args); + } + case "stop" -> stop(context); + case "loop" -> { + return loop(context, args); + } + case "list" -> { + return list(context, args); + } + case "skip" -> { + return skip(context); + } + } + + return Component.text("success"); + } + + public Component play (CommandContext context, String[] args) { + final Bot bot = context.bot(); + final MusicPlayerPlugin player = context.bot().music(); + + final String _path = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + final Path path = Path.of(root.toString(), _path); + + bot.logger().log(path.toString()); + + try { + if (!path.toString().contains("http")) player.loadSong(path); + else player.loadSong(new URL(_path)); + } catch (MalformedURLException e) { + return Component.text("Invalid URL").color(NamedTextColor.RED); + } + + return Component.text("success"); + } + + public void stop (CommandContext context) { + final Bot bot = context.bot(); + bot.music().stopPlaying(); + bot.music().songQueue().clear(); + } + + public Component loop (CommandContext context, String[] args) { + final Bot bot = context.bot(); + + int loop; + switch (args[1]) { + case "off" -> loop = 0; + case "current" -> loop = 1; + case "all" -> loop = 2; + default -> { + return Component.text("Invalid argument"); + } + } + + bot.music().currentSong().looping = loop; + + return Component.text("success"); + } + + public Component list (CommandContext context, String[] args) { + final String prefix = context.bot().chatCommandHandler().prefix(); + + final Path _path = Path.of(root.toString(), String.join(" ", args)); + final Path path = (args.length < 2) ? root : _path; + + final String[] filenames = path.toFile().list(); + if (filenames == null) return Component.text("Directory doesn't exist").color(NamedTextColor.RED); + + final List list = new ArrayList<>(); + int i = 0; + for (String filename : filenames) { + final String pathString = path.toString(); + final File file = new File(Paths.get(pathString, filename).toUri()); + + Path location; + try { + location = path; + } catch (IllegalArgumentException e) { + location = Paths.get(""); // wtf mabe + } + final String joinedPath = (args.length < 2) ? filename : Paths.get(location.getFileName().toString(), filename).toString(); + list.add( + Component + .text(filename, (i++ & 1) == 0 ? NamedTextColor.YELLOW : NamedTextColor.GOLD) + .clickEvent( + ClickEvent.suggestCommand( + prefix + + "music" + // ? How do I make this dynamic? + (file.isFile() ? " play " : " list ") + + joinedPath.replace("'", "\\'") + ) + ) + ); + } + + final Component component = Component.join(JoinConfiguration.separator(Component.space()), list); + context.sendOutput(component); + + return Component.text("success"); + } + + public Component skip (CommandContext context) { + final MusicPlayerPlugin music = context.bot().music(); + if (music.currentSong() == null) return Component.text("No song is currently playing").color(NamedTextColor.RED); + + context.sendOutput( + Component.empty() + .append(Component.text("Skipping ")) + .append(music.currentSong().name.color(NamedTextColor.GOLD)) + ); + + music.stopPlaying(); + + return Component.text("success"); + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CommandHandlerPlugin.java b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CommandHandlerPlugin.java index f823a1b..ae80574 100644 --- a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CommandHandlerPlugin.java +++ b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CommandHandlerPlugin.java @@ -27,6 +27,7 @@ public class CommandHandlerPlugin { registerCommand(new TestCommand()); registerCommand(new ThrowCommand()); registerCommand(new ValidateCommand()); + registerCommand(new MusicCommand()); } public void registerCommand (Command command) { diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CorePlugin.java b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CorePlugin.java index 2ed525b..ee8099a 100644 --- a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CorePlugin.java +++ b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/CorePlugin.java @@ -3,11 +3,19 @@ package me.chayapak1.chomensbot_mabe.plugins; import com.github.steveice10.mc.protocol.data.game.level.block.CommandBlockMode; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCommandBlockPacket; import com.nukkitx.math.vector.Vector3i; +import lombok.Getter; import me.chayapak1.chomensbot_mabe.Bot; +import java.util.ArrayList; +import java.util.List; + public class CorePlugin extends PositionPlugin.PositionListener { private final Bot bot; + @Getter private final List listeners = new ArrayList<>(); + + @Getter private boolean ready = false; + public final Vector3i coreStart = Vector3i.from(0, 0, 0); public final Vector3i coreEnd = Vector3i.from(15, 2, 15); @@ -80,6 +88,11 @@ public class CorePlugin extends PositionPlugin.PositionListener { bot.position().position().getZ() ); refill(); + + if (!ready) { + ready = true; + for (Listener listener : listeners) listener.ready(); + } } public void refill () { @@ -96,4 +109,10 @@ public class CorePlugin extends PositionPlugin.PositionListener { ); bot.chat().send(command); } + + public static class Listener { + public void ready () {} + } + + public void addListener (Listener listener) { listeners.add(listener); } } diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/MusicPlayerPlugin.java b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/MusicPlayerPlugin.java new file mode 100644 index 0000000..fcf78fb --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/MusicPlayerPlugin.java @@ -0,0 +1,195 @@ +package me.chayapak1.chomensbot_mabe.plugins; + +import com.github.steveice10.packetlib.event.session.DisconnectedEvent; +import com.github.steveice10.packetlib.event.session.SessionAdapter; +import lombok.Getter; +import lombok.Setter; +import me.chayapak1.chomensbot_mabe.Bot; +import me.chayapak1.chomensbot_mabe.song.Note; +import me.chayapak1.chomensbot_mabe.song.Song; +import me.chayapak1.chomensbot_mabe.song.SongLoaderException; +import me.chayapak1.chomensbot_mabe.song.SongLoaderThread; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; + +import java.io.File; +import java.net.URL; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class MusicPlayerPlugin extends SessionAdapter { + private final Bot bot; + + private ScheduledFuture futurePlayTask; + + public static final String SELECTOR = "@a[tag=!nomusic,tag=!chomens_bot_nomusic]"; + public static File SONG_DIR = new File("songs"); + static { + if (!SONG_DIR.exists()) { + SONG_DIR.mkdir(); + } + } + + @Getter @Setter private Song currentSong; + @Getter @Setter private LinkedList songQueue = new LinkedList<>(); + @Getter @Setter private SongLoaderThread loaderThread; + private int ticksUntilPausedActionbar = 20; + private final String bossbarName = "chomens_bot:music"; // maybe make this in the config? + + public MusicPlayerPlugin (Bot bot) { + this.bot = bot; + bot.addListener(this); + bot.core().addListener(new CorePlugin.Listener() { + public void ready () { coreReady(); } + }); + } + + public void loadSong (Path location) { + if (loaderThread != null) { + bot.chat().tellraw(Component.translatable("Already loading a song, can't load another", NamedTextColor.RED)); + return; + } + + try { + final SongLoaderThread _loaderThread = new SongLoaderThread(location, bot); + bot.chat().tellraw(Component.translatable("Loading %s", Component.text(location.getFileName().toString(), NamedTextColor.GOLD))); + _loaderThread.start(); + loaderThread = _loaderThread; + } catch (SongLoaderException e) { + e.printStackTrace(); + bot.chat().tellraw(Component.translatable("Failed to load song: %s", e.message()).color(NamedTextColor.RED)); + loaderThread = null; + } + } + + public void loadSong (URL location) { + if (loaderThread != null) { + bot.chat().tellraw(Component.translatable("Already loading a song, can't load another", NamedTextColor.RED)); + return; + } + + try { + final SongLoaderThread _loaderThread = new SongLoaderThread(location, bot); + bot.chat().tellraw(Component.translatable("Loading %s", Component.text(location.toString(), NamedTextColor.GOLD))); + _loaderThread.start(); + loaderThread = _loaderThread; + } catch (SongLoaderException e) { + bot.chat().tellraw(Component.translatable("Failed to load song: %s", e.message()).color(NamedTextColor.RED)); + loaderThread = null; + } + } + + public void coreReady () { + final Runnable playTask = () -> { + if (loaderThread != null && !loaderThread.isAlive()) { + if (loaderThread.exception != null) { + bot.chat().tellraw(Component.translatable("Failed to load song: %s", loaderThread.exception.message()).color(NamedTextColor.RED)); + } else { + songQueue.add(loaderThread.song); + bot.chat().tellraw(Component.translatable("Added %s to the song queue", Component.empty().append(loaderThread.song.name).color(NamedTextColor.GOLD))); + } + loaderThread = null; + } + + if (currentSong == null) { + if (songQueue.size() == 0) return; + + currentSong = songQueue.poll(); + bot.chat().tellraw(Component.translatable("Now playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.GOLD))); + currentSong.play(); + } + + if (currentSong.paused && ticksUntilPausedActionbar-- < 0) return; + else ticksUntilPausedActionbar = 20; + + bot.core().run("minecraft:bossbar add " + bossbarName + " \"\""); + bot.core().run("minecraft:bossbar set " + bossbarName + " players " + SELECTOR); + bot.core().run("minecraft:bossbar set " + bossbarName + " name " + GsonComponentSerializer.gson().serialize(generateBossbar())); + bot.core().run("minecraft:bossbar set " + bossbarName + " color yellow"); + bot.core().run("minecraft:bossbar set " + bossbarName + " visible true"); + bot.core().run("minecraft:bossbar set " + bossbarName + " value " + (int) Math.floor(currentSong.time)); + bot.core().run("minecraft:bossbar set " + bossbarName + " max " + currentSong.length); + + if (currentSong.paused) return; + + handlePlaying(); + + if (currentSong.finished()) { + removeBossbar(); + bot.chat().tellraw(Component.translatable("Finished playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.GOLD))); + currentSong = null; + } + }; + + futurePlayTask = bot.executor().scheduleAtFixedRate(playTask, 50, 50, TimeUnit.MILLISECONDS); + + if (currentSong != null) currentSong.play(); + } + + public void removeBossbar () { + bot.core().run("minecraft:bossbar remove " + bossbarName); + } + + public Component generateBossbar () { + Component component = Component.empty() + .append(Component.empty().append(currentSong.name).color(NamedTextColor.GREEN)) + .append(Component.text(" | ").color(NamedTextColor.DARK_GRAY)) + .append(Component.translatable("%s / %s", formatTime(currentSong.time).color(NamedTextColor.GRAY), formatTime(currentSong.length).color(NamedTextColor.GRAY)).color(NamedTextColor.DARK_GRAY)) + .append(Component.text(" | ").color(NamedTextColor.DARK_GRAY)) + .append(Component.translatable("%s / %s", Component.text(currentSong.position, NamedTextColor.GRAY), Component.text(currentSong.size(), NamedTextColor.GRAY)).color(NamedTextColor.DARK_GRAY)); + + if (currentSong.paused) { + return component + .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.text("Paused", NamedTextColor.GREEN)); + } + + if (currentSong.looping > 0) { + final int looping = currentSong().looping; + return component + .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.translatable("Looping " + ((looping == 1) ? "current" : "all"), NamedTextColor.DARK_GREEN)); + } + + return component; + } + + public Component formatTime (long millis) { + final int seconds = (int) millis / 1000; + + final String minutePart = String.valueOf(seconds / 60); + final String unpaddedSecondPart = String.valueOf(seconds % 60); + + return Component.translatable( + "%s:%s", + Component.text(minutePart), + Component.text(unpaddedSecondPart.length() < 2 ? "0" + unpaddedSecondPart : unpaddedSecondPart) + ); + } + + public void stopPlaying () { + currentSong = null; + removeBossbar(); + } + + @Override + public void disconnected (DisconnectedEvent event) { + futurePlayTask.cancel(false); + + if (currentSong != null) currentSong.pause(); // nice. + } + + public void handlePlaying () { + currentSong.advanceTime(); + while (currentSong.reachedNextNote()) { + final Note note = currentSong.getNextNote(); + + final double floatingPitch = Math.pow(2, (note.pitch - 12) / 12.0); + + bot.core().run("minecraft:execute as " + SELECTOR + " at @s run playsound " + note.instrument.sound + " record @s ~ ~ ~ " + note.volume + " " + floatingPitch); + } + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/SelfCarePlugin.java b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/SelfCarePlugin.java index 464fe37..259e0d7 100644 --- a/src/main/java/me/chayapak1/chomensbot_mabe/plugins/SelfCarePlugin.java +++ b/src/main/java/me/chayapak1/chomensbot_mabe/plugins/SelfCarePlugin.java @@ -87,6 +87,12 @@ public class SelfCarePlugin extends SessionAdapter { this.entityId = packet.getEntityId(); this.gamemode = packet.getGameMode(); + cspy = false; + vanish = false; + socialspy = false; + muted = false; + prefix = false; + final Runnable task = () -> { final Session session = bot.session(); final PacketProtocol protocol = session.getPacketProtocol(); diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/Instrument.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/Instrument.java new file mode 100644 index 0000000..94c833e --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/Instrument.java @@ -0,0 +1,48 @@ +package me.chayapak1.chomensbot_mabe.song; + +public class Instrument { + public static final Instrument HARP = new Instrument(0, "harp", 54); + public static final Instrument BASEDRUM = new Instrument(1, "basedrum", 0); + public static final Instrument SNARE = new Instrument(2, "snare", 0); + public static final Instrument HAT = new Instrument(3, "hat", 0); + public static final Instrument BASS = new Instrument(4, "bass", 30); + public static final Instrument FLUTE = new Instrument(5, "flute", 66); + public static final Instrument BELL = new Instrument(6, "bell", 78); + public static final Instrument GUITAR = new Instrument(7, "guitar", 42); + public static final Instrument CHIME = new Instrument(8, "chime", 78); + public static final Instrument XYLOPHONE = new Instrument(9, "xylophone", 78); + public static final Instrument IRON_XYLOPHONE = new Instrument(10, "iron_xylophone", 54); + public static final Instrument COW_BELL = new Instrument(11, "cow_bell", 66); + public static final Instrument DIDGERIDOO = new Instrument(12, "didgeridoo", 30); + public static final Instrument BIT = new Instrument(13, "bit", 54); + public static final Instrument BANJO = new Instrument(14, "banjo", 54); + public static final Instrument PLING = new Instrument(15, "pling", 54); + + public final int id; + public final String name; + public final int offset; + public final String sound; + + private Instrument (int id, String name, int offset, String sound) { + this.id = id; + this.name = name; + this.offset = offset; + this.sound = name; + } + + private Instrument (int id, String name, int offset) { + this.id = id; + this.name = name; + this.offset = offset; + this.sound = "minecraft:block.note_block." + name; + } + + public static Instrument of (String sound) { + return new Instrument(-1, null, 0, sound); + } + + private static Instrument[] values = {HARP, BASEDRUM, SNARE, HAT, BASS, FLUTE, BELL, GUITAR, CHIME, XYLOPHONE, IRON_XYLOPHONE, COW_BELL, DIDGERIDOO, BIT, BANJO, PLING}; + public static Instrument fromId (int id) { + return values[id]; + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/MidiConverter.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/MidiConverter.java new file mode 100644 index 0000000..5661c3e --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/MidiConverter.java @@ -0,0 +1,374 @@ +package me.chayapak1.chomensbot_mabe.song; + +import me.chayapak1.chomensbot_mabe.Bot; +import me.chayapak1.chomensbot_mabe.util.DownloadUtilities; +import java.io.*; +import java.net.*; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; + +import javax.sound.midi.*; + +public class MidiConverter { + public static final int SET_INSTRUMENT = 0xC0; + public static final int SET_TEMPO = 0x51; + public static final int NOTE_ON = 0x90; + public static final int NOTE_OFF = 0x80; + +// public static Song getSongFromUrl(URL url) throws IOException, InvalidMidiDataException, URISyntaxException, NoSuchAlgorithmException, KeyManagementException { +// Sequence sequence = MidiSystem.getSequence(DownloadUtilities.DownloadToInputStream(url, 5*1024*1024)); +// return getSong(sequence, Paths.get(url.toURI().getPath()).getFileName().toString()); +// } +// +// public static Song getSongFromFile(File file) throws InvalidMidiDataException, IOException { +// Sequence sequence = MidiSystem.getSequence(file); +// return getSong(sequence, file.getName()); +// } + + public static Song getSongFromBytes(byte[] bytes, String name, Bot bot) throws InvalidMidiDataException, IOException { + Sequence sequence = MidiSystem.getSequence(new ByteArrayInputStream(bytes)); + return getSong(sequence, name, bot); + } + + public static Song getSong(Sequence sequence, String name, Bot bot) { + Song song = new Song(name, bot); + + long tpq = sequence.getResolution(); + + ArrayList tempoEvents = new ArrayList<>(); + for (Track track : sequence.getTracks()) { + for (int i = 0; i < track.size(); i++) { + MidiEvent event = track.get(i); + MidiMessage message = event.getMessage(); + if (message instanceof MetaMessage) { + MetaMessage mm = (MetaMessage) message; + if (mm.getType() == SET_TEMPO) { + tempoEvents.add(event); + } + } + } + } + + Collections.sort(tempoEvents, (a, b) -> Long.compare(a.getTick(), b.getTick())); + + for (Track track : sequence.getTracks()) { + + long microTime = 0; + int[] ids = new int[16]; + int mpq = 500000; + int tempoEventIdx = 0; + long prevTick = 0; + + for (int i = 0; i < track.size(); i++) { + MidiEvent event = track.get(i); + MidiMessage message = event.getMessage(); + + while (tempoEventIdx < tempoEvents.size() && event.getTick() > tempoEvents.get(tempoEventIdx).getTick()) { + long deltaTick = tempoEvents.get(tempoEventIdx).getTick() - prevTick; + prevTick = tempoEvents.get(tempoEventIdx).getTick(); + microTime += (mpq/tpq) * deltaTick; + + MetaMessage mm = (MetaMessage) tempoEvents.get(tempoEventIdx).getMessage(); + byte[] data = mm.getData(); + int new_mpq = (data[2]&0xFF) | ((data[1]&0xFF)<<8) | ((data[0]&0xFF)<<16); + if (new_mpq != 0) mpq = new_mpq; + tempoEventIdx++; + } + + if (message instanceof ShortMessage) { + ShortMessage sm = (ShortMessage) message; + if (sm.getCommand() == SET_INSTRUMENT) { + ids[sm.getChannel()] = sm.getData1(); + } + else if (sm.getCommand() == NOTE_ON) { + if (sm.getData2() == 0) continue; + int pitch = sm.getData1(); + int velocity = sm.getData2(); + long deltaTick = event.getTick() - prevTick; + prevTick = event.getTick(); + microTime += (mpq/tpq) * deltaTick; + + Note note; + if (sm.getChannel() == 9) { + note = getMidiPercussionNote(pitch, velocity, microTime); + } + else { + note = getMidiInstrumentNote(ids[sm.getChannel()], pitch, velocity, microTime); + } + if (note != null) { + song.add(note); + } + + long time = microTime / 1000L; + if (time > song.length) { + song.length = time; + } + } + else if (sm.getCommand() == NOTE_OFF) { + long deltaTick = event.getTick() - prevTick; + prevTick = event.getTick(); + microTime += (mpq/tpq) * deltaTick; + long time = microTime / 1000L; + if (time > song.length) { + song.length = time; + } + } + } + } + } + + song.sort(); + + return song; + } + + public static Note getMidiInstrumentNote(int midiInstrument, int midiPitch, int velocity, long microTime) { + Instrument instrument = null; + Instrument[] instrumentList = instrumentMap.get(midiInstrument); + if (instrumentList != null) { + for (Instrument candidateInstrument : instrumentList) { + if (midiPitch >= candidateInstrument.offset && midiPitch <= candidateInstrument.offset+24) { + instrument = candidateInstrument; + break; + } + } + } + + if (instrument == null) { + return null; + } + + int pitch = midiPitch-instrument.offset; + float volume = (float) velocity / 127.0f; + long time = microTime / 1000L; + + return new Note(instrument, pitch, volume, time); + } + + private static Note getMidiPercussionNote (int midiPitch, int velocity, long microTime) { + if (percussionMap.containsKey(midiPitch)) { + int noteId = percussionMap.get(midiPitch); + int pitch = noteId % 25; + float volume = (float) velocity / 127.0f; + Instrument instrument = Instrument.fromId(noteId / 25); + long time = microTime / 1000L; + + return new Note(instrument, pitch, volume, time); + } + return null; + } + + public static HashMap instrumentMap = new HashMap<>(); + static { + // Piano (HARP BASS BELL) + instrumentMap.put(0, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Acoustic Grand Piano + instrumentMap.put(1, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Bright Acoustic Piano + instrumentMap.put(2, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Electric Grand Piano + instrumentMap.put(3, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Honky-tonk Piano + instrumentMap.put(4, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Electric Piano 1 + instrumentMap.put(5, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Electric Piano 2 + instrumentMap.put(6, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Harpsichord + instrumentMap.put(7, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Clavinet + + // Chromatic Percussion (IRON_XYLOPHONE XYLOPHONE BASS) + instrumentMap.put(8, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Celesta + instrumentMap.put(9, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Glockenspiel + instrumentMap.put(10, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Music Box + instrumentMap.put(11, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Vibraphone + instrumentMap.put(12, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Marimba + instrumentMap.put(13, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Xylophone + instrumentMap.put(14, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Tubular Bells + instrumentMap.put(15, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Dulcimer + + // Organ (BIT DIDGERIDOO BELL) + instrumentMap.put(16, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Drawbar Organ + instrumentMap.put(17, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Percussive Organ + instrumentMap.put(18, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Rock Organ + instrumentMap.put(19, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Church Organ + instrumentMap.put(20, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Reed Organ + instrumentMap.put(21, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Accordian + instrumentMap.put(22, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Harmonica + instrumentMap.put(23, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Tango Accordian + + // Guitar (BIT DIDGERIDOO BELL) + instrumentMap.put(24, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Acoustic Guitar (nylon) + instrumentMap.put(25, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Acoustic Guitar (steel) + instrumentMap.put(26, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Electric Guitar (jazz) + instrumentMap.put(27, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Electric Guitar (clean) + instrumentMap.put(28, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Electric Guitar (muted) + instrumentMap.put(29, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Overdriven Guitar + instrumentMap.put(30, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Distortion Guitar + instrumentMap.put(31, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Guitar Harmonics + + // Bass + instrumentMap.put(32, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Acoustic Bass + instrumentMap.put(33, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Electric Bass (finger) + instrumentMap.put(34, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Electric Bass (pick) + instrumentMap.put(35, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Fretless Bass + instrumentMap.put(36, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Slap Bass 1 + instrumentMap.put(37, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Slap Bass 2 + instrumentMap.put(38, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Synth Bass 1 + instrumentMap.put(39, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Synth Bass 2 + + // Strings + instrumentMap.put(40, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Violin + instrumentMap.put(41, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Viola + instrumentMap.put(42, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Cello + instrumentMap.put(43, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Contrabass + instrumentMap.put(44, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Tremolo Strings + instrumentMap.put(45, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Pizzicato Strings + instrumentMap.put(46, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.CHIME}); // Orchestral Harp + instrumentMap.put(47, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Timpani + + // Ensenble + instrumentMap.put(48, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // String Ensemble 1 + instrumentMap.put(49, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // String Ensemble 2 + instrumentMap.put(50, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Synth Strings 1 + instrumentMap.put(51, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Synth Strings 2 + instrumentMap.put(52, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Choir Aahs + instrumentMap.put(53, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Voice Oohs + instrumentMap.put(54, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Synth Choir + instrumentMap.put(55, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Orchestra Hit + + // Brass + instrumentMap.put(56, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(57, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(58, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(59, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(60, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(61, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(62, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(63, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + + // Reed + instrumentMap.put(64, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(65, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(66, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(67, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(68, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(69, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(70, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(71, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + + // Pipe + instrumentMap.put(72, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(73, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(74, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(75, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(76, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(77, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(78, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + instrumentMap.put(79, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL}); + + // Synth Lead + instrumentMap.put(80, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(81, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(82, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(83, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(84, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(85, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(86, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(87, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + + // Synth Pad + instrumentMap.put(88, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(89, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(90, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(91, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(92, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(93, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(94, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(95, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + + // Synth Effects +// instrumentMap.put(96, new Instrument[]{}); +// instrumentMap.put(97, new Instrument[]{}); + instrumentMap.put(98, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(99, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(100, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(101, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(102, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(103, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); + + // Ethnic + instrumentMap.put(104, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(105, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(106, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(107, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(108, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL}); + instrumentMap.put(109, new Instrument[]{Instrument.HARP, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(110, new Instrument[]{Instrument.HARP, Instrument.DIDGERIDOO, Instrument.BELL}); + instrumentMap.put(111, new Instrument[]{Instrument.HARP, Instrument.DIDGERIDOO, Instrument.BELL}); + + // Percussive + instrumentMap.put(112, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(113, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(114, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(115, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(116, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(117, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(118, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + instrumentMap.put(119, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); + } + + public static HashMap percussionMap = new HashMap<>(); + static { + percussionMap.put(35, 10 + 25*Instrument.BASEDRUM.id); + percussionMap.put(36, 6 + 25*Instrument.BASEDRUM.id); + percussionMap.put(37, 6 + 25*Instrument.HAT.id); + percussionMap.put(38, 8 + 25*Instrument.SNARE.id); + percussionMap.put(39, 6 + 25*Instrument.HAT.id); + percussionMap.put(40, 4 + 25*Instrument.SNARE.id); + percussionMap.put(41, 6 + 25*Instrument.BASEDRUM.id); + percussionMap.put(42, 22 + 25*Instrument.SNARE.id); + percussionMap.put(43, 13 + 25*Instrument.BASEDRUM.id); + percussionMap.put(44, 22 + 25*Instrument.SNARE.id); + percussionMap.put(45, 15 + 25*Instrument.BASEDRUM.id); + percussionMap.put(46, 18 + 25*Instrument.SNARE.id); + percussionMap.put(47, 20 + 25*Instrument.BASEDRUM.id); + percussionMap.put(48, 23 + 25*Instrument.BASEDRUM.id); + percussionMap.put(49, 17 + 25*Instrument.SNARE.id); + percussionMap.put(50, 23 + 25*Instrument.BASEDRUM.id); + percussionMap.put(51, 24 + 25*Instrument.SNARE.id); + percussionMap.put(52, 8 + 25*Instrument.SNARE.id); + percussionMap.put(53, 13 + 25*Instrument.SNARE.id); + percussionMap.put(54, 18 + 25*Instrument.HAT.id); + percussionMap.put(55, 18 + 25*Instrument.SNARE.id); + percussionMap.put(56, 1 + 25*Instrument.HAT.id); + percussionMap.put(57, 13 + 25*Instrument.SNARE.id); + percussionMap.put(58, 2 + 25*Instrument.HAT.id); + percussionMap.put(59, 13 + 25*Instrument.SNARE.id); + percussionMap.put(60, 9 + 25*Instrument.HAT.id); + percussionMap.put(61, 2 + 25*Instrument.HAT.id); + percussionMap.put(62, 8 + 25*Instrument.HAT.id); + percussionMap.put(63, 22 + 25*Instrument.BASEDRUM.id); + percussionMap.put(64, 15 + 25*Instrument.BASEDRUM.id); + percussionMap.put(65, 13 + 25*Instrument.SNARE.id); + percussionMap.put(66, 8 + 25*Instrument.SNARE.id); + percussionMap.put(67, 8 + 25*Instrument.HAT.id); + percussionMap.put(68, 3 + 25*Instrument.HAT.id); + percussionMap.put(69, 20 + 25*Instrument.HAT.id); + percussionMap.put(70, 23 + 25*Instrument.HAT.id); + percussionMap.put(71, 24 + 25*Instrument.HAT.id); + percussionMap.put(72, 24 + 25*Instrument.HAT.id); + percussionMap.put(73, 17 + 25*Instrument.HAT.id); + percussionMap.put(74, 11 + 25*Instrument.HAT.id); + percussionMap.put(75, 18 + 25*Instrument.HAT.id); + percussionMap.put(76, 9 + 25*Instrument.HAT.id); + percussionMap.put(77, 5 + 25*Instrument.HAT.id); + percussionMap.put(78, 22 + 25*Instrument.HAT.id); + percussionMap.put(79, 19 + 25*Instrument.SNARE.id); + percussionMap.put(80, 17 + 25*Instrument.HAT.id); + percussionMap.put(81, 22 + 25*Instrument.HAT.id); + percussionMap.put(82, 22 + 25*Instrument.SNARE.id); + percussionMap.put(83, 24 + 25*Instrument.CHIME.id); + percussionMap.put(84, 24 + 25*Instrument.CHIME.id); + percussionMap.put(85, 21 + 25*Instrument.HAT.id); + percussionMap.put(86, 14 + 25*Instrument.BASEDRUM.id); + percussionMap.put(87, 7 + 25*Instrument.BASEDRUM.id); + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/NBSConverter.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/NBSConverter.java new file mode 100644 index 0000000..aaf34c2 --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/NBSConverter.java @@ -0,0 +1,203 @@ +package me.chayapak1.chomensbot_mabe.song; + +import me.chayapak1.chomensbot_mabe.Bot; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +public class NBSConverter { + public static Instrument[] instrumentIndex = new Instrument[] { + Instrument.HARP, + Instrument.BASS, + Instrument.BASEDRUM, + Instrument.SNARE, + Instrument.HAT, + Instrument.GUITAR, + Instrument.FLUTE, + Instrument.BELL, + Instrument.CHIME, + Instrument.XYLOPHONE, + Instrument.IRON_XYLOPHONE, + Instrument.COW_BELL, + Instrument.DIDGERIDOO, + Instrument.BIT, + Instrument.BANJO, + Instrument.PLING, + }; + + private static class NBSNote { + public int tick; + public short layer; + public byte instrument; + public byte key; + public byte velocity = 100; + public byte panning = 100; + public short pitch = 0; + } + + private static class NBSLayer { + public String name; + public byte lock = 0; + public byte volume; + public byte stereo = 100; + } + + private static class NBSCustomInstrument { + public String name; + public String file; + public byte pitch = 0; + public boolean key = false; + } + + public static Song getSongFromBytes(byte[] bytes, String fileName, Bot bot) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + short songLength; + byte format = 0; + byte vanillaInstrumentCount = 0; + songLength = buffer.getShort(); // If it's not 0, then it uses the old format + if (songLength == 0) { + format = buffer.get(); + } + + if (format >= 1) { + vanillaInstrumentCount = buffer.get(); + } + if (format >= 3) { + songLength = buffer.getShort(); + } + + short layerCount = buffer.getShort(); + String songName = getString(buffer, bytes.length); + String songAuthor = getString(buffer, bytes.length); + String songOriginalAuthor = getString(buffer, bytes.length); + String songDescription = getString(buffer, bytes.length); + short tempo = buffer.getShort(); + byte autoSaving = buffer.get(); + byte autoSavingDuration = buffer.get(); + byte timeSignature = buffer.get(); + int minutesSpent = buffer.getInt(); + int leftClicks = buffer.getInt(); + int rightClicks = buffer.getInt(); + int blocksAdded = buffer.getInt(); + int blocksRemoved = buffer.getInt(); + String origFileName = getString(buffer, bytes.length); + + byte loop = 0; + byte maxLoopCount = 0; + short loopStartTick = 0; + if (format >= 4) { + loop = buffer.get(); + maxLoopCount = buffer.get(); + loopStartTick = buffer.getShort(); + } + + ArrayList nbsNotes = new ArrayList<>(); + short tick = -1; + while (true) { + int tickJumps = buffer.getShort(); + if (tickJumps == 0) break; + tick += tickJumps; + + short layer = -1; + while (true) { + int layerJumps = buffer.getShort(); + if (layerJumps == 0) break; + layer += layerJumps; + NBSNote note = new NBSNote(); + note.tick = tick; + note.layer = layer; + note.instrument = buffer.get(); + note.key = buffer.get(); + if (format >= 4) { + note.velocity = buffer.get(); + note.panning = buffer.get(); + note.pitch = buffer.getShort(); + } + nbsNotes.add(note); + } + } + + ArrayList nbsLayers = new ArrayList<>(); + if (buffer.hasRemaining()) { + for (int i=0; i= 4) { + layer.lock = buffer.get(); + } + layer.volume = buffer.get(); + if (format >= 2) { + layer.stereo = buffer.get(); + } + nbsLayers.add(layer); + } + } + + ArrayList customInstruments = new ArrayList<>(); + if (buffer.hasRemaining()) { + byte customInstrumentCount = buffer.get(); + for (int i = 0; i < customInstrumentCount; i++) { + NBSCustomInstrument customInstrument = new NBSCustomInstrument(); + customInstrument.name = getString(buffer, bytes.length); + customInstrument.file = getString(buffer, bytes.length); + customInstrument.pitch = buffer.get(); + customInstrument.key = buffer.get() == 0 ? false : true; + customInstruments.add(customInstrument); + } + } + + Song song = new Song(songName.trim().length() > 0 ? songName : fileName, bot); + if (loop > 0) { + song.looping = 1; + song.loopPosition = getMilliTime(loopStartTick, tempo); +// song.loopCount = maxLoopCount; + } + for (NBSNote note : nbsNotes) { + Instrument instrument; + int key = note.key; + if (note.instrument < instrumentIndex.length) { + instrument = instrumentIndex[note.instrument]; + } else { + int index = note.instrument - instrumentIndex.length; + if (index >= customInstruments.size()) continue; + NBSCustomInstrument customInstrument = customInstruments.get(index); + instrument = Instrument.of(customInstrument.name); + // key += customInstrument.pitch; + } + + if (key < 33 || key > 57) { + continue; + } + + byte layerVolume = 100; + if (nbsLayers.size() > note.layer) { + layerVolume = nbsLayers.get(note.layer).volume; + } + + int pitch = key-33; + song.add(new Note(instrument, pitch, (float) note.velocity * (float) layerVolume / 10000f, getMilliTime(note.tick, tempo))); + } + + song.length = song.get(song.size()-1).time + 50; + + return song; + } + + private static String getString (ByteBuffer buffer, int maxSize) throws IOException { + int length = buffer.getInt(); + if (length > maxSize) { + throw new IOException("String is too large"); + } + byte arr[] = new byte[length]; + buffer.get(arr, 0, length); + return new String(arr); + } + + private static int getMilliTime(int tick, int tempo) { + return 1000 * tick * 100 / tempo; + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/Note.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/Note.java new file mode 100644 index 0000000..3d3486f --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/Note.java @@ -0,0 +1,28 @@ +package me.chayapak1.chomensbot_mabe.song; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class Note implements Comparable { + public Instrument instrument; + public int pitch; + public float volume; + public long time; + + @Override + public int compareTo(Note other) { + if (time < other.time) { + return -1; + } + else if (time > other.time) { + return 1; + } + else { + return 0; + } + } + + public int noteId () { + return pitch + instrument.id * 25; + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/Song.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/Song.java new file mode 100644 index 0000000..d94644b --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/Song.java @@ -0,0 +1,165 @@ +package me.chayapak1.chomensbot_mabe.song; + +import me.chayapak1.chomensbot_mabe.Bot; +import net.kyori.adventure.text.Component; +import java.util.ArrayList; +import java.util.Collections; + +public class Song { + public ArrayList notes = new ArrayList<>(); + public Component name; + public int position = 0; // Current note index + public int looping = 0; // 0 for no looping, 1 for current loop, 2 for all loops + public boolean paused = true; + public long startTime = 0; // Start time in millis since unix epoch + public long length = 0; // Milliseconds in the song + public long time = 0; // Time since start of song + public long loopPosition = 0; // Milliseconds into the song to start looping + public int loopCount = 0; // Number of times to loop + public int currentLoop = 0; // Number of loops so far + public int queueIndex = 0; + + private Bot bot; + + public Song (Component name, Bot bot) { + this.bot = bot; + this.name = name; + } + + public Song (String name, Bot bot) { + this(Component.text(name), bot); + this.bot = bot; + } + + public Note get (int i) { + return notes.get(i); + } + + public void add (Note e) { + notes.add(e); + } + + public void sort () { + Collections.sort(notes); + } + + /** + * Starts playing song (does nothing if already playing) + */ + public void play () { + if (paused) { + paused = false; + startTime = System.currentTimeMillis() - time; + } + } + + /** + * Pauses song (does nothing if already paused) + */ + public void pause () { + if (!paused) { + paused = true; + // Recalculates time so that the song will continue playing after the exact point it was paused + advanceTime(); + } + } + + public void setTime (long t) { + time = t; +// System.out.println("time is " + time); + startTime = System.currentTimeMillis() - time; +// System.out.println("start time is " + startTime); + position = 0; + while (position < notes.size() && notes.get(position).time < t) { + position++; + } +// System.out.println("position is " + position); + } + + public void advanceTime () { + time = System.currentTimeMillis() - startTime; + } + + public boolean reachedNextNote () { + if (position < notes.size()) { + return notes.get(position).time <= time; + } else { + if (time > length && shouldLoop()) { + loop(); + if (position < notes.size()) { + return notes.get(position).time <= time; + } else { + return false; + } + } else { + return false; + } + } + } + + public Note getNextNote () { + if (position >= notes.size()) { + if (shouldLoop()) { + loop(); + } else { + return null; + } + } + return notes.get(position++); + } + + public boolean finished () { + return time > length && !shouldLoop(); + } + + private void loop () { +// position = 0; +// startTime += length - loopPosition; +// time -= length - loopPosition; +// while (position < notes.size() && notes.get(position).time < loopPosition) { +// position++; +// } +// currentLoop++; +// if (looping == 1) { + position = 0; + startTime += length - loopPosition; + time -= length - loopPosition; + while (position < notes.size() && notes.get(position).time < loopPosition) { + position++; + } +// System.out.println("looping is 1 we did all shits here and position is " + position); +// } else if (looping == 2) { +// position = 0; +// setTime(0); +// System.out.println("looping is 2 position " + position); +// queueIndex = (queueIndex + 1) % bot.music().songQueue().size(); +// System.out.println("queue INDEX IS " + queueIndex); +// System.out.println("BOT SONG QUEUEUE SIZE IS " + bot.music().songQueue().size()); +// } +// System.out.println(currentLoop); + currentLoop++; + } + + private boolean shouldLoop () { +// if (looping) { +// if (loopCount == 0) { +// return true; +// } else { +// return currentLoop < loopCount; +// } +// } else { +// return false; +// } + if (looping == 1) { + if (loopCount == 0) { + return true; + } else { + return currentLoop < loopCount; + } + } else return looping == 2; + } + + public int size () { + return notes.size(); + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderException.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderException.java new file mode 100644 index 0000000..b866b17 --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderException.java @@ -0,0 +1,24 @@ +package me.chayapak1.chomensbot_mabe.song; + +import net.kyori.adventure.text.Component; +import lombok.Getter; +import me.chayapak1.chomensbot_mabe.util.ComponentUtilities; + +public class SongLoaderException extends Exception { + @Getter private final Component message; + + public SongLoaderException (Component message) { + super(); + this.message = message; + } + + public SongLoaderException (Component message, Throwable cause) { + super(null, cause); + this.message = message; + } + + @Override + public String getMessage () { + return ComponentUtilities.stringify(message); + } +} diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderThread.java b/src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderThread.java new file mode 100644 index 0000000..202905c --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/song/SongLoaderThread.java @@ -0,0 +1,77 @@ +package me.chayapak1.chomensbot_mabe.song; + +import me.chayapak1.chomensbot_mabe.Bot; +import me.chayapak1.chomensbot_mabe.plugins.MusicPlayerPlugin; +import me.chayapak1.chomensbot_mabe.util.DownloadUtilities; +import net.kyori.adventure.text.Component; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class SongLoaderThread extends Thread { + private String location; + private File songPath; + private URL songUrl; + public SongLoaderException exception; + public Song song; + + private Bot bot; + + private boolean isUrl = false; + + public SongLoaderThread (URL location, Bot bot) throws SongLoaderException { + this.bot = bot; + isUrl = true; + System.out.println("its url"); + songUrl = location; + } + + public SongLoaderThread (Path location, Bot bot) throws SongLoaderException { + this.bot = bot; + isUrl = false; + System.out.println("its path"); + songPath = location.toFile(); + } + + public void run () { + byte[] bytes; + String name; + try { + if (isUrl) { + bytes = DownloadUtilities.DownloadToByteArray(songUrl, 10*1024*1024); + name = Paths.get(songUrl.toURI().getPath()).getFileName().toString(); + } else { + bytes = Files.readAllBytes(songPath.toPath()); + name = songPath.getName(); + } + } catch (Exception e) { + System.out.println("WHAT THE SUS EXCEPTION YUP ! !! %@%$"); + e.printStackTrace(); + exception = new SongLoaderException(Component.text(e.getMessage()), e); + return; + } + + try { + song = MidiConverter.getSongFromBytes(bytes, name, bot); + } catch (Exception e) { + } + + if (song == null) { + try { + song = NBSConverter.getSongFromBytes(bytes, name, bot); + } catch (Exception e) { + } + } + + if (song == null) { + exception = new SongLoaderException(Component.translatable("Invalid song format")); + } + } + + private File getSongFile (String name) { + return new File(MusicPlayerPlugin.SONG_DIR, name); + } +} \ No newline at end of file diff --git a/src/main/java/me/chayapak1/chomensbot_mabe/util/DownloadUtilities.java b/src/main/java/me/chayapak1/chomensbot_mabe/util/DownloadUtilities.java new file mode 100644 index 0000000..d1b0f53 --- /dev/null +++ b/src/main/java/me/chayapak1/chomensbot_mabe/util/DownloadUtilities.java @@ -0,0 +1,61 @@ +package me.chayapak1.chomensbot_mabe.util; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.*; +import java.net.URL; +import java.net.URLConnection; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +public class DownloadUtilities { + private static class DefaultTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) {} + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + public static byte[] DownloadToByteArray(URL url, int maxSize) throws IOException, KeyManagementException, NoSuchAlgorithmException { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom()); + SSLContext.setDefault(ctx); + URLConnection conn = url.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(10000); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"); + + try (BufferedInputStream downloadStream = new BufferedInputStream(conn.getInputStream())) { + ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int n; + int tot = 0; + while ((n = downloadStream.read(buf)) > 0) { + byteArrayStream.write(buf, 0, n); + tot += n; + if (tot > maxSize) { + throw new IOException("File is too large"); + } + if (Thread.interrupted()) { + return null; + } + } + return byteArrayStream.toByteArray(); + } + // Closing a ByteArrayInputStream has no effect, so I do not close it. + } + + public static InputStream DownloadToInputStream(URL url, int maxSize) throws KeyManagementException, NoSuchAlgorithmException, IOException { + return new ByteArrayInputStream(DownloadToByteArray(url, maxSize)); + } +}