Initial music implementation

This commit is contained in:
Chipmunk 2023-02-16 19:20:03 -05:00
parent ec0d1c8a93
commit 0e195c2299
14 changed files with 1189 additions and 7 deletions

View file

@ -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); }
}

View file

@ -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<CommandSource> context) {
final String location = getString(context, "location");
context.getSource().client().songPlayer().loadSong(location);
return 1;
}
public int stop (CommandContext<CommandSource> 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<CommandSource> 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<CommandSource> context) {
final CommandSource source = context.getSource();
final SongPlayer songPlayer = source.client().songPlayer();
final List<Component> 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;
}
}

View file

@ -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<Listener> listeners = new ArrayList<>();
@Getter private List<Listener> listeners = new ArrayList<>();
private List<SystemChatParser> 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); }
}

View file

@ -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<Listener> 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); }
}

View file

@ -30,7 +30,8 @@ public class CommandManager {
new EchoCommand(),
new InfoCommand(),
new ReconnectCommand(),
new NetMsgCommand()
new NetMsgCommand(),
new MusicCommand()
};
static {

View file

@ -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);
}
}
}

View file

@ -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];
}
}

View file

@ -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<MidiEvent> 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<Integer, Instrument[]> 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<Integer, Integer> 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);
}
}

View file

@ -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<NBSNote> 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<NBSLayer> nbsLayers = new ArrayList<>();
if (buffer.hasRemaining()) {
for (int i=0; i<layerCount; i++) {
NBSLayer layer = new NBSLayer();
layer.name = getString(buffer, bytes.length);
if (format >= 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;
}
}

View file

@ -0,0 +1,27 @@
package land.chipmunk.chipmunkbot.song;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class Note implements Comparable<Note> {
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;
}
}

View file

@ -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<Note> 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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}