From 0e195c2299bceae377d712d09a71f0045b877526 Mon Sep 17 00:00:00 2001 From: Chipmunk <65827213+ChipmunkMC@users.noreply.github.com> Date: Thu, 16 Feb 2023 19:20:03 -0500 Subject: [PATCH] Initial music implementation --- .../chipmunk/chipmunkbot/ChipmunkBot.java | 1 + .../chipmunkbot/commands/MusicCommand.java | 93 +++++ .../chipmunkbot/plugins/ChatPlugin.java | 9 +- .../chipmunkbot/plugins/CommandCore.java | 26 +- .../chipmunkbot/plugins/CommandManager.java | 3 +- .../chipmunkbot/plugins/SongPlayer.java | 151 +++++++ .../chipmunk/chipmunkbot/song/Instrument.java | 35 ++ .../chipmunkbot/song/MidiConverter.java | 369 ++++++++++++++++++ .../chipmunkbot/song/NBSConverter.java | 177 +++++++++ .../land/chipmunk/chipmunkbot/song/Note.java | 27 ++ .../land/chipmunk/chipmunkbot/song/Song.java | 133 +++++++ .../chipmunkbot/song/SongLoaderException.java | 24 ++ .../chipmunkbot/song/SongLoaderThread.java | 81 ++++ .../chipmunkbot/util/DownloadUtilities.java | 67 ++++ 14 files changed, 1189 insertions(+), 7 deletions(-) create mode 100644 src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/Instrument.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/MidiConverter.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/NBSConverter.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/Note.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/Song.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderException.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java create mode 100644 src/main/java/land/chipmunk/chipmunkbot/util/DownloadUtilities.java diff --git a/src/main/java/land/chipmunk/chipmunkbot/ChipmunkBot.java b/src/main/java/land/chipmunk/chipmunkbot/ChipmunkBot.java index 7d19cd1..d554a80 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/ChipmunkBot.java +++ b/src/main/java/land/chipmunk/chipmunkbot/ChipmunkBot.java @@ -22,6 +22,7 @@ public class ChipmunkBot extends Client { @Getter public final PositionManager position = new PositionManager(this); @Getter public final CommandCore core = new CommandCore(this); @Getter public final SelfCarePlugin selfCare = new SelfCarePlugin(this); + @Getter public final SongPlayer songPlayer = new SongPlayer(this); public ChipmunkBot (ClientOptions options) { super(options); } } diff --git a/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java b/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java new file mode 100644 index 0000000..dcd4ab9 --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java @@ -0,0 +1,93 @@ +package land.chipmunk.chipmunkbot.commands; + +import land.chipmunk.chipmunkbot.song.Song; +import land.chipmunk.chipmunkbot.plugins.SongPlayer; +import land.chipmunk.chipmunkbot.command.*; +import static land.chipmunk.chipmunkbot.plugins.CommandManager.literal; +import static land.chipmunk.chipmunkbot.plugins.CommandManager.argument; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.JoinConfiguration; +import java.util.List; +import java.util.ArrayList; + +public class MusicCommand extends Command { + private static SimpleCommandExceptionType NO_SONG_IS_CURRENTLY_PLAYING = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("No song is currently playing"))); + + public MusicCommand () { + super(); + + this.node( + literal("music") + .then( + literal("play") + .then( + argument("location", greedyString()) + .executes(this::play) + ) + ) + + .then(literal("stop").executes(this::stop)) + .then(literal("pause").executes(this::pause)) + .then(literal("list").executes(this::list)) + ); + } + + public int play (CommandContext context) { + final String location = getString(context, "location"); + context.getSource().client().songPlayer().loadSong(location); + + return 1; + } + + public int stop (CommandContext context) throws CommandSyntaxException { + final CommandSource source = context.getSource(); + final SongPlayer songPlayer = source.client().songPlayer(); + + if (songPlayer.currentSong() == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create(); + + songPlayer.stopPlaying(); + source.sendOutput(Component.translatable("Stopped playing the current song", NamedTextColor.GREEN)); + + return 1; + } + + public int pause (CommandContext context) throws CommandSyntaxException { + final CommandSource source = context.getSource(); + final SongPlayer songPlayer = source.client().songPlayer(); + final Song currentSong = songPlayer.currentSong(); + + if (currentSong == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create(); + + if (!currentSong.paused) { + currentSong.pause(); + source.sendOutput(Component.translatable("Paused the current song")); + } else { + currentSong.play(); + source.sendOutput(Component.translatable("Unpaused the current song")); + } + + return 1; + } + + public int list (CommandContext context) { + final CommandSource source = context.getSource(); + final SongPlayer songPlayer = source.client().songPlayer(); + + final List list = new ArrayList<>(); + int i = 0; + for (String filename : songPlayer.SONG_DIR.list()) { + list.add(Component.text(filename, (i++ & 1) == 0 ? NamedTextColor.DARK_GREEN : NamedTextColor.GREEN)); + } + + final Component component = Component.translatable("Songs - %s", Component.join(JoinConfiguration.separator(Component.space()), list)).color(NamedTextColor.GREEN); + source.sendOutput(component, false); + + return 1; + } +} diff --git a/src/main/java/land/chipmunk/chipmunkbot/plugins/ChatPlugin.java b/src/main/java/land/chipmunk/chipmunkbot/plugins/ChatPlugin.java index e020741..b6590f7 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/plugins/ChatPlugin.java +++ b/src/main/java/land/chipmunk/chipmunkbot/plugins/ChatPlugin.java @@ -14,6 +14,7 @@ import com.github.steveice10.packetlib.event.session.SessionAdapter; import com.github.steveice10.packetlib.event.session.SessionListener; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import lombok.Getter; import java.util.BitSet; import java.util.ArrayList; import java.util.List; @@ -24,7 +25,7 @@ import land.chipmunk.chipmunkbot.systemChat.*; public class ChatPlugin extends SessionAdapter { private final ChipmunkBot client; - private List listeners = new ArrayList<>(); + @Getter private List listeners = new ArrayList<>(); private List systemChatParsers; @@ -77,12 +78,10 @@ public class ChatPlugin extends SessionAdapter { public void tellraw (Component message) { tellraw(message, "@a"); } public void tellraw (Component message, UUID uuid) { tellraw(message, UUIDUtilities.selector(uuid)); } - public void addListener (Listener listener) { - listeners.add(listener); - } - public static class Listener { public void systemMessageReceived (Component component, boolean overlay) {} public void playerMessageReceived (PlayerMessage message) {} } + + public void addListener (Listener listener) { listeners.add(listener); } } diff --git a/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandCore.java b/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandCore.java index 8c08c71..b986d00 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandCore.java +++ b/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandCore.java @@ -8,6 +8,7 @@ import com.github.steveice10.packetlib.packet.Packet; import com.github.steveice10.packetlib.Session; import com.github.steveice10.packetlib.event.session.SessionListener; import com.github.steveice10.packetlib.event.session.SessionAdapter; +import com.github.steveice10.packetlib.event.session.DisconnectedEvent; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerPositionPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket; @@ -25,17 +26,22 @@ import com.github.steveice10.opennbt.tag.builtin.ByteTag; import com.google.gson.JsonObject; import lombok.Getter; import lombok.Setter; +import java.util.List; +import java.util.ArrayList; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.CompletableFuture; public class CommandCore extends SessionAdapter { private ChipmunkBot client; - private Vector3i origin; + @Getter @Setter private boolean ready = false; + @Getter @Setter private Vector3i origin; // TODO: Make it configurable @Getter private final BlockArea relativeArea = new BlockArea(Vector3i.from(0, 0, 0), Vector3i.from(15, 0, 15)); @Getter @Setter private Vector3i currentBlockRelative; + @Getter private List listeners = new ArrayList<>(); + public CommandCore (ChipmunkBot client) { this.client = client; client.addListener((SessionListener) this); @@ -47,6 +53,11 @@ public class CommandCore extends SessionAdapter { } public void packetReceived (Session session, ClientboundPlayerPositionPacket packet) { + if (!ready) { + ready = true; + for (Listener listener : listeners) listener.ready(); + } + origin = Vector3i.from( ((int) packet.getX() / 16) * 16, 0, // TODO: Use the actual bottom of the world instead of hardcoding to 0 @@ -157,4 +168,17 @@ public class CommandCore extends SessionAdapter { return future; } + + @Override + public void disconnected (DisconnectedEvent event) { + origin = null; + currentBlockRelative = null; + ready = false; + } + + public static class Listener { + public void ready () {} + } + + public void addListener (Listener listener) { listeners.add(listener); } } diff --git a/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandManager.java b/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandManager.java index 186d590..ba4d579 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandManager.java +++ b/src/main/java/land/chipmunk/chipmunkbot/plugins/CommandManager.java @@ -30,7 +30,8 @@ public class CommandManager { new EchoCommand(), new InfoCommand(), new ReconnectCommand(), - new NetMsgCommand() + new NetMsgCommand(), + new MusicCommand() }; static { diff --git a/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java b/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java new file mode 100644 index 0000000..c728b8a --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java @@ -0,0 +1,151 @@ +package land.chipmunk.chipmunkbot.plugins; + +import land.chipmunk.chipmunkbot.ChipmunkBot; +import land.chipmunk.chipmunkbot.song.*; +// import com.github.steveice10.packetlib.packet.Packet; +// import com.github.steveice10.packetlib.Session; +import com.github.steveice10.packetlib.event.session.SessionListener; +import com.github.steveice10.packetlib.event.session.SessionAdapter; +import com.github.steveice10.packetlib.event.session.DisconnectedEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import lombok.Getter; +import lombok.Setter; +import java.io.File; +import java.util.Timer; +import java.util.TimerTask; + +public class SongPlayer extends SessionAdapter { + private final ChipmunkBot client; + + public static File SONG_DIR = new File("songs"); + static { + if (!SONG_DIR.exists()) { + SONG_DIR.mkdir(); + } + } + + @Getter @Setter private Song currentSong; + @Getter @Setter private Timer playTimer; + @Getter @Setter private SongLoaderThread loaderThread; + private int ticksUntilPausedActionbar = 20; + + public SongPlayer (ChipmunkBot client) { + this.client = client; + client.addListener((SessionListener) this); + client.core().addListener(new CommandCore.Listener() { + public void ready () { coreReady(); } // TODO: Handle listeners in a better way than this + }); + } + + public void loadSong (String location) { + if (loaderThread != null) { + client.chat().tellraw(Component.translatable("Already loading a song, cannot load another", NamedTextColor.RED)); + return; + } + + try { + final SongLoaderThread _loaderThread = new SongLoaderThread(location); + client.chat().tellraw(Component.translatable("Loading %s", Component.text(location, NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)); + _loaderThread.start(); + loaderThread = _loaderThread; + } catch (SongLoaderException e) { + client.chat().tellraw(Component.translatable("Failed to load song: %s", e.message()).color(NamedTextColor.RED)); + loaderThread = null; + } + } + + public void coreReady () { + playTimer = new Timer(); + + final TimerTask playTask = new TimerTask() { + @Override + public void run () { + if (loaderThread != null && !loaderThread.isAlive()) { + if (loaderThread.exception != null) { + client.chat().tellraw(Component.translatable("Failed to load song: %s", loaderThread.exception.message()).color(NamedTextColor.RED)); + } else { + currentSong = loaderThread.song; + client.chat().tellraw(Component.translatable("Now playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)); + currentSong.play(); + } + loaderThread = null; + } + + if (currentSong == null) return; + + if (currentSong.paused && ticksUntilPausedActionbar-- < 0) return; + else ticksUntilPausedActionbar = 20; + + client.core().run("title @a actionbar " + GsonComponentSerializer.gson().serialize(generateActionbar())); + + if (currentSong.paused) return; + + handlePlaying(); + + if (currentSong.finished()) { + client.chat().tellraw(Component.translatable("Finished playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)); + currentSong = null; + } + } + }; + + playTimer.schedule(playTask, 50, 50); + + if (currentSong != null) currentSong.play(); + } + + public Component generateActionbar () { + Component component = Component.empty() + .append(Component.translatable("Now playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)) + .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.translatable("%s / %s", formatTime(currentSong.time).color(NamedTextColor.GREEN), formatTime(currentSong.length).color(NamedTextColor.GREEN)).color(NamedTextColor.GRAY)) + .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.translatable("%s / %s", Component.text(currentSong.position, NamedTextColor.GREEN), Component.text(currentSong.size(), NamedTextColor.GREEN)).color(NamedTextColor.GRAY)); + + if (currentSong.paused) { + return component + .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY)) + .append(Component.translatable("Paused", 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; + } + + @Override + public void disconnected (DisconnectedEvent event) { + playTimer.cancel(); + playTimer.purge(); + + if (currentSong != null) currentSong.pause(); + } + + public void handlePlaying () { + currentSong.advanceTime(); + while (currentSong.reachedNextNote()) { + final Note note = currentSong.getNextNote(); + + final double floatingPitch = Math.pow(2, (note.pitch - 12) / 12.0); + + client.core().run("execute as @a at @s run playsound minecraft:block.note_block." + note.instrument.name + " record @s ~ ~ ~ 1 " + floatingPitch); + } + } +} diff --git a/src/main/java/land/chipmunk/chipmunkbot/song/Instrument.java b/src/main/java/land/chipmunk/chipmunkbot/song/Instrument.java new file mode 100644 index 0000000..9071d96 --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/Instrument.java @@ -0,0 +1,35 @@ +package land.chipmunk.chipmunkbot.song; + +public enum Instrument {//{"harp", "basedrum", "snare", "hat", "bass", "flute", "bell", "guitar", "chime", "xylophone", "iron_xylophone", "cow_bell", "didgeridoo", "bit", "banjo", "pling"}; + HARP(0, "harp", 54), + BASEDRUM(1, "basedrum", 0), + SNARE(2, "snare", 0), + HAT(3, "hat", 0), + BASS(4, "bass", 30), + FLUTE(5, "flute", 66), + BELL(6, "bell", 78), + GUITAR(7, "guitar", 42), + CHIME(8, "chime", 78), + XYLOPHONE(9, "xylophone", 78), + IRON_XYLOPHONE(10, "iron_xylophone", 54), + COW_BELL(11, "cow_bell", 66), + DIDGERIDOO(12, "didgeridoo", 30), + BIT(13, "bit", 54), + BANJO(14, "banjo", 54), + PLING(15, "pling", 54); + + public final int id; + public final String name; + public final int offset; + + Instrument (int id, String name, int offset) { + this.id = id; + this.name = name; + this.offset = offset; + } + + private static Instrument[] values = values(); + public static Instrument getInstrumentFromId (int instrumentId) { + return values[instrumentId]; + } +} diff --git a/src/main/java/land/chipmunk/chipmunkbot/song/MidiConverter.java b/src/main/java/land/chipmunk/chipmunkbot/song/MidiConverter.java new file mode 100644 index 0000000..705a444 --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/MidiConverter.java @@ -0,0 +1,369 @@ +package land.chipmunk.chipmunkbot.song; +import land.chipmunk.chipmunkbot.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) throws InvalidMidiDataException, IOException { + Sequence sequence = MidiSystem.getSequence(new ByteArrayInputStream(bytes)); + return getSong(sequence, name); + } + + public static Song getSong(Sequence sequence, String name) { + Song song = new Song(name); + + 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(); + long deltaTick = event.getTick() - prevTick; + prevTick = event.getTick(); + microTime += (mpq/tpq) * deltaTick; + + Note note; + if (sm.getChannel() == 9) { + note = getMidiPercussionNote(pitch, microTime); + } + else { + note = getMidiInstrumentNote(ids[sm.getChannel()], pitch, 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, 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; + long time = microTime / 1000L; + + return new Note(instrument, pitch, time); + } + + private static Note getMidiPercussionNote (int midiPitch, long microTime) { + if (percussionMap.containsKey(midiPitch)) { + int noteId = percussionMap.get(midiPitch); + int pitch = noteId % 25; + Instrument instrument = Instrument.getInstrumentFromId(noteId / 25); + long time = microTime / 1000L; + + return new Note(instrument, pitch, 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/land/chipmunk/chipmunkbot/song/NBSConverter.java b/src/main/java/land/chipmunk/chipmunkbot/song/NBSConverter.java new file mode 100644 index 0000000..5528dd2 --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/NBSConverter.java @@ -0,0 +1,177 @@ +package land.chipmunk.chipmunkbot.song; + +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; + } + + public static Song getSongFromBytes(byte[] bytes, String fileName) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + short songLength = 0; + 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); + } + } + + Song song = new Song(songName.trim().length() > 0 ? songName : fileName); + if (loop > 0) { + song.looping = true; + song.loopPosition = getMilliTime(loopStartTick, tempo); + song.loopCount = maxLoopCount; + } + for (NBSNote note : nbsNotes) { + Instrument instrument; + if (note.instrument < instrumentIndex.length) { + instrument = instrumentIndex[note.instrument]; + } + else { + continue; + } + + if (note.key < 33 || note.key > 57) { + continue; + } + + byte layerVolume = 100; + if (nbsLayers.size() > note.layer) { + layerVolume = nbsLayers.get(note.layer).volume; + } + + int pitch = note.key-33; + song.add(new Note(instrument, pitch, 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/land/chipmunk/chipmunkbot/song/Note.java b/src/main/java/land/chipmunk/chipmunkbot/song/Note.java new file mode 100644 index 0000000..24e8156 --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/Note.java @@ -0,0 +1,27 @@ +package land.chipmunk.chipmunkbot.song; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class Note implements Comparable { + public Instrument instrument; + public int pitch; + 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/land/chipmunk/chipmunkbot/song/Song.java b/src/main/java/land/chipmunk/chipmunkbot/song/Song.java new file mode 100644 index 0000000..5f74d3f --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/Song.java @@ -0,0 +1,133 @@ +package land.chipmunk.chipmunkbot.song; + +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 boolean[] requiredNotes = new boolean[400]; + public boolean looping = false; + 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 Song (Component name) { + this.name = name; + } + + public Song (String name) { + this(Component.text(name)); + } + + public Note get (int i) { + return notes.get(i); + } + + public void add (Note e) { + notes.add(e); + requiredNotes[e.noteId()] = true; + } + + 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; + startTime = System.currentTimeMillis() - time; + position = 0; + while (position < notes.size() && notes.get(position).time < t) { + 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++; + } + + private boolean shouldLoop () { + if (looping) { + if (loopCount == 0) { + return true; + } else { + return currentLoop < loopCount; + } + } else { + return false; + } + } + + public int size () { + return notes.size(); + } +} diff --git a/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderException.java b/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderException.java new file mode 100644 index 0000000..e38588d --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderException.java @@ -0,0 +1,24 @@ +package land.chipmunk.chipmunkbot.song; + +import net.kyori.adventure.text.Component; +import lombok.Getter; +import land.chipmunk.chipmunkbot.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/land/chipmunk/chipmunkbot/song/SongLoaderThread.java b/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java new file mode 100644 index 0000000..90a14fb --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java @@ -0,0 +1,81 @@ +package land.chipmunk.chipmunkbot.song; + +import land.chipmunk.chipmunkbot.plugins.SongPlayer; +import land.chipmunk.chipmunkbot.util.DownloadUtilities; +import net.kyori.adventure.text.Component; +import java.io.File; +import java.io.IOException; +import java.net.URL; +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 boolean isUrl = false; + + public SongLoaderThread (String location) throws SongLoaderException { + this.location = location; + if (location.startsWith("http://") || location.startsWith("https://")) { + isUrl = true; + try { + songUrl = new URL(location); + } catch (IOException exception) { + throw new SongLoaderException(Component.text(exception.getMessage()), exception); + } + } else if (location.contains("/") || location.contains("\\")) { + throw new SongLoaderException(Component.translatable("Invalid characters in song name: %s", Component.text(location))); + } else if (getSongFile(location).exists()) { + songPath = getSongFile(location); + } else if (getSongFile(location+".mid").exists()) { + songPath = getSongFile(location+".mid"); + } else if (getSongFile(location+".midi").exists()) { + songPath = getSongFile(location+".midi"); + } else if (getSongFile(location+".nbs").exists()) { + songPath = getSongFile(location+".nbs"); + } else { + throw new SongLoaderException(Component.translatable("Could not find song: %s", Component.text(location))); + } + } + + 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 exception) { + exception = new SongLoaderException(Component.text(exception.getMessage()), exception); + return; + } + + try { + song = MidiConverter.getSongFromBytes(bytes, name); + } catch (Exception e) { + } + + if (song == null) { + try { + song = NBSConverter.getSongFromBytes(bytes, name); + } catch (Exception e) { + } + } + + if (song == null) { + exception = new SongLoaderException(Component.translatable("Invalid song format")); + } + } + + private File getSongFile (String name) { + return new File(SongPlayer.SONG_DIR, name); + } +} diff --git a/src/main/java/land/chipmunk/chipmunkbot/util/DownloadUtilities.java b/src/main/java/land/chipmunk/chipmunkbot/util/DownloadUtilities.java new file mode 100644 index 0000000..c02035e --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/util/DownloadUtilities.java @@ -0,0 +1,67 @@ +package land.chipmunk.chipmunkbot.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.CertificateException; +import java.security.cert.X509Certificate; + +public class DownloadUtilities { + + private static class DefaultTrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + + @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"); + BufferedInputStream downloadStream = new BufferedInputStream(conn.getInputStream()); + ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); + + try { + 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(); + } finally { + // Closing a ByteArrayInputStream has no effect, so I do not close it. + downloadStream.close(); + } + } + + public static InputStream DownloadToInputStream(URL url, int maxSize) throws KeyManagementException, NoSuchAlgorithmException, IOException { + return new ByteArrayInputStream(DownloadToByteArray(url, maxSize)); + } +}