diff --git a/README.md b/README.md index 04e44c3..c26f372 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ To use NoteBlockLib in your application, check out the [Usage](#usage) section. For a reference implementation of NoteBlockLib, check out [NoteBlockTool](https://github.com/RaphiMC/NoteBlockTool). ## Features -- Reads .nbs, .mcsp2, .mid, .txt and .notebot files -- Can convert all of the above to .nbs +- Supports reading .nbs, .mid, .txt, .mcsp, .mcsp2 and .notebot files +- Supports writing .nbs, .txt and .mcsp2 files +- Can convert all formats to .nbs - Offers an easy way to play note block songs in your application - Good MIDI importer - Supports most MIDI files @@ -14,7 +15,7 @@ For a reference implementation of NoteBlockLib, check out [NoteBlockTool](https: - Can handle Black MIDI files - Supports all NBS versions - Version 0 - 5 - - Supports undocumented features like Tempo Changers + - Supports Tempo Changers - Many tools for manipulating songs - Optimize songs for use in Minecraft (Transposing, Resampling) - Resampling songs with a different TPS @@ -34,29 +35,14 @@ If you just want the latest jar file you can download it from [GitHub Actions](h ## Usage ### Concepts and terminology The main class of NoteBlockLib is the ``NoteBlockLib`` class. It contains all the methods for reading, writing and creating songs. -The utils for manipulating songs are located in the ``util`` package. +Some utils for manipulating and getting metrics about songs are located in the ``util`` package. -#### Song -Song is a wrapper class around the Header, Data and the View of a song. -The Header and Data classes are the low level representation of a song. They are used by I/O operations. -The View class is a high level representation of a song and is generated from the Header and Data classes. +The ``Song`` class is the main data structure for parsed songs. It contains generalized and format specific data. +The generalized data only includes the most important data of songs like the format, the title, the description, the author, the notes and the tempo. +To access format specific data you have to cast the song instance to a specific format (``NbsSong`` for example). +Most of the time you will only need the generalized data and all the methods for manipulating, playing and converting songs will work with the generalized data. -#### Header -The header usually contains the metadata of a song. This includes the author, the original author, the description, the tempo, the delay and the length of the song. - -#### Data -The data contains all the notes of a song. This includes the tick at which the note should be played, the instrument and the key. - -#### SongView -The SongView is a high level and generalized representation of a song. It contains only the most important information of a song. -The view is used for most operations like playing a song or manipulating it. Due to the fact that the view is a high level representation of a song, it is not suitable for I/O operations directly. -To create a low level representation (Song) from the view again you can use the ``NoteBlockLib.createSong(view, format)`` method. -The returned song only has the bare minimum of data required to be written to a file. You can use the setter methods of the Header and Data class to add more data to the song. -The view is generated by default only once when the Song class is created. If you want to refresh the view you can use the ``Song.refreshView()`` method. - -#### Note -The Note class is a wrapper class around the instrument and key of a note. Each format has its own Note class which can have additional data like volume or panning. -One way of accessing that data is through the use of the ``NoteWithVolume`` and ``NoteWithPanning`` classes. +All data structures in NoteBlockLib are mutable and can be modified at any time. All data structures also have a ``copy`` method to create a deep copy of the data structure. ### Reading a song To read a song you can use the ``NoteBlockLib.readSong(, [format])`` method. @@ -67,13 +53,13 @@ The format is optional and can be used to specify the format of the input. If th To write a song you can use the ``NoteBlockLib.writeSong(, )`` method. ### Creating a song -The easiest way to create a song is to create a SongView and then use the ``NoteBlockLib.createSongFromView(, [format])`` method to create a Song from it. -Alternatively you can create a Song directly by using the ``new Song(null,
, )`` constructor. This requires you to create the Header and Data yourself. +The easiest way to create a song is to create a ``new GenericSong()``, fill in the data and then use the ``NoteBlockLib.convertSong(, )`` method to create a format specific song from it. +Alternatively you can create a format specific Song directly by using for example the ``new NbsSong()`` constructor. This requires you to fill in all the format specific data yourself. ### Playing a song To play a song you can use the ``SongPlayer`` class. The SongPlayer provides basic controls like play, pause, stop and seek. -To instantiate it you can use the ``new SongPlayer(, )`` constructor. -The callback contains basic methods like ``onFinished`` and ``playNote`` to handle the playback of the song. +To create a SongPlayer implementation, you have to create a class which extends the ``SongPlayer`` class. +The SongPlayer class requires you to implement the ``playNotes`` method, but also offers several optional methods like ``onFinished``. ### Manipulating a song There are multiple utils for manipulating a song. @@ -84,94 +70,91 @@ This is very useful if you want to export the song as a schematic and play it in #### MinecraftDefinitions The MinecraftDefinitions class contains definitions and formulas for Minecraft related manipulations. -This includes multiple methods for getting notes within the Minecraft octave range, converting between Minecraft and NBS id systems and more. +This for example includes multiple methods for getting notes within the Minecraft octave range, such as transposing them. #### SongUtil -This class has some general utils for manipulating songs like applying a modification to all notes of a song. +This class has some general utils for getting various metrics about a song. ## Examples **Reading a song, transposing its notes and writing it back** ```java -Song song = NoteBlockLib.readSong(new File("input.nbs")); +Song song = NoteBlockLib.readSong(new File("input.nbs")); // Clamp the note key -// SongUtil.applyToAllNotes(song.getView(), MinecraftDefinitions::clampNoteKey); +// song.getNotes().forEach(MinecraftDefinitions::clampNoteKey); // Transpose the note key -//SongUtil.applyToAllNotes(song.getView(), MinecraftDefinitions::transposeNoteKey); +// song.getNotes().forEach(MinecraftDefinitions::transposeNoteKey); // Shift the instrument of out of range notes to a higher/lower one. Sounds better than all above. -SongUtil.applyToAllNotes(song.getView(), MinecraftDefinitions::instrumentShiftNote); +song.getNotes().forEach(MinecraftDefinitions::instrumentShiftNote); // Clamp the remaining out of range notes -SongUtil.applyToAllNotes(song.getView(), MinecraftDefinitions::clampNoteKey); - -NoteBlockLib.writeSong(song, new File("output.nbs")); +song.getNotes().forEach(MinecraftDefinitions::clampNoteKey); + +// The operations above work with the generalized song model. If you want to write it back to a specific format, you need to convert it first. +Song convertedSong = NoteBlockLib.convertSong(song, SongFormat.NBS); + +NoteBlockLib.writeSong(convertedSong, new File("output.nbs")); ``` **Reading a MIDI, and writing it as NBS** ```java -Song midiSong = NoteBlockLib.readSong(new File("input.mid")); -Song nbsSong = NoteBlockLib.createSongFromView(midiSong.getView(), SongFormat.NBS); +Song midiSong = NoteBlockLib.readSong(new File("input.mid")); +Song nbsSong = NoteBlockLib.convertSong(midiSong, SongFormat.NBS); NoteBlockLib.writeSong(nbsSong, new File("output.nbs")); ``` -**Reading a song, changing its sample rate to 10 TPS and writing it back** +**Reading a song, changing its tempo to 10 TPS and writing it back** ```java -Song song = NoteBlockLib.readSong(new File("input.nbs")); -SongResampler.changeTickSpeed(song.getView(), 10F); -Song newSong = NoteBlockLib.createSongFromView(song.getView(), SongFormat.NBS); +Song song = NoteBlockLib.readSong(new File("input.nbs")); +SongResampler.changeTickSpeed(song, 10F); +// The operations above work with the generalized song model. If you want to write it back to a specific format, you need to convert it first. +Song newSong = NoteBlockLib.convertSong(song, SongFormat.NBS); NoteBlockLib.writeSong(newSong, new File("output.nbs")); ``` **Creating a new song and saving it as NBS** ```java -// tick -> list of notes -Map> notes = new TreeMap<>(); -// Add the notes to the song -notes.put(0, Lists.newArrayList(new NbsNote(Instrument.HARP, (byte) 46))); -notes.put(5, Lists.newArrayList(new NbsNote(Instrument.BASS, (byte) 60))); -notes.put(8, Lists.newArrayList(new NbsNote(Instrument.BIT, (byte) 84))); -SongView mySong = new SongView<>("My song" /*title*/, 10F /*ticks per second*/, notes); -Song nbsSong = NoteBlockLib.createSongFromView(mySong, SongFormat.NBS); -NoteBlockLib.writeSong(nbsSong, new File("C:\\Users\\User\\Desktop\\output.nbs")); +Song mySong = new GenericSong(); +mySong.setTitle("My song"); +mySong.getTempoEvents().set(0, 10F); // set the tempo to 10 ticks per second +mySong.getNotes().add(0, new Note().setInstrument(MinecraftInstrument.HARP).setNbsKey((byte) 46)); +mySong.getNotes().add(5, new Note().setInstrument(MinecraftInstrument.BASS).setNbsKey((byte) 60)); +mySong.getNotes().add(8, new Note().setInstrument(MinecraftInstrument.BIT).setNbsKey((byte) 84)); +Song nbsSong = NoteBlockLib.convertSong(mySong, SongFormat.NBS); +NoteBlockLib.writeSong(nbsSong, new File("output.nbs")); ``` **Playing a song** -Define a callback class +Create the custom SongPlayer implementation: ```java -// Default callback. This callback has a method which receives the already calculated pitch, volume and panning. -// Note: The FullNoteConsumer interface may change over time when new note data is added by one of the formats. -public class MyCallback implements SongPlayerCallback, FullNoteConsumer { - @Override - public void playNote(final Instrument instrument, final float pitch, final float volume, final float panning) { - // This method gets called in real time as the song is played. - System.out.println(instrument + " " + pitch + " " + volume + " " + panning); +public class MySongPlayer extends SongPlayer { + + public MySongPlayer(Song song) { + super(song); } - - // There are other methods like playCustomNote, onFinished which can be overridden. -} -// Raw callback. This callback receives the raw Note class. Data like pitch, volume or panning have to be calculated/accessed manually. -public class MyRawCallback implements SongPlayerCallback { @Override - public void playNote(Note note) { - // This method gets called in real time as the song is played. - // For an example to calculate the various note data see the FullNoteConsumer class. - System.out.println(note.getInstrument() + " " + note.getKey()); + protected void playNotes(List notes) { + for (Note note : notes) { + // This method gets called in real time as the song is played. + // Make sure to check the javadoc of the various methods from the Note class to see how you should use the returned values. + System.out.println(note.getInstrument() + " " + note.getPitch() + " " + note.getVolume() + " " + note.getPanning()); + } } - + // There are other methods like onFinished which can be overridden. } ``` -Start playing the song +Start playing the song; ```java -Song song = NoteBlockLib.readSong(new File("input.nbs")); +Song song = NoteBlockLib.readSong(new File("input.nbs")); // Optionally apply a modification to all notes here (For example to transpose the note keys) // Create a song player -SongPlayer player = new SongPlayer(song.getView(), new MyCallback()); +SongPlayer player = new MySongPlayer(song); // Start playing -player.play(); +player.start(); // Pause player.setPaused(true); @@ -179,9 +162,15 @@ player.setPaused(true); // Resume player.setPaused(false); -// Seek +// Seek to a specific tick player.setTick(50); +// Seek to a specific time +player.setMillisecondPosition(1000); + +// Get the current millisecond position +player.getMillisecondPosition(); + // Stop player.stop(); ``` diff --git a/gradle.properties b/gradle.properties index 4df962b..5923fb6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ org.gradle.configureondemand=true maven_group=net.raphimc maven_name=NoteBlockLib -maven_version=2.1.5-SNAPSHOT +maven_version=3.0.0-SNAPSHOT diff --git a/src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java b/src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java index c18dd6e..85b9b72 100644 --- a/src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java +++ b/src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java @@ -17,20 +17,22 @@ */ package net.raphimc.noteblocklib; -import com.google.common.io.ByteStreams; import net.raphimc.noteblocklib.format.SongFormat; -import net.raphimc.noteblocklib.format.future.FutureParser; -import net.raphimc.noteblocklib.format.mcsp.McSpParser; -import net.raphimc.noteblocklib.format.midi.MidiParser; -import net.raphimc.noteblocklib.format.nbs.NbsParser; -import net.raphimc.noteblocklib.format.nbs.NbsSong; -import net.raphimc.noteblocklib.format.nbs.model.NbsData; -import net.raphimc.noteblocklib.format.nbs.model.NbsHeader; -import net.raphimc.noteblocklib.format.txt.TxtParser; -import net.raphimc.noteblocklib.format.txt.TxtSong; +import net.raphimc.noteblocklib.format.futureclient.FutureClientIo; +import net.raphimc.noteblocklib.format.mcsp.McSpIo; +import net.raphimc.noteblocklib.format.mcsp2.McSp2Converter; +import net.raphimc.noteblocklib.format.mcsp2.McSp2Io; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song; +import net.raphimc.noteblocklib.format.midi.MidiIo; +import net.raphimc.noteblocklib.format.nbs.NbsConverter; +import net.raphimc.noteblocklib.format.nbs.NbsIo; +import net.raphimc.noteblocklib.format.nbs.model.NbsSong; +import net.raphimc.noteblocklib.format.txt.TxtConverter; +import net.raphimc.noteblocklib.format.txt.TxtIo; +import net.raphimc.noteblocklib.format.txt.model.TxtSong; import net.raphimc.noteblocklib.model.Song; -import net.raphimc.noteblocklib.model.SongView; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.io.OutputStream; @@ -41,86 +43,88 @@ public class NoteBlockLib { - public static Song readSong(final File file) throws Exception { + public static Song readSong(final File file) throws Exception { return readSong(file.toPath()); } - public static Song readSong(final Path path) throws Exception { + public static Song readSong(final Path path) throws Exception { return readSong(path, getFormat(path)); } - public static Song readSong(final Path path, final SongFormat format) throws Exception { - return readSong(Files.readAllBytes(path), format, path.getFileName().toString()); + public static Song readSong(final Path path, final SongFormat format) throws Exception { + return readSong(Files.newInputStream(path), format, path.getFileName().toString()); } - public static Song readSong(final InputStream is, final SongFormat format) throws Exception { - return readSong(ByteStreams.toByteArray(is), format, null); + public static Song readSong(final byte[] bytes, final SongFormat format) throws Exception { + return readSong(new ByteArrayInputStream(bytes), format); } - public static Song readSong(final byte[] bytes, final SongFormat format) throws Exception { - return readSong(bytes, format, null); + public static Song readSong(final InputStream is, final SongFormat format) throws Exception { + return readSong(is, format, null); } - public static Song readSong(final byte[] bytes, final SongFormat format, final String fileName) throws Exception { + public static Song readSong(final InputStream is, final SongFormat format, final String fileName) throws Exception { try { - if (format == null) throw new IllegalArgumentException("Unknown format"); - switch (format) { case NBS: - return NbsParser.read(bytes, fileName); + return NbsIo.readSong(is, fileName); case MCSP: - return McSpParser.read(bytes, fileName); + return McSpIo.readSong(is, fileName); + case MCSP2: + return McSp2Io.readSong(is, fileName); case TXT: - return TxtParser.read(bytes, fileName); - case FUTURE: - return FutureParser.read(bytes, fileName); + return TxtIo.readSong(is, fileName); + case FUTURE_CLIENT: + return FutureClientIo.readSong(is, fileName); case MIDI: - return MidiParser.read(bytes, fileName); + return MidiIo.readSong(is, fileName); default: throw new IllegalStateException("Unknown format"); } } catch (Throwable e) { throw new Exception("Failed to read song", e); + } finally { + is.close(); } } - public static void writeSong(final Song song, final File file) throws Exception { + public static void writeSong(final Song song, final File file) throws Exception { writeSong(song, file.toPath()); } - public static void writeSong(final Song song, final Path path) throws Exception { - Files.write(path, writeSong(song)); - } - - public static void writeSong(final Song song, final OutputStream os) throws Exception { - os.write(writeSong(song)); + public static void writeSong(final Song song, final Path path) throws Exception { + writeSong(song, Files.newOutputStream(path)); } - public static byte[] writeSong(final Song song) throws Exception { - byte[] bytes = null; + public static void writeSong(final Song song, final OutputStream os) throws Exception { try { if (song instanceof NbsSong) { - bytes = NbsParser.write((NbsSong) song); + NbsIo.writeSong((NbsSong) song, os); + } else if (song instanceof McSp2Song) { + McSp2Io.writeSong((McSp2Song) song, os); } else if (song instanceof TxtSong) { - bytes = TxtParser.write((TxtSong) song); + TxtIo.writeSong((TxtSong) song, os); + } else { + throw new Exception("Unsupported song format for writing: " + song.getClass().getSimpleName()); } } catch (Throwable e) { throw new Exception("Failed to write song", e); + } finally { + os.close(); } - - if (bytes == null) { - throw new Exception("Unsupported song type for writing: " + song.getClass().getSimpleName()); - } - - return bytes; } - public static Song createSongFromView(final SongView songView, final SongFormat format) { - if (format != SongFormat.NBS) { - throw new IllegalArgumentException("Only NBS is supported for creating songs from views"); + public static Song convertSong(final Song song, final SongFormat targetFormat) { + switch (targetFormat) { + case NBS: + return NbsConverter.createSong(song); + case MCSP2: + return McSp2Converter.createSong(song); + case TXT: + return TxtConverter.createSong(song); + default: + throw new IllegalStateException("Unsupported target format: " + targetFormat); } - - return new NbsSong(null, new NbsHeader(songView), new NbsData(songView)); } public static SongFormat getFormat(final Path path) { diff --git a/src/main/java/net/raphimc/noteblocklib/player/SongPlayerCallback.java b/src/main/java/net/raphimc/noteblocklib/data/Constants.java similarity index 70% rename from src/main/java/net/raphimc/noteblocklib/player/SongPlayerCallback.java rename to src/main/java/net/raphimc/noteblocklib/data/Constants.java index 6851250..51d1eb7 100644 --- a/src/main/java/net/raphimc/noteblocklib/player/SongPlayerCallback.java +++ b/src/main/java/net/raphimc/noteblocklib/data/Constants.java @@ -15,24 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.player; +package net.raphimc.noteblocklib.data; -@FunctionalInterface -public interface SongPlayerCallback extends NoteConsumer { +public class Constants { - default void onFinished() { - } + public static final int F_SHARP_4_MIDI_KEY = 66; - default boolean shouldTick() { - return true; - } - - default boolean shouldLoop() { - return false; - } - - default int getLoopDelay() { - return 0; - } + public static final int KEYS_PER_OCTAVE = 12; } diff --git a/src/main/java/net/raphimc/noteblocklib/data/MinecraftDefinitions.java b/src/main/java/net/raphimc/noteblocklib/data/MinecraftDefinitions.java new file mode 100644 index 0000000..5921203 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/data/MinecraftDefinitions.java @@ -0,0 +1,152 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.data; + +import net.raphimc.noteblocklib.model.Note; +import net.raphimc.noteblocklib.model.instrument.Instrument; + +import java.util.EnumMap; +import java.util.Map; + +import static net.raphimc.noteblocklib.data.MinecraftInstrument.*; + +public class MinecraftDefinitions { + + public static final int MC_LOWEST_MIDI_KEY = 54; + public static final int MC_HIGHEST_MIDI_KEY = 78; + public static final int MC_LOWEST_KEY = 0; + public static final int MC_HIGHEST_KEY = 24; + public static final int MC_KEYS = Constants.KEYS_PER_OCTAVE * 2; + + // Instrument -> [lower shifts, upper shifts] + private static final Map INSTRUMENT_SHIFTS = new EnumMap<>(MinecraftInstrument.class); + + static { + INSTRUMENT_SHIFTS.put(HARP, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS}, new MinecraftInstrument[]{BELL}}); + INSTRUMENT_SHIFTS.put(BASS, new MinecraftInstrument[][]{new MinecraftInstrument[0], new MinecraftInstrument[]{HARP, BELL}}); + INSTRUMENT_SHIFTS.put(BASS_DRUM, new MinecraftInstrument[][]{new MinecraftInstrument[0], new MinecraftInstrument[]{SNARE}}); + INSTRUMENT_SHIFTS.put(SNARE, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS_DRUM}, new MinecraftInstrument[]{HAT}}); + INSTRUMENT_SHIFTS.put(HAT, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS_DRUM}, new MinecraftInstrument[]{CHIME}}); + INSTRUMENT_SHIFTS.put(GUITAR, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS}, new MinecraftInstrument[]{COW_BELL, XYLOPHONE}}); + INSTRUMENT_SHIFTS.put(FLUTE, new MinecraftInstrument[][]{new MinecraftInstrument[]{DIDGERIDOO}, new MinecraftInstrument[]{BELL, CHIME}}); + INSTRUMENT_SHIFTS.put(BELL, new MinecraftInstrument[][]{new MinecraftInstrument[]{HARP}, new MinecraftInstrument[0]}); + INSTRUMENT_SHIFTS.put(CHIME, new MinecraftInstrument[][]{new MinecraftInstrument[]{BELL}, new MinecraftInstrument[0]}); + INSTRUMENT_SHIFTS.put(XYLOPHONE, new MinecraftInstrument[][]{new MinecraftInstrument[]{COW_BELL}, new MinecraftInstrument[]{CHIME}}); + INSTRUMENT_SHIFTS.put(IRON_XYLOPHONE, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS}, new MinecraftInstrument[]{BELL}}); + INSTRUMENT_SHIFTS.put(COW_BELL, new MinecraftInstrument[][]{new MinecraftInstrument[]{GUITAR}, new MinecraftInstrument[]{XYLOPHONE}}); + INSTRUMENT_SHIFTS.put(DIDGERIDOO, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS}, new MinecraftInstrument[]{FLUTE, BELL}}); + INSTRUMENT_SHIFTS.put(BIT, new MinecraftInstrument[][]{new MinecraftInstrument[]{DIDGERIDOO}, new MinecraftInstrument[]{BELL}}); + INSTRUMENT_SHIFTS.put(BANJO, new MinecraftInstrument[][]{new MinecraftInstrument[]{DIDGERIDOO}, new MinecraftInstrument[]{BELL}}); + INSTRUMENT_SHIFTS.put(PLING, new MinecraftInstrument[][]{new MinecraftInstrument[]{BASS}, new MinecraftInstrument[]{BELL}}); + } + + /** + * Clamps the key of the note to fall within minecraft octave range.
+ * Any key below 33 (NBS) will be set to 33 (NBS), and any key above 57 (NBS) will be set to 57 (NBS). + * + * @param note The note to clamp + */ + public static void clampNoteKey(final Note note) { + note.setMidiKey(Math.max(MC_LOWEST_MIDI_KEY, Math.min(MC_HIGHEST_MIDI_KEY, note.getMidiKey()))); + } + + /** + * Transposes the key of the note to fall within minecraft octave range.
+ * Any key below 33 (NBS) will be transposed up an octave, and any key above 57 (NBS) will be transposed down an octave. + * + * @param note The note to transpose + */ + public static void transposeNoteKey(final Note note) { + transposeNoteKey(note, Constants.KEYS_PER_OCTAVE); + } + + /** + * Transposes the key of the note to fall within minecraft octave range.
+ * Any key below 33 (NBS) will be transposed up by transposeAmount, and any key above 57 (NBS) will be transposed down by transposeAmount. + * + * @param note The note to transpose + * @param transposeAmount The amount of keys to transpose by + */ + public static void transposeNoteKey(final Note note, final int transposeAmount) { + float key = note.getMidiKey(); + while (key < MC_LOWEST_MIDI_KEY) { + key += transposeAmount; + } + while (key > MC_HIGHEST_MIDI_KEY) { + key -= transposeAmount; + } + note.setMidiKey(key); + } + + /** + * "Transposes" the key of the note by shifting the instrument to a higher or lower sounding one.
+ * This often sounds the best of the three methods as it keeps the musical key the same and only changes the instrument.
+ * The note might still be slightly outside of the minecraft octave range. Use one of the other methods to fix this. Clamp is recommended. + * + * @param note The note to transpose + */ + public static void instrumentShiftNote(final Note note) { + Instrument instrument = note.getInstrument(); + if (!(instrument instanceof MinecraftInstrument)) { // Custom instrument + return; + } + + final MinecraftInstrument[][] shifts = INSTRUMENT_SHIFTS.get(instrument); + if (shifts == null) { // No shifts defined for this instrument + return; + } + + float key = note.getMidiKey(); + int downShifts = 0; + while (key < MC_LOWEST_MIDI_KEY && downShifts < shifts[0].length) { + instrument = shifts[0][downShifts++]; + key += MinecraftDefinitions.MC_KEYS; + } + + int upShifts = 0; + while (key > MC_HIGHEST_MIDI_KEY && upShifts < shifts[1].length) { + instrument = shifts[1][upShifts++]; + key -= MinecraftDefinitions.MC_KEYS; + } + + note.setInstrument(instrument); + note.setMidiKey(key); + } + + /** + * Returns the octave delta to use as a suffix for the sound name and modifies the note key to be within the Minecraft octave range.
+ * The octave delta value has to be appended to the sound name to play the note at the correct pitch. (For example: "block.note_block.harp_" + octaveDelta)
+ * Link to the resource pack: Extended Notes + * + * @param note The note to modify + * @return The octave delta to use as a suffix for the sound name + */ + public static int applyExtendedNotesResourcePack(final Note note) { + int octavesDelta = 0; + while (note.getMidiKey() < MC_LOWEST_MIDI_KEY) { + note.setMidiKey(note.getMidiKey() + MC_KEYS); + octavesDelta--; + } + while (note.getMidiKey() > MC_HIGHEST_MIDI_KEY) { + note.setMidiKey(note.getMidiKey() - MC_KEYS); + octavesDelta++; + } + return octavesDelta; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/data/MinecraftInstrument.java b/src/main/java/net/raphimc/noteblocklib/data/MinecraftInstrument.java new file mode 100644 index 0000000..cf37129 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/data/MinecraftInstrument.java @@ -0,0 +1,95 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.data; + +import net.raphimc.noteblocklib.model.instrument.Instrument; + +public enum MinecraftInstrument implements Instrument { + + HARP(0, 0, "block.note_block.harp"), + BASS(1, 4, "block.note_block.bass"), + BASS_DRUM(2, 1, "block.note_block.basedrum"), + SNARE(3, 2, "block.note_block.snare"), + HAT(4, 3, "block.note_block.hat"), + GUITAR(5, 7, "block.note_block.guitar"), + FLUTE(6, 5, "block.note_block.flute"), + BELL(7, 6, "block.note_block.bell"), + CHIME(8, 8, "block.note_block.chime"), + XYLOPHONE(9, 9, "block.note_block.xylophone"), + IRON_XYLOPHONE(10, 10, "block.note_block.iron_xylophone"), + COW_BELL(11, 11, "block.note_block.cow_bell"), + DIDGERIDOO(12, 12, "block.note_block.didgeridoo"), + BIT(13, 13, "block.note_block.bit"), + BANJO(14, 14, "block.note_block.banjo"), + PLING(15, 15, "block.note_block.pling"); + + private final byte nbsId; + private final byte mcId; + private final String mcSoundName; + + MinecraftInstrument(final int nbsId, final int mcId, final String mcSoundName) { + this.nbsId = (byte) nbsId; + this.mcId = (byte) mcId; + this.mcSoundName = mcSoundName; + } + + public static MinecraftInstrument fromNbsId(final byte nbsId) { + for (final MinecraftInstrument instrument : MinecraftInstrument.values()) { + if (instrument.nbsId == nbsId) { + return instrument; + } + } + return null; + } + + public static MinecraftInstrument fromMcId(final byte mcId) { + for (final MinecraftInstrument instrument : MinecraftInstrument.values()) { + if (instrument.mcId == mcId) { + return instrument; + } + } + return null; + } + + public static MinecraftInstrument fromMcSoundName(final String mcSoundName) { + for (final MinecraftInstrument instrument : MinecraftInstrument.values()) { + if (instrument.mcSoundName.equals(mcSoundName)) { + return instrument; + } + } + return null; + } + + public byte nbsId() { + return this.nbsId; + } + + public byte mcId() { + return this.mcId; + } + + public String mcSoundName() { + return this.mcSoundName; + } + + @Override + public MinecraftInstrument copy() { + return this; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/SongFormat.java b/src/main/java/net/raphimc/noteblocklib/format/SongFormat.java index 14b9928..a4463e5 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/SongFormat.java +++ b/src/main/java/net/raphimc/noteblocklib/format/SongFormat.java @@ -34,7 +34,13 @@ public enum SongFormat { * * @see Minecraft Forum */ - MCSP("mcsp2", "mcsp"), + MCSP("mcsp"), + /** + * Minecraft Song Planner v2 (Ancestor of Minecraft Note Block Studio) + * + * @see Minecraft Forum + */ + MCSP2("mcsp2"), /** * BleachHack NoteBot * @@ -46,7 +52,7 @@ public enum SongFormat { * * @see Future Client */ - FUTURE("notebot"), + FUTURE_CLIENT("notebot"), /** * Standard MIDI * diff --git a/src/main/java/net/raphimc/noteblocklib/format/future/FutureParser.java b/src/main/java/net/raphimc/noteblocklib/format/future/FutureParser.java deleted file mode 100644 index 15d8ac1..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/future/FutureParser.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.future; - -import com.google.common.io.LittleEndianDataInputStream; -import net.raphimc.noteblocklib.format.future.model.FutureData; -import net.raphimc.noteblocklib.format.future.model.FutureHeader; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class FutureParser { - - public static FutureSong read(final byte[] bytes, final String fileName) throws IOException { - final LittleEndianDataInputStream dis = new LittleEndianDataInputStream(new ByteArrayInputStream(bytes)); - - final FutureHeader header = new FutureHeader(dis); - final FutureData data = new FutureData(header, dis); - - return new FutureSong(fileName, header, data); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/future/FutureSong.java b/src/main/java/net/raphimc/noteblocklib/format/future/FutureSong.java deleted file mode 100644 index c6c1302..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/future/FutureSong.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.future; - -import net.raphimc.noteblocklib.format.SongFormat; -import net.raphimc.noteblocklib.format.future.model.FutureData; -import net.raphimc.noteblocklib.format.future.model.FutureHeader; -import net.raphimc.noteblocklib.format.future.model.FutureNote; -import net.raphimc.noteblocklib.model.Song; -import net.raphimc.noteblocklib.model.SongView; - -public class FutureSong extends Song { - - public FutureSong(final String fileName, final FutureHeader header, final FutureData data) { - super(SongFormat.FUTURE, fileName, header, data); - } - - @Override - protected SongView createView() { - final String title = this.fileName == null ? "Future Song" : this.fileName; - - return new SongView<>(title, 20F, this.getData().getNotes()); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureData.java b/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureData.java deleted file mode 100644 index bfd3d7c..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureData.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.future.model; - -import com.google.common.io.LittleEndianDataInputStream; -import net.raphimc.noteblocklib.model.NotemapData; -import net.raphimc.noteblocklib.util.Instrument; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class FutureData extends NotemapData { - - public FutureData(final FutureHeader header, final LittleEndianDataInputStream dis) throws IOException { - int tick = 0; - while (dis.available() > 0) { - final byte b = dis.readByte(); - if (b == (header.useMagicValue() ? 5 : 64)) { - tick += dis.readUnsignedShort(); - } else { - this.notes.computeIfAbsent(tick, k -> new ArrayList<>()).add(new FutureNote(dis.readByte(), Instrument.fromMcId(b))); - } - } - } - - public FutureData(final Map> notes) { - super(notes); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureHeader.java b/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureHeader.java deleted file mode 100644 index c9da72e..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureHeader.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.future.model; - -import com.google.common.io.ByteStreams; -import net.raphimc.noteblocklib.model.Header; - -import java.io.IOException; -import java.io.InputStream; - -public class FutureHeader implements Header { - - private boolean useMagicValue = true; - - public FutureHeader(final InputStream is) throws IOException { - is.mark(is.available()); - final byte[] data = ByteStreams.toByteArray(is); - for (byte b : data) { - if (b == 64) { - this.useMagicValue = false; - break; - } - } - is.reset(); - } - - public FutureHeader(final boolean useMagicValue) { - this.useMagicValue = useMagicValue; - } - - public boolean useMagicValue() { - return this.useMagicValue; - } - - public void setUseMagicValue(final boolean useMagicValue) { - this.useMagicValue = useMagicValue; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureNote.java b/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureNote.java deleted file mode 100644 index a3d7c6a..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/future/model/FutureNote.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.future.model; - -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.util.Instrument; -import net.raphimc.noteblocklib.util.MinecraftDefinitions; - -public class FutureNote extends Note { - - public FutureNote(final byte key, final Instrument instrument) { - super(instrument, key); - } - - @Override - public byte getKey() { - return (byte) (super.getKey() + MinecraftDefinitions.MC_LOWEST_KEY); - } - - @Override - public void setKey(final byte key) { - super.setKey((byte) (key - MinecraftDefinitions.MC_LOWEST_KEY)); - } - - @Override - public FutureNote clone() { - return new FutureNote(this.key, this.instrument); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/futureclient/FutureClientIo.java b/src/main/java/net/raphimc/noteblocklib/format/futureclient/FutureClientIo.java new file mode 100644 index 0000000..3d4a257 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/futureclient/FutureClientIo.java @@ -0,0 +1,83 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.futureclient; + +import com.google.common.io.ByteStreams; +import com.google.common.io.LittleEndianDataInputStream; +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.futureclient.model.FutureClientNote; +import net.raphimc.noteblocklib.format.futureclient.model.FutureClientSong; +import net.raphimc.noteblocklib.model.Note; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FutureClientIo { + + private static final int BUFFER_SIZE = 1024 * 1024; + + public static FutureClientSong readSong(final InputStream is, final String fileName) throws IOException { + final LittleEndianDataInputStream dis = new LittleEndianDataInputStream(new BufferedInputStream(is, BUFFER_SIZE)); + final FutureClientSong song = new FutureClientSong(fileName); + + final Map> notes = song.getFutureClientNotes(); + + boolean use64 = false; + dis.mark(dis.available()); + final byte[] data = ByteStreams.toByteArray(dis); + for (byte b : data) { + if (b == 64) { + use64 = true; + break; + } + } + dis.reset(); + + int tick = 0; + while (dis.available() > 0) { + final byte instrument = dis.readByte(); + if (instrument == (use64 ? 64 : 5)) { + tick += dis.readUnsignedShort(); + } else { + final FutureClientNote note = new FutureClientNote(); + note.setInstrument(instrument); + note.setKey(dis.readByte()); + notes.computeIfAbsent(tick, k -> new ArrayList<>()).add(note); + } + } + + { // Fill generalized song structure with data + song.getTempoEvents().set(0, 20); + for (Map.Entry> entry : notes.entrySet()) { + for (FutureClientNote futureClientNote : entry.getValue()) { + final Note note = new Note(); + note.setInstrument(MinecraftInstrument.fromMcId(futureClientNote.getInstrument())); + note.setMcKey(futureClientNote.getKey()); + song.getNotes().add(entry.getKey(), note); + } + } + } + + return song; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/futureclient/model/FutureClientNote.java b/src/main/java/net/raphimc/noteblocklib/format/futureclient/model/FutureClientNote.java new file mode 100644 index 0000000..14a2ba0 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/futureclient/model/FutureClientNote.java @@ -0,0 +1,64 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.futureclient.model; + +import java.util.Objects; + +public class FutureClientNote { + + private byte instrument; + private byte key; + + public byte getInstrument() { + return this.instrument; + } + + public FutureClientNote setInstrument(final byte instrument) { + this.instrument = instrument; + return this; + } + + public byte getKey() { + return this.key; + } + + public FutureClientNote setKey(final byte key) { + this.key = key; + return this; + } + + public FutureClientNote copy() { + final FutureClientNote copyNote = new FutureClientNote(); + copyNote.setInstrument(this.getInstrument()); + copyNote.setKey(this.getKey()); + return copyNote; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + FutureClientNote futureClientNote = (FutureClientNote) o; + return instrument == futureClientNote.instrument && key == futureClientNote.key; + } + + @Override + public int hashCode() { + return Objects.hash(instrument, key); + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/futureclient/model/FutureClientSong.java b/src/main/java/net/raphimc/noteblocklib/format/futureclient/model/FutureClientSong.java new file mode 100644 index 0000000..838a260 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/futureclient/model/FutureClientSong.java @@ -0,0 +1,63 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.futureclient.model; + +import net.raphimc.noteblocklib.format.SongFormat; +import net.raphimc.noteblocklib.model.Song; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FutureClientSong extends Song { + + private final Map> notes = new HashMap<>(); + + public FutureClientSong() { + this(null); + } + + public FutureClientSong(final String fileName) { + super(SongFormat.TXT, fileName); + } + + /** + * @return A map of all notes, with the tick as the key. + */ + public Map> getFutureClientNotes() { + return this.notes; + } + + @Override + public FutureClientSong copy() { + final FutureClientSong copySong = new FutureClientSong(this.getFileName()); + copySong.copyGeneralData(this); + final Map> notes = this.getFutureClientNotes(); + final Map> copyNotes = copySong.getFutureClientNotes(); + for (Map.Entry> entry : notes.entrySet()) { + final List copyNoteList = new ArrayList<>(entry.getValue().size()); + for (FutureClientNote note : entry.getValue()) { + copyNoteList.add(note.copy()); + } + copyNotes.put(entry.getKey(), copyNoteList); + } + return copySong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpDefinitions.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpDefinitions.java new file mode 100644 index 0000000..117cf8a --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpDefinitions.java @@ -0,0 +1,24 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp; + +public class McSpDefinitions { + + public static final int NOTE_COUNT = 7; + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpIo.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpIo.java new file mode 100644 index 0000000..e9bf80f --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpIo.java @@ -0,0 +1,81 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp; + +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.mcsp.model.McSpNote; +import net.raphimc.noteblocklib.format.mcsp.model.McSpSong; +import net.raphimc.noteblocklib.model.Note; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Scanner; + +public class McSpIo { + + private static final int BUFFER_SIZE = 1024 * 1024; + + public static McSpSong readSong(final InputStream is, final String fileName) { + final Scanner scanner = new Scanner(new BufferedInputStream(is, BUFFER_SIZE), StandardCharsets.ISO_8859_1.name()).useDelimiter("\\|"); + final McSpSong song = new McSpSong(fileName); + + scanner.nextInt(); // version? Is ignored by Minecraft Song Planner v2.5 + + final Map notes = song.getMcSpNotes(); + + int tick = 0; + while (scanner.hasNext()) { + tick += scanner.nextInt(); + final char[] noteData = scanner.next().toCharArray(); + if (noteData.length != McSpDefinitions.NOTE_COUNT * 2) { + throw new IllegalArgumentException("Invalid note data: " + new String(noteData)); + } + final McSpNote[] noteArray = new McSpNote[McSpDefinitions.NOTE_COUNT]; + for (int i = 0; i < noteArray.length; i++) { + final int instrument = noteData[i * 2] - '0'; + final int key = noteData[i * 2 + 1] - 'A'; + if (instrument == 0) continue; + + final McSpNote note = new McSpNote(); + note.setInstrument((byte) (instrument - 1)); + note.setKey((byte) key); + noteArray[i] = note; + } + notes.put(tick, noteArray); + } + + { // Fill generalized song structure with data + song.getTempoEvents().set(0, 10); + for (Map.Entry entry : notes.entrySet()) { + for (McSpNote mcSpNote : entry.getValue()) { + if (mcSpNote == null) continue; + + final Note note = new Note(); + note.setInstrument(MinecraftInstrument.fromNbsId(mcSpNote.getInstrument())); + note.setMcKey(mcSpNote.getKey()); + song.getNotes().add(entry.getKey(), note); + } + } + } + + return song; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpParser.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpParser.java deleted file mode 100644 index 729b56c..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpParser.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.mcsp; - -import net.raphimc.noteblocklib.format.mcsp.model.McSpData; -import net.raphimc.noteblocklib.format.mcsp.model.McSpHeader; - -import java.io.ByteArrayInputStream; -import java.util.Scanner; - -public class McSpParser { - - public static McSpSong read(final byte[] bytes, final String fileName) { - final McSpHeader header = new McSpHeader(new Scanner(new ByteArrayInputStream(bytes))); - final McSpData data = new McSpData(header, new Scanner(new ByteArrayInputStream(bytes))); - - return new McSpSong(fileName, header, data); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpSong.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpSong.java deleted file mode 100644 index 0f799e9..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/mcsp/McSpSong.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.mcsp; - -import net.raphimc.noteblocklib.format.SongFormat; -import net.raphimc.noteblocklib.format.mcsp.model.McSpData; -import net.raphimc.noteblocklib.format.mcsp.model.McSpHeader; -import net.raphimc.noteblocklib.format.mcsp.model.McSpLayer; -import net.raphimc.noteblocklib.format.mcsp.model.McSpNote; -import net.raphimc.noteblocklib.model.Song; -import net.raphimc.noteblocklib.model.SongView; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public class McSpSong extends Song { - - public McSpSong(final String fileName, final McSpHeader header, final McSpData data) { - super(SongFormat.MCSP, fileName, header, data); - } - - @Override - protected SongView createView() { - final String title = this.getHeader().getTitle().isEmpty() ? this.fileName == null ? "MCSP Song" : this.fileName : this.getHeader().getTitle(); - - final Map> notes = new TreeMap<>(); - for (McSpLayer layer : this.getData().getLayers()) { - for (Map.Entry note : layer.getNotesAtTick().entrySet()) { - notes.computeIfAbsent(note.getKey(), k -> new ArrayList<>()).add(note.getValue()); - } - } - - return new SongView<>(title, this.getHeader().getSpeed(), notes); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpData.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpData.java deleted file mode 100644 index ec5d74d..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpData.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.mcsp.model; - -import net.raphimc.noteblocklib.model.Data; -import net.raphimc.noteblocklib.util.Instrument; - -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class McSpData implements Data { - - private List layers; - - private static final Pattern NOTE_DATA_PATTERN = Pattern.compile("(\\d+)?>(.)"); - - public McSpData(final McSpHeader header, final Scanner scanner) { - this.layers = new ArrayList<>(); - scanner.useDelimiter("[|\\n]"); - if (header.getVersion() == 2) { - for (int i = 0; i < 6; i++) { - scanner.next(); // skip header - } - - int tick = 0; - while (scanner.hasNext()) { - tick += scanner.nextInt(); - final Matcher noteData = NOTE_DATA_PATTERN.matcher(scanner.next()); - - int layer = 0; - while (noteData.find()) { - if (noteData.groupCount() == 2) { - layer += Integer.parseInt(noteData.group(1)); - while (this.layers.size() <= layer) { - this.layers.add(new McSpLayer()); - } - this.layers.get(layer).getNotesAtTick().put(tick, new McSpNote(noteData.group(2).charAt(0))); - } else if (noteData.groupCount() == 1) { - if (this.layers.isEmpty()) { - this.layers.add(new McSpLayer()); - } - this.layers.get(layer).getNotesAtTick().put(tick, new McSpNote(noteData.group(1).charAt(0))); - } else if (noteData.groupCount() != 0) { - throw new IllegalArgumentException("Invalid note data: " + noteData.group(0)); - } - } - } - } else if (header.getVersion() == 0) { - scanner.next(); // skip header - - int tick = 0; - while (scanner.hasNext()) { - tick += scanner.nextInt(); - final char[] noteData = scanner.next().toCharArray(); - if (noteData.length != 14) { - throw new IllegalArgumentException("Invalid note data: " + new String(noteData)); - } - for (int layer = 0; layer <= 6; layer++) { - final int instrument = noteData[layer * 2] - '0'; - final int key = noteData[layer * 2 + 1] - 'A'; - if (instrument == 0) continue; - - while (this.layers.size() <= layer) { - this.layers.add(new McSpLayer()); - } - - this.layers.get(layer).getNotesAtTick().put(tick, new McSpNote((byte) key, Instrument.fromNbsId((byte) (instrument - 1)))); - } - } - } else { - throw new IllegalArgumentException("Unsupported MCSP version: " + header.getVersion()); - } - } - - public McSpData(final List layers) { - this.layers = layers; - } - - /** - * @return The layers of this song - */ - public List getLayers() { - return this.layers; - } - - /** - * @param layers The layers of this song - */ - public void setLayers(final List layers) { - this.layers = layers; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpHeader.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpHeader.java deleted file mode 100644 index fa77280..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpHeader.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.mcsp.model; - -import net.raphimc.noteblocklib.model.Header; - -import java.util.NoSuchElementException; -import java.util.Scanner; - -public class McSpHeader implements Header { - - private int version; - private String title = ""; - private String author = ""; - private String originalAuthor = ""; - private int speed = 10; - private int autoSaveInterval; - private int leftClicks; - private int rightClicks; - private int noteBlocksAdded; - private int noteBlocksRemoved; - private int minutesSpent; - - public McSpHeader(final Scanner scanner) { - scanner.useDelimiter("\\|"); - this.version = scanner.nextInt(); - if (this.version != 0 && this.version != 2) { - throw new IllegalStateException("Unsupported MCSP version: " + this.version); - } - if (this.version == 2) { - this.autoSaveInterval = scanner.nextInt(); - this.title = scanner.next(); - this.author = scanner.next(); - this.originalAuthor = scanner.next(); - if (!scanner.next().isEmpty()) { - throw new IllegalStateException("Invalid MCSP header"); - } - try { // Optional metadata - scanner.nextLine(); - this.speed = scanner.nextInt(); - this.leftClicks = scanner.nextInt(); - this.rightClicks = scanner.nextInt(); - this.noteBlocksAdded = scanner.nextInt(); - this.noteBlocksRemoved = scanner.nextInt(); - this.minutesSpent = scanner.nextInt(); - } catch (NoSuchElementException ignored) { - } - } - } - - public McSpHeader(final int version) { - this.version = version; - } - - public McSpHeader(final int version, final int autoSaveInterval, final String title, final String author, final String originalAuthor, final int speed, final int leftClicks, final int rightClicks, final int noteBlocksAdded, final int noteBlocksRemoved, final int minutesSpent) { - this.version = version; - this.autoSaveInterval = autoSaveInterval; - this.title = title; - this.author = author; - this.originalAuthor = originalAuthor; - this.speed = speed; - this.leftClicks = leftClicks; - this.rightClicks = rightClicks; - this.noteBlocksAdded = noteBlocksAdded; - this.noteBlocksRemoved = noteBlocksRemoved; - this.minutesSpent = minutesSpent; - } - - public McSpHeader() { - } - - /** - * @return The version of the MCSP format. - */ - public int getVersion() { - return this.version; - } - - /** - * @param version The version of the MCSP format. - */ - public void setVersion(final int version) { - this.version = version; - } - - /** - * @return The name of the song. - */ - public String getTitle() { - return this.title; - } - - /** - * @param title The name of the song. - */ - public void setTitle(final String title) { - this.title = title; - } - - /** - * @return The author of the song. - */ - public String getAuthor() { - return this.author; - } - - /** - * @param author The author of the song. - */ - public void setAuthor(final String author) { - this.author = author; - } - - /** - * @return The original author of the song. - */ - public String getOriginalAuthor() { - return this.originalAuthor; - } - - /** - * @param originalAuthor The original author of the song. - */ - public void setOriginalAuthor(final String originalAuthor) { - this.originalAuthor = originalAuthor; - } - - /** - * @return The tempo of the song. Measured in ticks per second. - */ - public int getSpeed() { - return this.speed; - } - - /** - * @param speed The tempo of the song. Measured in ticks per second. - */ - public void setSpeed(final int speed) { - this.speed = speed; - } - - /** - * @return The amount of minutes between each auto-save (0 indicates that auto-save is disabled) (0-60). - */ - public int getAutoSaveInterval() { - return this.autoSaveInterval; - } - - /** - * @param autoSaveInterval The amount of minutes between each auto-save (0 indicates that auto-save is disabled) (0-60). - */ - public void setAutoSaveInterval(final int autoSaveInterval) { - this.autoSaveInterval = autoSaveInterval; - } - - /** - * @return Amount of times the user has left-clicked. - */ - public int getLeftClicks() { - return this.leftClicks; - } - - /** - * @param leftClicks Amount of times the user has left-clicked. - */ - public void setLeftClicks(final int leftClicks) { - this.leftClicks = leftClicks; - } - - /** - * @return Amount of times the user has right-clicked. - */ - public int getRightClicks() { - return this.rightClicks; - } - - /** - * @param rightClicks Amount of times the user has right-clicked. - */ - public void setRightClicks(final int rightClicks) { - this.rightClicks = rightClicks; - } - - /** - * @return Amount of times the user has added a note block. - */ - public int getNoteBlocksAdded() { - return this.noteBlocksAdded; - } - - /** - * @param noteBlocksAdded Amount of times the user has added a note block. - */ - public void setNoteBlocksAdded(final int noteBlocksAdded) { - this.noteBlocksAdded = noteBlocksAdded; - } - - /** - * @return Amount of times the user has removed a note block. - */ - public int getNoteBlocksRemoved() { - return this.noteBlocksRemoved; - } - - /** - * @param noteBlocksRemoved Amount of times the user has removed a note block. - */ - public void setNoteBlocksRemoved(final int noteBlocksRemoved) { - this.noteBlocksRemoved = noteBlocksRemoved; - } - - /** - * @return Amount of minutes spent on the project. - */ - public int getMinutesSpent() { - return this.minutesSpent; - } - - /** - * @param minutesSpent Amount of minutes spent on the project. - */ - public void setMinutesSpent(final int minutesSpent) { - this.minutesSpent = minutesSpent; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpNote.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpNote.java index af65a7a..3a22c29 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpNote.java +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpNote.java @@ -17,35 +17,48 @@ */ package net.raphimc.noteblocklib.format.mcsp.model; -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.util.Instrument; -import net.raphimc.noteblocklib.util.MinecraftDefinitions; +import java.util.Objects; -public class McSpNote extends Note { +public class McSpNote { - private static final String MAPPING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!§½#£¤$%&/{[(])=}?\\+´`^~¨*'.;,:-_<µ€ÌìíÍïÏîÎóÓòÒöÖåÅäÄñÑõÕúÚùÙüûÜÛéÉèÈêÊë"; + private byte instrument; + private byte key; - public McSpNote(final char noteData) { - this((byte) (MAPPING.indexOf(noteData) % 25), Instrument.fromNbsId((byte) (MAPPING.indexOf(noteData) / 25))); + public byte getInstrument() { + return this.instrument; } - public McSpNote(final byte key, final Instrument instrument) { - super(instrument, key); + public McSpNote setInstrument(final byte instrument) { + this.instrument = instrument; + return this; } - @Override public byte getKey() { - return (byte) (super.getKey() + MinecraftDefinitions.MC_LOWEST_KEY); + return this.key; + } + + public McSpNote setKey(final byte key) { + this.key = key; + return this; + } + + public McSpNote copy() { + final McSpNote copyNote = new McSpNote(); + copyNote.setInstrument(this.getInstrument()); + copyNote.setKey(this.getKey()); + return copyNote; } @Override - public void setKey(final byte key) { - super.setKey((byte) (key - MinecraftDefinitions.MC_LOWEST_KEY)); + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + McSpNote mcSpNote = (McSpNote) o; + return instrument == mcSpNote.instrument && key == mcSpNote.key; } @Override - public McSpNote clone() { - return new McSpNote(this.key, this.instrument); + public int hashCode() { + return Objects.hash(instrument, key); } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpSong.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpSong.java new file mode 100644 index 0000000..d4094cf --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpSong.java @@ -0,0 +1,64 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp.model; + +import net.raphimc.noteblocklib.format.SongFormat; +import net.raphimc.noteblocklib.model.Song; + +import java.util.HashMap; +import java.util.Map; + +public class McSpSong extends Song { + + private final Map notes = new HashMap<>(); + + public McSpSong() { + this(null); + } + + public McSpSong(final String fileName) { + super(SongFormat.MCSP, fileName); + } + + /** + * @return A map of all notes, with the tick as the key. The notes array must be 7 elements long. + */ + public Map getMcSpNotes() { + return this.notes; + } + + @Override + public McSpSong copy() { + final McSpSong copySong = new McSpSong(this.getFileName()); + copySong.copyGeneralData(this); + final Map notes = this.getMcSpNotes(); + final Map copyNotes = copySong.getMcSpNotes(); + for (Map.Entry entry : notes.entrySet()) { + final McSpNote[] copyNotesArray = new McSpNote[entry.getValue().length]; + for (int i = 0; i < copyNotesArray.length; i++) { + final McSpNote note = entry.getValue()[i]; + if (note != null) { + copyNotesArray[i] = note.copy(); + } + } + copyNotes.put(entry.getKey(), copyNotesArray); + } + return copySong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Converter.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Converter.java new file mode 100644 index 0000000..90ebebe --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Converter.java @@ -0,0 +1,88 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp2; + +import net.raphimc.noteblocklib.data.MinecraftDefinitions; +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Layer; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Note; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song; +import net.raphimc.noteblocklib.format.nbs.model.NbsSong; +import net.raphimc.noteblocklib.model.Note; +import net.raphimc.noteblocklib.model.Song; +import net.raphimc.noteblocklib.util.SongResampler; + +import java.util.Arrays; +import java.util.List; + +public class McSp2Converter { + + private static final List SUPPORTED_INSTRUMENTS = Arrays.asList(MinecraftInstrument.HARP, MinecraftInstrument.BASS, MinecraftInstrument.BASS_DRUM, MinecraftInstrument.SNARE, MinecraftInstrument.HAT); + + /** + * Creates a new MCSP2 song from the general data of the given song (Also copies some format specific fields if applicable). + * + * @param song The song + * @return The new MCSP2 song + */ + public static McSp2Song createSong(Song song) { + song = song.copy(); + SongResampler.changeTickSpeed(song, Math.max(McSp2Definitions.MIN_TEMPO, Math.min(McSp2Definitions.MAX_TEMPO, Math.round(song.getTempoEvents().get(0))))); + + final McSp2Song newSong = new McSp2Song(); + newSong.copyGeneralData(song); + newSong.setTempo((int) song.getTempoEvents().get(0)); + + for (int tick : song.getNotes().getTicks()) { + final List notes = song.getNotes().get(tick); + for (int i = 0; i < notes.size(); i++) { + final Note note = notes.get(i); + if (note.getInstrument() instanceof MinecraftInstrument && SUPPORTED_INSTRUMENTS.contains((MinecraftInstrument) note.getInstrument()) && note.getVolume() > 0) { + final McSp2Note mcSp2Note = new McSp2Note(); + mcSp2Note.setInstrument(((MinecraftInstrument) note.getInstrument()).nbsId()); + mcSp2Note.setKey((byte) Math.max(MinecraftDefinitions.MC_LOWEST_KEY, Math.min(MinecraftDefinitions.MC_HIGHEST_KEY, note.getMcKey()))); + + final McSp2Layer mcSp2Layer = newSong.getLayers().computeIfAbsent(i, k -> new McSp2Layer()); + mcSp2Layer.getNotes().put(tick, mcSp2Note); + } + } + } + + if (song instanceof McSp2Song) { + final McSp2Song mcSp2Song = (McSp2Song) song; + newSong.setAutoSaveInterval(mcSp2Song.getAutoSaveInterval()); + newSong.setAutoSaveInterval((byte) mcSp2Song.getAutoSaveInterval()); + newSong.setMinutesSpent(mcSp2Song.getMinutesSpent()); + newSong.setLeftClicks(mcSp2Song.getLeftClicks()); + newSong.setRightClicks(mcSp2Song.getRightClicks()); + newSong.setNoteBlocksAdded(mcSp2Song.getNoteBlocksAdded()); + newSong.setNoteBlocksRemoved(mcSp2Song.getNoteBlocksRemoved()); + } else if (song instanceof NbsSong) { + final NbsSong nbsSong = (NbsSong) song; + newSong.setAutoSaveInterval(nbsSong.isAutoSave() ? nbsSong.getAutoSaveInterval() : (byte) 0); + newSong.setMinutesSpent(nbsSong.getMinutesSpent()); + newSong.setLeftClicks(nbsSong.getLeftClicks()); + newSong.setRightClicks(nbsSong.getRightClicks()); + newSong.setNoteBlocksAdded(nbsSong.getNoteBlocksAdded()); + newSong.setNoteBlocksRemoved(nbsSong.getNoteBlocksRemoved()); + } + + return newSong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/player/NoteConsumer.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Definitions.java similarity index 61% rename from src/main/java/net/raphimc/noteblocklib/player/NoteConsumer.java rename to src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Definitions.java index b130a2b..d9e902f 100644 --- a/src/main/java/net/raphimc/noteblocklib/player/NoteConsumer.java +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Definitions.java @@ -15,21 +15,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.player; +package net.raphimc.noteblocklib.format.mcsp2; -import net.raphimc.noteblocklib.model.Note; +import java.util.regex.Pattern; -import java.util.List; +public class McSp2Definitions { -@FunctionalInterface -public interface NoteConsumer { + public static final int MIN_TEMPO = 1; + public static final int MAX_TEMPO = 20; - default void playNotes(final List notes) { - for (Note note : notes) { - this.playNote(note); - } - } - - void playNote(final Note note); + public static final Pattern NOTE_DATA_PATTERN = Pattern.compile("(\\d+)?>(.)"); + public static final String NOTE_DATA_MAPPING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!§½#£¤$%&/{[(])=}?\\+´`^~¨*'.;,:-_<µ€ÌìíÍïÏîÎóÓòÒöÖåÅäÄñÑõÕúÚùÙüûÜÛéÉèÈêÊë"; } diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Io.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Io.java new file mode 100644 index 0000000..496e6b3 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Io.java @@ -0,0 +1,154 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp2; + +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Layer; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Note; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song; +import net.raphimc.noteblocklib.model.Note; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.TreeMap; +import java.util.regex.Matcher; + +public class McSp2Io { + + private static final int BUFFER_SIZE = 1024 * 1024; + + public static McSp2Song readSong(final InputStream is, final String fileName) { + final Scanner scanner = new Scanner(new BufferedInputStream(is, BUFFER_SIZE), StandardCharsets.ISO_8859_1.name()).useDelimiter("[|\\n]"); + final McSp2Song song = new McSp2Song(fileName); + + scanner.nextInt(); // version? Is ignored by Minecraft Song Planner v2.5 + + final Map layers = song.getLayers(); + + song.setAutoSaveInterval(scanner.nextInt()); + song.setTitle(scanner.next()); + song.setAuthor(scanner.next()); + song.setOriginalAuthor(scanner.next()); + if (!scanner.next().isEmpty()) { + throw new IllegalStateException("Invalid MCSP2 header"); + } + + int tick = 0; + while (scanner.hasNext()) { + tick += scanner.nextInt(); + final Matcher noteData = McSp2Definitions.NOTE_DATA_PATTERN.matcher(scanner.next()); + + int layer = 0; + while (noteData.find()) { + if (noteData.groupCount() == 2) { + layer += Integer.parseInt(noteData.group(1)); + layers.computeIfAbsent(layer, k -> new McSp2Layer()).getNotes().put(tick, new McSp2Note().setInstrumentAndKey(noteData.group(2).charAt(0))); + } else if (noteData.groupCount() == 1) { + layers.computeIfAbsent(layer, k -> new McSp2Layer()).getNotes().put(tick, new McSp2Note().setInstrumentAndKey(noteData.group(1).charAt(0))); + } else if (noteData.groupCount() != 0) { + throw new IllegalArgumentException("Invalid note data: " + noteData.group(0)); + } + } + } + + try { // Optional metadata + scanner.nextLine(); + song.setTempo(scanner.nextInt()); + song.setLeftClicks(scanner.nextInt()); + song.setRightClicks(scanner.nextInt()); + song.setNoteBlocksAdded(scanner.nextInt()); + song.setNoteBlocksRemoved(scanner.nextInt()); + song.setMinutesSpent(scanner.nextInt()); + } catch (NoSuchElementException ignored) { + } + + { // Fill generalized song structure with data + song.getTempoEvents().set(0, song.getTempo()); + for (McSp2Layer layer : song.getLayers().values()) { + for (Map.Entry noteEntry : layer.getNotes().entrySet()) { + final McSp2Note mcSp2Note = noteEntry.getValue(); + + final Note note = new Note(); + note.setInstrument(MinecraftInstrument.fromNbsId(mcSp2Note.getInstrument())); + note.setMcKey(mcSp2Note.getKey()); + song.getNotes().add(noteEntry.getKey(), note); + } + } + } + + return song; + } + + public static void writeSong(final McSp2Song song, final OutputStream os) throws IOException { + final OutputStreamWriter writer = new OutputStreamWriter(new BufferedOutputStream(os, BUFFER_SIZE), StandardCharsets.ISO_8859_1); + writer.write("2"); + writer.write("|"); + writer.write(String.valueOf(song.getAutoSaveInterval())); + writer.write("|"); + writer.write(song.getTitleOr("").replace("|", "_")); + writer.write("|"); + writer.write(song.getAuthorOr("").replace("|", "_")); + writer.write("|"); + writer.write(song.getOriginalAuthorOr("").replace("|", "_")); + writer.write("|"); + + final Map> notes = new TreeMap<>(); + for (Map.Entry layerEntry : song.getLayers().entrySet()) { + for (Map.Entry noteEntry : layerEntry.getValue().getNotes().entrySet()) { + notes.computeIfAbsent(noteEntry.getKey(), k -> new TreeMap<>()).put(layerEntry.getKey(), noteEntry.getValue()); + } + } + + int lastTick = 0; + for (Map.Entry> tickEntry : notes.entrySet()) { + writer.write("|"); + writer.write(String.valueOf(tickEntry.getKey() - lastTick)); + lastTick = tickEntry.getKey(); + + int lastLayer = 0; + final StringBuilder noteData = new StringBuilder(); + for (Map.Entry layerEntry : tickEntry.getValue().entrySet()) { + noteData.append(layerEntry.getKey() - lastLayer); + noteData.append('>'); + noteData.append(layerEntry.getValue().getInstrumentAndKey()); + lastLayer = layerEntry.getKey(); + } + writer.write("|"); + writer.write(noteData.toString()); + } + writer.write("\n"); + + writer.write(String.valueOf(song.getTempo())); + writer.write("|"); + writer.write(String.valueOf(song.getLeftClicks())); + writer.write("|"); + writer.write(String.valueOf(song.getRightClicks())); + writer.write("|"); + writer.write(String.valueOf(song.getNoteBlocksAdded())); + writer.write("|"); + writer.write(String.valueOf(song.getNoteBlocksRemoved())); + writer.write("|"); + writer.write(String.valueOf(song.getMinutesSpent())); + + writer.flush(); + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpLayer.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Layer.java similarity index 58% rename from src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpLayer.java rename to src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Layer.java index ecd91a1..920842f 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/mcsp/model/McSpLayer.java +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Layer.java @@ -15,34 +15,30 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.format.mcsp.model; +package net.raphimc.noteblocklib.format.mcsp2.model; +import java.util.HashMap; import java.util.Map; -import java.util.TreeMap; -public class McSpLayer { +public class McSp2Layer { - private Map notesAtTick = new TreeMap<>(); - - public McSpLayer(final Map notesAtTick) { - this.notesAtTick = notesAtTick; - } - - public McSpLayer() { - } + private final Map notes = new HashMap<>(); /** * @return A map of all notes in this layer, with the tick as the key. */ - public Map getNotesAtTick() { - return this.notesAtTick; + public Map getNotes() { + return this.notes; } - /** - * @param notesAtTick A map of all notes in this layer, with the tick as the key. - */ - public void setNotesAtTick(final Map notesAtTick) { - this.notesAtTick = notesAtTick; + public McSp2Layer copy() { + final McSp2Layer copyLayer = new McSp2Layer(); + final Map notes = this.getNotes(); + final Map copyNotes = copyLayer.getNotes(); + for (final Map.Entry entry : notes.entrySet()) { + copyNotes.put(entry.getKey(), entry.getValue().copy()); + } + return copyLayer; } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Note.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Note.java new file mode 100644 index 0000000..0473093 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Note.java @@ -0,0 +1,81 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp2.model; + +import net.raphimc.noteblocklib.format.mcsp2.McSp2Definitions; + +import java.util.Objects; + +public class McSp2Note { + + private byte instrument; + private byte key; + + public byte getInstrument() { + return this.instrument; + } + + public McSp2Note setInstrument(final byte instrument) { + this.instrument = instrument; + return this; + } + + public byte getKey() { + return this.key; + } + + public McSp2Note setKey(final byte key) { + this.key = key; + return this; + } + + public McSp2Note setInstrumentAndKey(final char data) { + final int index = McSp2Definitions.NOTE_DATA_MAPPING.indexOf(data); + if (index == -1) { + throw new IllegalArgumentException("Invalid note data: " + data); + } + + this.instrument = (byte) (index / 25); + this.key = (byte) (index % 25); + return this; + } + + public char getInstrumentAndKey() { + return McSp2Definitions.NOTE_DATA_MAPPING.charAt(this.instrument * 25 + this.key); + } + + public McSp2Note copy() { + final McSp2Note copyNote = new McSp2Note(); + copyNote.setInstrument(this.getInstrument()); + copyNote.setKey(this.getKey()); + return copyNote; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + McSp2Note mcSp2Note = (McSp2Note) o; + return instrument == mcSp2Note.instrument && key == mcSp2Note.key; + } + + @Override + public int hashCode() { + return Objects.hash(instrument, key); + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Song.java b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Song.java new file mode 100644 index 0000000..cbb28da --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/mcsp2/model/McSp2Song.java @@ -0,0 +1,184 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.mcsp2.model; + +import net.raphimc.noteblocklib.format.SongFormat; +import net.raphimc.noteblocklib.model.Song; + +import java.util.HashMap; +import java.util.Map; + +public class McSp2Song extends Song { + + private int tempo; + private int autoSaveInterval; + private int leftClicks; + private int rightClicks; + private int noteBlocksAdded; + private int noteBlocksRemoved; + private int minutesSpent; + private final Map layers = new HashMap<>(); + + public McSp2Song() { + this(null); + } + + public McSp2Song(final String fileName) { + super(SongFormat.MCSP, fileName); + this.tempo = 10; + } + + /** + * @return The tempo of the song. Measured in ticks per second. + */ + public int getTempo() { + return this.tempo; + } + + /** + * @param tempo The tempo of the song. Measured in ticks per second. + * @return this + */ + public McSp2Song setTempo(final int tempo) { + this.tempo = tempo; + return this; + } + + /** + * @return The amount of minutes between each auto-save (0 indicates that auto-save is disabled) (0-60). + */ + public int getAutoSaveInterval() { + return this.autoSaveInterval; + } + + /** + * @param autoSaveInterval The amount of minutes between each auto-save (0 indicates that auto-save is disabled) (0-60). + * @return this + */ + public McSp2Song setAutoSaveInterval(final int autoSaveInterval) { + this.autoSaveInterval = autoSaveInterval; + return this; + } + + /** + * @return Amount of times the user has left-clicked. + */ + public int getLeftClicks() { + return this.leftClicks; + } + + /** + * @param leftClicks Amount of times the user has left-clicked. + * @return this + */ + public McSp2Song setLeftClicks(final int leftClicks) { + this.leftClicks = leftClicks; + return this; + } + + /** + * @return Amount of times the user has right-clicked. + */ + public int getRightClicks() { + return this.rightClicks; + } + + /** + * @param rightClicks Amount of times the user has right-clicked. + * @return this + */ + public McSp2Song setRightClicks(final int rightClicks) { + this.rightClicks = rightClicks; + return this; + } + + /** + * @return Amount of times the user has added a note block. + */ + public int getNoteBlocksAdded() { + return this.noteBlocksAdded; + } + + /** + * @param noteBlocksAdded Amount of times the user has added a note block. + * @return this + */ + public McSp2Song setNoteBlocksAdded(final int noteBlocksAdded) { + this.noteBlocksAdded = noteBlocksAdded; + return this; + } + + /** + * @return Amount of times the user has removed a note block. + */ + public int getNoteBlocksRemoved() { + return this.noteBlocksRemoved; + } + + /** + * @param noteBlocksRemoved Amount of times the user has removed a note block. + * @return this + */ + public McSp2Song setNoteBlocksRemoved(final int noteBlocksRemoved) { + this.noteBlocksRemoved = noteBlocksRemoved; + return this; + } + + /** + * @return Amount of minutes spent on the project. + */ + public int getMinutesSpent() { + return this.minutesSpent; + } + + /** + * @param minutesSpent Amount of minutes spent on the project. + * @return this + */ + public McSp2Song setMinutesSpent(final int minutesSpent) { + this.minutesSpent = minutesSpent; + return this; + } + + /** + * @return The layers of this song + */ + public Map getLayers() { + return this.layers; + } + + @Override + public McSp2Song copy() { + final McSp2Song copySong = new McSp2Song(this.getFileName()); + copySong.copyGeneralData(this); + copySong.setTempo(this.getTempo()); + copySong.setAutoSaveInterval(this.getAutoSaveInterval()); + copySong.setMinutesSpent(this.getMinutesSpent()); + copySong.setLeftClicks(this.getLeftClicks()); + copySong.setRightClicks(this.getRightClicks()); + copySong.setNoteBlocksAdded(this.getNoteBlocksAdded()); + copySong.setNoteBlocksRemoved(this.getNoteBlocksRemoved()); + final Map layers = this.getLayers(); + final Map copyLayers = copySong.getLayers(); + for (final Map.Entry entry : layers.entrySet()) { + copyLayers.put(entry.getKey(), entry.getValue().copy()); + } + return copySong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/MidiDefinitions.java b/src/main/java/net/raphimc/noteblocklib/format/midi/MidiDefinitions.java index 6b73a9c..2bfc967 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/MidiDefinitions.java +++ b/src/main/java/net/raphimc/noteblocklib/format/midi/MidiDefinitions.java @@ -19,18 +19,20 @@ public class MidiDefinitions { - public static final int SET_TEMPO = 0x51; + public static final int META_COPYRIGHT_NOTICE = 0x02; + public static final int META_TRACK_NAME = 0x03; + public static final int META_SET_TEMPO = 0x51; + public static final int PERCUSSION_CHANNEL = 9; public static final int VOLUME_CONTROL_MSB = 0x07; public static final int PAN_CONTROL_MSB = 0x0A; public static final int RESET_CONTROLS = 0x79; - public static final int CHANNELS = 16; - public static final byte NBS_KEY_OFFSET = 21; + public static final int CHANNEL_COUNT = 16; public static final int DEFAULT_TEMPO_MPQ = 500_000; public static final byte MAX_VELOCITY = 127; public static final byte CENTER_PAN = 64; - public static final float SONG_TICKS_PER_SECOND = 100F; + public static final float SONG_TARGET_TEMPO = 100F; } diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiData.java b/src/main/java/net/raphimc/noteblocklib/format/midi/MidiIo.java similarity index 53% rename from src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiData.java rename to src/main/java/net/raphimc/noteblocklib/format/midi/MidiIo.java index c5ade8c..670308c 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiData.java +++ b/src/main/java/net/raphimc/noteblocklib/format/midi/MidiIo.java @@ -15,80 +15,52 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.format.midi.model; +package net.raphimc.noteblocklib.format.midi; +import net.raphimc.noteblocklib.data.Constants; import net.raphimc.noteblocklib.format.midi.mapping.InstrumentMapping; import net.raphimc.noteblocklib.format.midi.mapping.MidiMappings; import net.raphimc.noteblocklib.format.midi.mapping.PercussionMapping; -import net.raphimc.noteblocklib.model.NotemapData; +import net.raphimc.noteblocklib.format.midi.model.MidiSong; +import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; +import net.raphimc.noteblocklib.model.Note; +import net.raphimc.noteblocklib.util.SongResampler; import javax.sound.midi.*; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import static javax.sound.midi.ShortMessage.*; import static net.raphimc.noteblocklib.format.midi.MidiDefinitions.*; -import static net.raphimc.noteblocklib.format.nbs.NbsDefinitions.*; -public class MidiData extends NotemapData { +public class MidiIo { - public MidiData(final MidiHeader header, final InputStream is) throws InvalidMidiDataException, IOException { - if (header.getMidiFileFormat().getType() != 0 && header.getMidiFileFormat().getType() != 1) { - throw new IllegalArgumentException("Midi file type must be 0 or 1"); - } - if (header.getMidiFileFormat().getDivisionType() != Sequence.PPQ) { - throw new IllegalArgumentException("Midi file division type must be PPQ"); - } + public static MidiSong readSong(final InputStream is, final String fileName) throws IOException, InvalidMidiDataException { final Sequence sequence = MidiSystem.getSequence(is); + final MidiSong song = new MidiSong(fileName); - final List tempoEvents = new ArrayList<>(); - for (int trackIdx = 0; trackIdx < sequence.getTracks().length; trackIdx++) { - final Track track = sequence.getTracks()[trackIdx]; - for (int eventIdx = 0; eventIdx < track.size(); eventIdx++) { - final MidiEvent event = track.get(eventIdx); - final MidiMessage message = event.getMessage(); - if (message instanceof MetaMessage) { - final MetaMessage metaMessage = (MetaMessage) message; - if (metaMessage.getType() == SET_TEMPO && metaMessage.getData().length == 3) { - final int newMpq = ((metaMessage.getData()[0] & 0xFF) << 16) | ((metaMessage.getData()[1] & 0xFF) << 8) | (metaMessage.getData()[2] & 0xFF); - tempoEvents.add(new TempoEvent(event.getTick(), (double) newMpq / sequence.getResolution())); - } - } - - } + if (sequence.getDivisionType() != Sequence.PPQ) { + throw new IllegalArgumentException("Unsupported MIDI division type: " + sequence.getDivisionType()); } - if (tempoEvents.isEmpty() || tempoEvents.get(0).getTick() != 0) { - tempoEvents.add(0, new TempoEvent(0L, (double) DEFAULT_TEMPO_MPQ / sequence.getResolution())); + if (sequence.getTickLength() > Integer.MAX_VALUE) { + throw new IllegalArgumentException("MIDI sequence has too many ticks"); } - tempoEvents.sort(Comparator.comparingLong(TempoEvent::getTick)); - final byte[] channelInstruments = new byte[CHANNELS]; - final byte[] channelVolumes = new byte[CHANNELS]; - final byte[] channelPans = new byte[CHANNELS]; - Arrays.fill(channelVolumes, MAX_VELOCITY); - Arrays.fill(channelPans, CENTER_PAN); + song.getTempoEvents().set(0, (float) (1_000_000D / ((double) MidiDefinitions.DEFAULT_TEMPO_MPQ / sequence.getResolution()))); + final byte[] channelInstruments = new byte[MidiDefinitions.CHANNEL_COUNT]; + final byte[] channelVolumes = new byte[MidiDefinitions.CHANNEL_COUNT]; + final byte[] channelPans = new byte[MidiDefinitions.CHANNEL_COUNT]; + Arrays.fill(channelVolumes, MidiDefinitions.MAX_VELOCITY); + Arrays.fill(channelPans, MidiDefinitions.CENTER_PAN); for (int trackIdx = 0; trackIdx < sequence.getTracks().length; trackIdx++) { final Track track = sequence.getTracks()[trackIdx]; - - double microTime = 0; - long lastTick = 0; - double microsPerTick = tempoEvents.get(0).getMicrosPerTick(); - int tempoEventIdx = 1; for (int eventIdx = 0; eventIdx < track.size(); eventIdx++) { final MidiEvent event = track.get(eventIdx); final MidiMessage message = event.getMessage(); - while (tempoEventIdx < tempoEvents.size() && event.getTick() > tempoEvents.get(tempoEventIdx).getTick()) { - final TempoEvent tempoEvent = tempoEvents.get(tempoEventIdx++); - microTime += (tempoEvent.getTick() - lastTick) * microsPerTick; - lastTick = tempoEvent.getTick(); - microsPerTick = tempoEvent.getMicrosPerTick(); - } - microTime += (event.getTick() - lastTick) * microsPerTick; - lastTick = event.getTick(); - if (message instanceof ShortMessage) { final ShortMessage shortMessage = (ShortMessage) message; switch (shortMessage.getCommand()) { @@ -96,25 +68,26 @@ public MidiData(final MidiHeader header, final InputStream is) throws InvalidMid final byte instrument = channelInstruments[shortMessage.getChannel()]; final byte key = (byte) shortMessage.getData1(); final byte velocity = (byte) shortMessage.getData2(); - final byte effectiveVelocity = (byte) ((float) velocity * channelVolumes[shortMessage.getChannel()] / MAX_VELOCITY); final byte pan = channelPans[shortMessage.getChannel()]; - final MidiNote note; + final Note note = new Note(); if (shortMessage.getChannel() == PERCUSSION_CHANNEL) { final PercussionMapping mapping = MidiMappings.PERCUSSION_MAPPINGS.get(key); if (mapping == null) continue; - note = new MidiNote(event.getTick(), mapping.getInstrument(), mapping.getKey(), effectiveVelocity, pan); + note.setInstrument(mapping.getInstrument()); + note.setNbsKey(mapping.getNbsKey()); } else { final InstrumentMapping mapping = MidiMappings.INSTRUMENT_MAPPINGS.get(instrument); if (mapping == null) continue; - final int transposedKey = key - NBS_KEY_OFFSET + KEYS_PER_OCTAVE * mapping.getOctaveModifier(); - final byte clampedKey = (byte) Math.max(NBS_LOWEST_KEY, Math.min(transposedKey, NBS_HIGHEST_KEY)); - note = new MidiNote(event.getTick(), mapping.getInstrument(), clampedKey, effectiveVelocity, pan); + note.setInstrument(mapping.getInstrument()); + note.setMidiKey(Math.max(NbsDefinitions.NBS_LOWEST_MIDI_KEY, Math.min(NbsDefinitions.NBS_HIGHEST_MIDI_KEY, key + Constants.KEYS_PER_OCTAVE * mapping.getOctaveModifier()))); } + note.setVolume(((float) velocity / MAX_VELOCITY) * (float) channelVolumes[shortMessage.getChannel()] / MAX_VELOCITY); + note.setPanning((float) (pan - CENTER_PAN) / CENTER_PAN); - this.notes.computeIfAbsent((int) Math.round(microTime * SONG_TICKS_PER_SECOND / 1_000_000D), k -> new ArrayList<>()).add(note); + song.getNotes().add((int) event.getTick(), note); break; case NOTE_OFF: // Ignore note off events @@ -145,31 +118,27 @@ public MidiData(final MidiHeader header, final InputStream is) throws InvalidMid Arrays.fill(channelPans, CENTER_PAN); break; } + } else if (message instanceof MetaMessage) { + final MetaMessage metaMessage = (MetaMessage) message; + if (metaMessage.getType() == META_SET_TEMPO && metaMessage.getData().length == 3) { + final int newMpq = ((metaMessage.getData()[0] & 0xFF) << 16) | ((metaMessage.getData()[1] & 0xFF) << 8) | (metaMessage.getData()[2] & 0xFF); + final double microsPerTick = (double) newMpq / sequence.getResolution(); + song.getTempoEvents().set((int) event.getTick(), (float) (1_000_000D / microsPerTick)); + } else if (metaMessage.getType() == META_COPYRIGHT_NOTICE) { + song.setOriginalAuthor(new String(metaMessage.getData(), StandardCharsets.US_ASCII)); + } else if (metaMessage.getType() == META_TRACK_NAME) { + song.getTrackNames().put(trackIdx, new String(metaMessage.getData(), StandardCharsets.US_ASCII)); + } } } } - } - public MidiData(final Map> notes) { - super(notes); - } - - private static class TempoEvent { - private final long tick; - private final double microsPerTick; - - public TempoEvent(final long tick, final double microsPerTick) { - this.tick = tick; - this.microsPerTick = microsPerTick; + final float maxTempo = song.getTempoEvents().getTempoRange()[1]; + if (maxTempo > SONG_TARGET_TEMPO) { + SongResampler.changeTickSpeed(song, SONG_TARGET_TEMPO); } - public long getTick() { - return this.tick; - } - - public double getMicrosPerTick() { - return this.microsPerTick; - } + return song; } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/MidiParser.java b/src/main/java/net/raphimc/noteblocklib/format/midi/MidiParser.java deleted file mode 100644 index c5a4ce2..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/MidiParser.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.midi; - -import net.raphimc.noteblocklib.format.midi.model.MidiData; -import net.raphimc.noteblocklib.format.midi.model.MidiHeader; - -import javax.sound.midi.InvalidMidiDataException; -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class MidiParser { - - public static MidiSong read(final byte[] bytes, final String fileName) throws InvalidMidiDataException, IOException { - final MidiHeader header = new MidiHeader(new ByteArrayInputStream(bytes)); - final MidiData data = new MidiData(header, new ByteArrayInputStream(bytes)); - - return new MidiSong(fileName, header, data); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/MidiSong.java b/src/main/java/net/raphimc/noteblocklib/format/midi/MidiSong.java deleted file mode 100644 index 659c3b4..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/MidiSong.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.midi; - -import net.raphimc.noteblocklib.format.SongFormat; -import net.raphimc.noteblocklib.format.midi.model.MidiData; -import net.raphimc.noteblocklib.format.midi.model.MidiHeader; -import net.raphimc.noteblocklib.format.midi.model.MidiNote; -import net.raphimc.noteblocklib.model.Song; -import net.raphimc.noteblocklib.model.SongView; - -import static net.raphimc.noteblocklib.format.midi.MidiDefinitions.SONG_TICKS_PER_SECOND; - -public class MidiSong extends Song { - - public MidiSong(final String fileName, final MidiHeader header, final MidiData data) { - super(SongFormat.MIDI, fileName, header, data); - } - - @Override - protected SongView createView() { - final String midiTitle = (String) this.getHeader().getMidiFileFormat().getProperty("title"); - final String title = midiTitle == null || midiTitle.isEmpty() ? this.fileName == null ? "MIDI Song" : this.fileName : midiTitle; - - return new SongView<>(title, SONG_TICKS_PER_SECOND, this.getData().getNotes()); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/InstrumentMapping.java b/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/InstrumentMapping.java index 194908c..a5ad374 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/InstrumentMapping.java +++ b/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/InstrumentMapping.java @@ -17,19 +17,19 @@ */ package net.raphimc.noteblocklib.format.midi.mapping; -import net.raphimc.noteblocklib.util.Instrument; +import net.raphimc.noteblocklib.data.MinecraftInstrument; public class InstrumentMapping { - private final Instrument instrument; + private final MinecraftInstrument instrument; private final int octaveModifier; - public InstrumentMapping(final Instrument instrument, final int octaveModifier) { + public InstrumentMapping(final MinecraftInstrument instrument, final int octaveModifier) { this.instrument = instrument; this.octaveModifier = octaveModifier; } - public Instrument getInstrument() { + public MinecraftInstrument getInstrument() { return this.instrument; } diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/MidiMappings.java b/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/MidiMappings.java index 52de031..0c55fb8 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/MidiMappings.java +++ b/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/MidiMappings.java @@ -17,7 +17,7 @@ */ package net.raphimc.noteblocklib.format.midi.mapping; -import net.raphimc.noteblocklib.util.Instrument; +import net.raphimc.noteblocklib.data.MinecraftInstrument; import java.util.HashMap; import java.util.Map; @@ -29,199 +29,199 @@ public class MidiMappings { public static final Map PERCUSSION_MAPPINGS = new HashMap<>(); static { - INSTRUMENT_MAPPINGS.put((byte) 0, new InstrumentMapping(Instrument.HARP, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 1, new InstrumentMapping(Instrument.PLING, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 2, new InstrumentMapping(Instrument.PLING, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 3, new InstrumentMapping(Instrument.PLING, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 4, new InstrumentMapping(Instrument.HARP, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 5, new InstrumentMapping(Instrument.HARP, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 6, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 7, new InstrumentMapping(Instrument.BANJO, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 8, new InstrumentMapping(Instrument.BELL, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 9, new InstrumentMapping(Instrument.BELL, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 10, new InstrumentMapping(Instrument.BELL, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 11, new InstrumentMapping(Instrument.IRON_XYLOPHONE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 12, new InstrumentMapping(Instrument.IRON_XYLOPHONE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 13, new InstrumentMapping(Instrument.XYLOPHONE, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 14, new InstrumentMapping(Instrument.BELL, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 15, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 16, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 17, new InstrumentMapping(Instrument.IRON_XYLOPHONE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 18, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 19, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 20, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 21, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 22, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 23, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 24, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 25, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 26, new InstrumentMapping(Instrument.HARP, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 27, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 28, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 29, new InstrumentMapping(Instrument.DIDGERIDOO, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 30, new InstrumentMapping(Instrument.DIDGERIDOO, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 31, new InstrumentMapping(Instrument.GUITAR, (byte) 3)); - INSTRUMENT_MAPPINGS.put((byte) 32, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 33, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 34, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 35, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 36, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 37, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 38, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 39, new InstrumentMapping(Instrument.PLING, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 40, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 41, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 42, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 43, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 44, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 45, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 46, new InstrumentMapping(Instrument.HARP, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 47, new InstrumentMapping(Instrument.SNARE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 48, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 49, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 50, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 51, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 52, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 53, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 54, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 55, new InstrumentMapping(Instrument.SNARE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 56, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 57, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 58, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 59, new InstrumentMapping(Instrument.DIDGERIDOO, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 60, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 61, new InstrumentMapping(Instrument.DIDGERIDOO, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 62, new InstrumentMapping(Instrument.DIDGERIDOO, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 63, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 64, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 65, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 66, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 67, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 68, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 69, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 70, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 71, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 72, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 73, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 74, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 75, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 76, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 77, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 78, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 79, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 80, new InstrumentMapping(Instrument.BIT, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 81, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 82, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 83, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 84, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 85, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 86, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 87, new InstrumentMapping(Instrument.BASS, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 88, new InstrumentMapping(Instrument.BELL, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 89, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 90, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 91, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 92, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 93, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 94, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 95, new InstrumentMapping(Instrument.CHIME, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 96, new InstrumentMapping(Instrument.CHIME, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 97, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 98, new InstrumentMapping(Instrument.CHIME, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 99, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 100, new InstrumentMapping(Instrument.PLING, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 101, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 102, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 103, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 104, new InstrumentMapping(Instrument.BANJO, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 105, new InstrumentMapping(Instrument.BANJO, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 106, new InstrumentMapping(Instrument.BANJO, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 107, new InstrumentMapping(Instrument.GUITAR, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 108, new InstrumentMapping(Instrument.IRON_XYLOPHONE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 109, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 110, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 111, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 112, new InstrumentMapping(Instrument.CHIME, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 113, new InstrumentMapping(Instrument.COW_BELL, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 114, new InstrumentMapping(Instrument.IRON_XYLOPHONE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 115, new InstrumentMapping(Instrument.XYLOPHONE, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 116, new InstrumentMapping(Instrument.BASS_DRUM, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 117, new InstrumentMapping(Instrument.SNARE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 118, new InstrumentMapping(Instrument.SNARE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 119, new InstrumentMapping(Instrument.CHIME, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 120, new InstrumentMapping(Instrument.HAT, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 121, new InstrumentMapping(Instrument.FLUTE, (byte) -1)); - INSTRUMENT_MAPPINGS.put((byte) 122, new InstrumentMapping(Instrument.CHIME, (byte) -2)); - INSTRUMENT_MAPPINGS.put((byte) 123, new InstrumentMapping(Instrument.FLUTE, (byte) 1)); - INSTRUMENT_MAPPINGS.put((byte) 124, new InstrumentMapping(Instrument.BELL, (byte) 2)); - INSTRUMENT_MAPPINGS.put((byte) 125, new InstrumentMapping(Instrument.BASS_DRUM, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 126, new InstrumentMapping(Instrument.SNARE, (byte) 0)); - INSTRUMENT_MAPPINGS.put((byte) 127, new InstrumentMapping(Instrument.SNARE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 0, new InstrumentMapping(MinecraftInstrument.HARP, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 1, new InstrumentMapping(MinecraftInstrument.PLING, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 2, new InstrumentMapping(MinecraftInstrument.PLING, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 3, new InstrumentMapping(MinecraftInstrument.PLING, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 4, new InstrumentMapping(MinecraftInstrument.HARP, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 5, new InstrumentMapping(MinecraftInstrument.HARP, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 6, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 7, new InstrumentMapping(MinecraftInstrument.BANJO, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 8, new InstrumentMapping(MinecraftInstrument.BELL, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 9, new InstrumentMapping(MinecraftInstrument.BELL, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 10, new InstrumentMapping(MinecraftInstrument.BELL, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 11, new InstrumentMapping(MinecraftInstrument.IRON_XYLOPHONE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 12, new InstrumentMapping(MinecraftInstrument.IRON_XYLOPHONE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 13, new InstrumentMapping(MinecraftInstrument.XYLOPHONE, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 14, new InstrumentMapping(MinecraftInstrument.BELL, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 15, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 16, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 17, new InstrumentMapping(MinecraftInstrument.IRON_XYLOPHONE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 18, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 19, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 20, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 21, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 22, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 23, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 24, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 25, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 26, new InstrumentMapping(MinecraftInstrument.HARP, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 27, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 28, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 29, new InstrumentMapping(MinecraftInstrument.DIDGERIDOO, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 30, new InstrumentMapping(MinecraftInstrument.DIDGERIDOO, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 31, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 3)); + INSTRUMENT_MAPPINGS.put((byte) 32, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 33, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 34, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 35, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 36, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 37, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 38, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 39, new InstrumentMapping(MinecraftInstrument.PLING, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 40, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 41, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 42, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 43, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 44, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 45, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 46, new InstrumentMapping(MinecraftInstrument.HARP, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 47, new InstrumentMapping(MinecraftInstrument.SNARE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 48, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 49, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 50, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 51, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 52, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 53, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 54, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 55, new InstrumentMapping(MinecraftInstrument.SNARE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 56, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 57, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 58, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 59, new InstrumentMapping(MinecraftInstrument.DIDGERIDOO, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 60, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 61, new InstrumentMapping(MinecraftInstrument.DIDGERIDOO, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 62, new InstrumentMapping(MinecraftInstrument.DIDGERIDOO, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 63, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 64, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 65, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 66, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 67, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 68, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 69, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 70, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 71, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 72, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 73, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 74, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 75, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 76, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 77, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 78, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 79, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 80, new InstrumentMapping(MinecraftInstrument.BIT, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 81, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 82, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 83, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 84, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 85, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 86, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 87, new InstrumentMapping(MinecraftInstrument.BASS, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 88, new InstrumentMapping(MinecraftInstrument.BELL, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 89, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 90, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 91, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 92, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 93, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 94, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 95, new InstrumentMapping(MinecraftInstrument.CHIME, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 96, new InstrumentMapping(MinecraftInstrument.CHIME, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 97, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 98, new InstrumentMapping(MinecraftInstrument.CHIME, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 99, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 100, new InstrumentMapping(MinecraftInstrument.PLING, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 101, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 102, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 103, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 104, new InstrumentMapping(MinecraftInstrument.BANJO, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 105, new InstrumentMapping(MinecraftInstrument.BANJO, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 106, new InstrumentMapping(MinecraftInstrument.BANJO, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 107, new InstrumentMapping(MinecraftInstrument.GUITAR, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 108, new InstrumentMapping(MinecraftInstrument.IRON_XYLOPHONE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 109, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 110, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 111, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 112, new InstrumentMapping(MinecraftInstrument.CHIME, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 113, new InstrumentMapping(MinecraftInstrument.COW_BELL, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 114, new InstrumentMapping(MinecraftInstrument.IRON_XYLOPHONE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 115, new InstrumentMapping(MinecraftInstrument.XYLOPHONE, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 116, new InstrumentMapping(MinecraftInstrument.BASS_DRUM, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 117, new InstrumentMapping(MinecraftInstrument.SNARE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 118, new InstrumentMapping(MinecraftInstrument.SNARE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 119, new InstrumentMapping(MinecraftInstrument.CHIME, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 120, new InstrumentMapping(MinecraftInstrument.HAT, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 121, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) -1)); + INSTRUMENT_MAPPINGS.put((byte) 122, new InstrumentMapping(MinecraftInstrument.CHIME, (byte) -2)); + INSTRUMENT_MAPPINGS.put((byte) 123, new InstrumentMapping(MinecraftInstrument.FLUTE, (byte) 1)); + INSTRUMENT_MAPPINGS.put((byte) 124, new InstrumentMapping(MinecraftInstrument.BELL, (byte) 2)); + INSTRUMENT_MAPPINGS.put((byte) 125, new InstrumentMapping(MinecraftInstrument.BASS_DRUM, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 126, new InstrumentMapping(MinecraftInstrument.SNARE, (byte) 0)); + INSTRUMENT_MAPPINGS.put((byte) 127, new InstrumentMapping(MinecraftInstrument.SNARE, (byte) 0)); - PERCUSSION_MAPPINGS.put((byte) 24, new PercussionMapping(Instrument.BIT, (byte) 72)); - PERCUSSION_MAPPINGS.put((byte) 25, new PercussionMapping(Instrument.SNARE, (byte) 41)); - PERCUSSION_MAPPINGS.put((byte) 26, new PercussionMapping(Instrument.HAT, (byte) 58)); - PERCUSSION_MAPPINGS.put((byte) 27, new PercussionMapping(Instrument.SNARE, (byte) 51)); - PERCUSSION_MAPPINGS.put((byte) 28, new PercussionMapping(Instrument.SNARE, (byte) 60)); - PERCUSSION_MAPPINGS.put((byte) 29, new PercussionMapping(Instrument.HAT, (byte) 49)); - PERCUSSION_MAPPINGS.put((byte) 30, new PercussionMapping(Instrument.HAT, (byte) 46)); - PERCUSSION_MAPPINGS.put((byte) 31, new PercussionMapping(Instrument.HAT, (byte) 42)); - PERCUSSION_MAPPINGS.put((byte) 32, new PercussionMapping(Instrument.HAT, (byte) 39)); - PERCUSSION_MAPPINGS.put((byte) 33, new PercussionMapping(Instrument.HAT, (byte) 35)); - PERCUSSION_MAPPINGS.put((byte) 34, new PercussionMapping(Instrument.CHIME, (byte) 50)); - PERCUSSION_MAPPINGS.put((byte) 35, new PercussionMapping(Instrument.BASS_DRUM, (byte) 43)); - PERCUSSION_MAPPINGS.put((byte) 36, new PercussionMapping(Instrument.BASS_DRUM, (byte) 39)); - PERCUSSION_MAPPINGS.put((byte) 37, new PercussionMapping(Instrument.HAT, (byte) 39)); - PERCUSSION_MAPPINGS.put((byte) 38, new PercussionMapping(Instrument.SNARE, (byte) 41)); - PERCUSSION_MAPPINGS.put((byte) 39, new PercussionMapping(Instrument.HAT, (byte) 39)); - PERCUSSION_MAPPINGS.put((byte) 40, new PercussionMapping(Instrument.SNARE, (byte) 37)); - PERCUSSION_MAPPINGS.put((byte) 41, new PercussionMapping(Instrument.BASS_DRUM, (byte) 39)); - PERCUSSION_MAPPINGS.put((byte) 42, new PercussionMapping(Instrument.SNARE, (byte) 55)); - PERCUSSION_MAPPINGS.put((byte) 43, new PercussionMapping(Instrument.BASS_DRUM, (byte) 46)); - PERCUSSION_MAPPINGS.put((byte) 44, new PercussionMapping(Instrument.SNARE, (byte) 55)); - PERCUSSION_MAPPINGS.put((byte) 45, new PercussionMapping(Instrument.BASS_DRUM, (byte) 48)); - PERCUSSION_MAPPINGS.put((byte) 46, new PercussionMapping(Instrument.SNARE, (byte) 51)); - PERCUSSION_MAPPINGS.put((byte) 47, new PercussionMapping(Instrument.BASS_DRUM, (byte) 53)); - PERCUSSION_MAPPINGS.put((byte) 48, new PercussionMapping(Instrument.BASS_DRUM, (byte) 56)); - PERCUSSION_MAPPINGS.put((byte) 49, new PercussionMapping(Instrument.SNARE, (byte) 50)); - PERCUSSION_MAPPINGS.put((byte) 50, new PercussionMapping(Instrument.BASS_DRUM, (byte) 56)); - PERCUSSION_MAPPINGS.put((byte) 51, new PercussionMapping(Instrument.SNARE, (byte) 57)); - PERCUSSION_MAPPINGS.put((byte) 52, new PercussionMapping(Instrument.SNARE, (byte) 41)); - PERCUSSION_MAPPINGS.put((byte) 53, new PercussionMapping(Instrument.SNARE, (byte) 46)); - PERCUSSION_MAPPINGS.put((byte) 54, new PercussionMapping(Instrument.HAT, (byte) 51)); - PERCUSSION_MAPPINGS.put((byte) 55, new PercussionMapping(Instrument.SNARE, (byte) 51)); - PERCUSSION_MAPPINGS.put((byte) 56, new PercussionMapping(Instrument.COW_BELL, (byte) 38)); - PERCUSSION_MAPPINGS.put((byte) 57, new PercussionMapping(Instrument.SNARE, (byte) 46)); - PERCUSSION_MAPPINGS.put((byte) 58, new PercussionMapping(Instrument.HAT, (byte) 35)); - PERCUSSION_MAPPINGS.put((byte) 59, new PercussionMapping(Instrument.SNARE, (byte) 46)); - PERCUSSION_MAPPINGS.put((byte) 60, new PercussionMapping(Instrument.HAT, (byte) 42)); - PERCUSSION_MAPPINGS.put((byte) 61, new PercussionMapping(Instrument.HAT, (byte) 35)); - PERCUSSION_MAPPINGS.put((byte) 62, new PercussionMapping(Instrument.HAT, (byte) 41)); - PERCUSSION_MAPPINGS.put((byte) 63, new PercussionMapping(Instrument.BASS_DRUM, (byte) 55)); - PERCUSSION_MAPPINGS.put((byte) 64, new PercussionMapping(Instrument.BASS_DRUM, (byte) 48)); - PERCUSSION_MAPPINGS.put((byte) 65, new PercussionMapping(Instrument.SNARE, (byte) 46)); - PERCUSSION_MAPPINGS.put((byte) 66, new PercussionMapping(Instrument.SNARE, (byte) 41)); - PERCUSSION_MAPPINGS.put((byte) 67, new PercussionMapping(Instrument.XYLOPHONE, (byte) 45)); - PERCUSSION_MAPPINGS.put((byte) 68, new PercussionMapping(Instrument.XYLOPHONE, (byte) 38)); - PERCUSSION_MAPPINGS.put((byte) 69, new PercussionMapping(Instrument.HAT, (byte) 53)); - PERCUSSION_MAPPINGS.put((byte) 70, new PercussionMapping(Instrument.HAT, (byte) 56)); - PERCUSSION_MAPPINGS.put((byte) 71, new PercussionMapping(Instrument.FLUTE, (byte) 67)); - PERCUSSION_MAPPINGS.put((byte) 72, new PercussionMapping(Instrument.FLUTE, (byte) 66)); - PERCUSSION_MAPPINGS.put((byte) 73, new PercussionMapping(Instrument.HAT, (byte) 50)); - PERCUSSION_MAPPINGS.put((byte) 74, new PercussionMapping(Instrument.HAT, (byte) 44)); - PERCUSSION_MAPPINGS.put((byte) 75, new PercussionMapping(Instrument.HAT, (byte) 51)); - PERCUSSION_MAPPINGS.put((byte) 76, new PercussionMapping(Instrument.HAT, (byte) 43)); - PERCUSSION_MAPPINGS.put((byte) 77, new PercussionMapping(Instrument.HAT, (byte) 38)); - PERCUSSION_MAPPINGS.put((byte) 78, new PercussionMapping(Instrument.DIDGERIDOO, (byte) 58)); - PERCUSSION_MAPPINGS.put((byte) 79, new PercussionMapping(Instrument.DIDGERIDOO, (byte) 59)); - PERCUSSION_MAPPINGS.put((byte) 80, new PercussionMapping(Instrument.HAT, (byte) 49)); - PERCUSSION_MAPPINGS.put((byte) 81, new PercussionMapping(Instrument.CHIME, (byte) 52)); - PERCUSSION_MAPPINGS.put((byte) 82, new PercussionMapping(Instrument.SNARE, (byte) 55)); - PERCUSSION_MAPPINGS.put((byte) 83, new PercussionMapping(Instrument.CHIME, (byte) 39)); - PERCUSSION_MAPPINGS.put((byte) 84, new PercussionMapping(Instrument.CHIME, (byte) 48)); - PERCUSSION_MAPPINGS.put((byte) 85, new PercussionMapping(Instrument.HAT, (byte) 54)); - PERCUSSION_MAPPINGS.put((byte) 86, new PercussionMapping(Instrument.BASS_DRUM, (byte) 47)); - PERCUSSION_MAPPINGS.put((byte) 87, new PercussionMapping(Instrument.BASS_DRUM, (byte) 40)); + PERCUSSION_MAPPINGS.put((byte) 24, new PercussionMapping(MinecraftInstrument.BIT, (byte) 72)); + PERCUSSION_MAPPINGS.put((byte) 25, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 41)); + PERCUSSION_MAPPINGS.put((byte) 26, new PercussionMapping(MinecraftInstrument.HAT, (byte) 58)); + PERCUSSION_MAPPINGS.put((byte) 27, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 51)); + PERCUSSION_MAPPINGS.put((byte) 28, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 60)); + PERCUSSION_MAPPINGS.put((byte) 29, new PercussionMapping(MinecraftInstrument.HAT, (byte) 49)); + PERCUSSION_MAPPINGS.put((byte) 30, new PercussionMapping(MinecraftInstrument.HAT, (byte) 46)); + PERCUSSION_MAPPINGS.put((byte) 31, new PercussionMapping(MinecraftInstrument.HAT, (byte) 42)); + PERCUSSION_MAPPINGS.put((byte) 32, new PercussionMapping(MinecraftInstrument.HAT, (byte) 39)); + PERCUSSION_MAPPINGS.put((byte) 33, new PercussionMapping(MinecraftInstrument.HAT, (byte) 35)); + PERCUSSION_MAPPINGS.put((byte) 34, new PercussionMapping(MinecraftInstrument.CHIME, (byte) 50)); + PERCUSSION_MAPPINGS.put((byte) 35, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 43)); + PERCUSSION_MAPPINGS.put((byte) 36, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 39)); + PERCUSSION_MAPPINGS.put((byte) 37, new PercussionMapping(MinecraftInstrument.HAT, (byte) 39)); + PERCUSSION_MAPPINGS.put((byte) 38, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 41)); + PERCUSSION_MAPPINGS.put((byte) 39, new PercussionMapping(MinecraftInstrument.HAT, (byte) 39)); + PERCUSSION_MAPPINGS.put((byte) 40, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 37)); + PERCUSSION_MAPPINGS.put((byte) 41, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 39)); + PERCUSSION_MAPPINGS.put((byte) 42, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 55)); + PERCUSSION_MAPPINGS.put((byte) 43, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 46)); + PERCUSSION_MAPPINGS.put((byte) 44, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 55)); + PERCUSSION_MAPPINGS.put((byte) 45, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 48)); + PERCUSSION_MAPPINGS.put((byte) 46, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 51)); + PERCUSSION_MAPPINGS.put((byte) 47, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 53)); + PERCUSSION_MAPPINGS.put((byte) 48, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 56)); + PERCUSSION_MAPPINGS.put((byte) 49, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 50)); + PERCUSSION_MAPPINGS.put((byte) 50, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 56)); + PERCUSSION_MAPPINGS.put((byte) 51, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 57)); + PERCUSSION_MAPPINGS.put((byte) 52, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 41)); + PERCUSSION_MAPPINGS.put((byte) 53, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 46)); + PERCUSSION_MAPPINGS.put((byte) 54, new PercussionMapping(MinecraftInstrument.HAT, (byte) 51)); + PERCUSSION_MAPPINGS.put((byte) 55, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 51)); + PERCUSSION_MAPPINGS.put((byte) 56, new PercussionMapping(MinecraftInstrument.COW_BELL, (byte) 38)); + PERCUSSION_MAPPINGS.put((byte) 57, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 46)); + PERCUSSION_MAPPINGS.put((byte) 58, new PercussionMapping(MinecraftInstrument.HAT, (byte) 35)); + PERCUSSION_MAPPINGS.put((byte) 59, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 46)); + PERCUSSION_MAPPINGS.put((byte) 60, new PercussionMapping(MinecraftInstrument.HAT, (byte) 42)); + PERCUSSION_MAPPINGS.put((byte) 61, new PercussionMapping(MinecraftInstrument.HAT, (byte) 35)); + PERCUSSION_MAPPINGS.put((byte) 62, new PercussionMapping(MinecraftInstrument.HAT, (byte) 41)); + PERCUSSION_MAPPINGS.put((byte) 63, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 55)); + PERCUSSION_MAPPINGS.put((byte) 64, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 48)); + PERCUSSION_MAPPINGS.put((byte) 65, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 46)); + PERCUSSION_MAPPINGS.put((byte) 66, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 41)); + PERCUSSION_MAPPINGS.put((byte) 67, new PercussionMapping(MinecraftInstrument.XYLOPHONE, (byte) 45)); + PERCUSSION_MAPPINGS.put((byte) 68, new PercussionMapping(MinecraftInstrument.XYLOPHONE, (byte) 38)); + PERCUSSION_MAPPINGS.put((byte) 69, new PercussionMapping(MinecraftInstrument.HAT, (byte) 53)); + PERCUSSION_MAPPINGS.put((byte) 70, new PercussionMapping(MinecraftInstrument.HAT, (byte) 56)); + PERCUSSION_MAPPINGS.put((byte) 71, new PercussionMapping(MinecraftInstrument.FLUTE, (byte) 67)); + PERCUSSION_MAPPINGS.put((byte) 72, new PercussionMapping(MinecraftInstrument.FLUTE, (byte) 66)); + PERCUSSION_MAPPINGS.put((byte) 73, new PercussionMapping(MinecraftInstrument.HAT, (byte) 50)); + PERCUSSION_MAPPINGS.put((byte) 74, new PercussionMapping(MinecraftInstrument.HAT, (byte) 44)); + PERCUSSION_MAPPINGS.put((byte) 75, new PercussionMapping(MinecraftInstrument.HAT, (byte) 51)); + PERCUSSION_MAPPINGS.put((byte) 76, new PercussionMapping(MinecraftInstrument.HAT, (byte) 43)); + PERCUSSION_MAPPINGS.put((byte) 77, new PercussionMapping(MinecraftInstrument.HAT, (byte) 38)); + PERCUSSION_MAPPINGS.put((byte) 78, new PercussionMapping(MinecraftInstrument.DIDGERIDOO, (byte) 58)); + PERCUSSION_MAPPINGS.put((byte) 79, new PercussionMapping(MinecraftInstrument.DIDGERIDOO, (byte) 59)); + PERCUSSION_MAPPINGS.put((byte) 80, new PercussionMapping(MinecraftInstrument.HAT, (byte) 49)); + PERCUSSION_MAPPINGS.put((byte) 81, new PercussionMapping(MinecraftInstrument.CHIME, (byte) 52)); + PERCUSSION_MAPPINGS.put((byte) 82, new PercussionMapping(MinecraftInstrument.SNARE, (byte) 55)); + PERCUSSION_MAPPINGS.put((byte) 83, new PercussionMapping(MinecraftInstrument.CHIME, (byte) 39)); + PERCUSSION_MAPPINGS.put((byte) 84, new PercussionMapping(MinecraftInstrument.CHIME, (byte) 48)); + PERCUSSION_MAPPINGS.put((byte) 85, new PercussionMapping(MinecraftInstrument.HAT, (byte) 54)); + PERCUSSION_MAPPINGS.put((byte) 86, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 47)); + PERCUSSION_MAPPINGS.put((byte) 87, new PercussionMapping(MinecraftInstrument.BASS_DRUM, (byte) 40)); } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/PercussionMapping.java b/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/PercussionMapping.java index 42b34a9..819a602 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/PercussionMapping.java +++ b/src/main/java/net/raphimc/noteblocklib/format/midi/mapping/PercussionMapping.java @@ -17,24 +17,24 @@ */ package net.raphimc.noteblocklib.format.midi.mapping; -import net.raphimc.noteblocklib.util.Instrument; +import net.raphimc.noteblocklib.data.MinecraftInstrument; public class PercussionMapping { - private final Instrument instrument; - private final byte key; + private final MinecraftInstrument instrument; + private final byte nbsKey; - public PercussionMapping(final Instrument instrument, final byte key) { + public PercussionMapping(final MinecraftInstrument instrument, final byte nbsKey) { this.instrument = instrument; - this.key = key; + this.nbsKey = nbsKey; } - public Instrument getInstrument() { + public MinecraftInstrument getInstrument() { return this.instrument; } - public byte getKey() { - return this.key; + public byte getNbsKey() { + return this.nbsKey; } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiHeader.java b/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiHeader.java deleted file mode 100644 index 5f0d476..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiHeader.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.midi.model; - -import net.raphimc.noteblocklib.model.Header; - -import javax.sound.midi.InvalidMidiDataException; -import javax.sound.midi.MidiFileFormat; -import javax.sound.midi.MidiSystem; -import java.io.IOException; -import java.io.InputStream; - -public class MidiHeader implements Header { - - private MidiFileFormat midiFileFormat; - - public MidiHeader(final InputStream is) throws IOException, InvalidMidiDataException { - this.midiFileFormat = MidiSystem.getMidiFileFormat(is); - } - - public MidiHeader(final MidiFileFormat midiFileFormat) { - this.midiFileFormat = midiFileFormat; - } - - public MidiFileFormat getMidiFileFormat() { - return this.midiFileFormat; - } - - public void setMidiFileFormat(final MidiFileFormat midiFileFormat) { - this.midiFileFormat = midiFileFormat; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiNote.java b/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiNote.java deleted file mode 100644 index ef770ce..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiNote.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.midi.model; - -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.model.NoteWithPanning; -import net.raphimc.noteblocklib.model.NoteWithVolume; -import net.raphimc.noteblocklib.util.Instrument; - -import java.util.Objects; - -import static net.raphimc.noteblocklib.format.midi.MidiDefinitions.CENTER_PAN; -import static net.raphimc.noteblocklib.format.midi.MidiDefinitions.MAX_VELOCITY; - -public class MidiNote extends Note implements NoteWithVolume, NoteWithPanning { - - private final long midiTick; - private byte velocity; - private byte panning; - - public MidiNote(final long midiTick, final Instrument instrument, final byte key, final byte velocity, final byte panning) { - super(instrument, key); - - this.midiTick = midiTick; - this.velocity = velocity; - this.panning = panning; - } - - /** - * The MIDI tick of this note.
- * This value is excluded from equals and hashcode. - * - * @return The tick of the note in the midi sequence. - */ - public long getMidiTick() { - return this.midiTick; - } - - @Override - public float getVolume() { - return (float) this.velocity / MAX_VELOCITY * 100F; - } - - @Override - public void setVolume(final float volume) { - this.velocity = (byte) (volume / 100F * MAX_VELOCITY); - } - - public byte getRawVelocity() { - return this.velocity; - } - - @Override - public float getPanning() { - return ((this.panning - CENTER_PAN) / (float) CENTER_PAN) * 100F; - } - - @Override - public void setPanning(final float panning) { - this.panning = (byte) (panning / 100F * CENTER_PAN + CENTER_PAN); - } - - public byte getRawPanning() { - return this.panning; - } - - @Override - public MidiNote clone() { - return new MidiNote(this.midiTick, this.instrument, this.key, this.velocity, this.panning); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - MidiNote midiNote = (MidiNote) o; - return velocity == midiNote.velocity && panning == midiNote.panning; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), velocity, panning); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtSong.java b/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiSong.java similarity index 57% rename from src/main/java/net/raphimc/noteblocklib/format/txt/TxtSong.java rename to src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiSong.java index 1bb2579..1963ab4 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtSong.java +++ b/src/main/java/net/raphimc/noteblocklib/format/midi/model/MidiSong.java @@ -15,26 +15,36 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.format.txt; +package net.raphimc.noteblocklib.format.midi.model; import net.raphimc.noteblocklib.format.SongFormat; -import net.raphimc.noteblocklib.format.txt.model.TxtData; -import net.raphimc.noteblocklib.format.txt.model.TxtHeader; -import net.raphimc.noteblocklib.format.txt.model.TxtNote; import net.raphimc.noteblocklib.model.Song; -import net.raphimc.noteblocklib.model.SongView; -public class TxtSong extends Song { +import java.util.HashMap; +import java.util.Map; - public TxtSong(final String fileName, final TxtHeader header, final TxtData data) { - super(SongFormat.TXT, fileName, header, data); +public class MidiSong extends Song { + + private final Map trackNames = new HashMap<>(); + + public MidiSong() { + this(null); } - @Override - protected SongView createView() { - final String title = this.fileName == null ? "Txt Song" : this.fileName; + public MidiSong(final String fileName) { + super(SongFormat.MIDI, fileName); + } - return new SongView<>(title, this.getHeader().getSpeed(), this.getData().getNotes()); + public Map getTrackNames() { + return this.trackNames; + } + + @Override + public MidiSong copy() { + final MidiSong copySong = new MidiSong(this.getFileName()); + copySong.copyGeneralData(this); + copySong.getTrackNames().putAll(this.getTrackNames()); + return copySong; } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsConverter.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsConverter.java new file mode 100644 index 0000000..6092294 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsConverter.java @@ -0,0 +1,124 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.nbs; + +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song; +import net.raphimc.noteblocklib.format.nbs.model.NbsCustomInstrument; +import net.raphimc.noteblocklib.format.nbs.model.NbsLayer; +import net.raphimc.noteblocklib.format.nbs.model.NbsNote; +import net.raphimc.noteblocklib.format.nbs.model.NbsSong; +import net.raphimc.noteblocklib.model.Note; +import net.raphimc.noteblocklib.model.Song; + +import java.util.List; + +public class NbsConverter { + + /** + * Creates a new NBS song from the general data of the given song (Also copies some format specific fields if applicable). + * + * @param song The song + * @return The new NBS song + */ + public static NbsSong createSong(final Song song) { + final NbsSong newSong = new NbsSong(); + newSong.copyGeneralData(song); + newSong.setLength((short) song.getNotes().getLengthInTicks()); + newSong.setTempo((short) Math.round(song.getTempoEvents().get(0) * 100F)); + + for (int tick : song.getNotes().getTicks()) { + final List notes = song.getNotes().get(tick); + for (int i = 0; i < notes.size(); i++) { + final Note note = notes.get(i); + final NbsNote nbsNote = new NbsNote(); + if (note.getInstrument() instanceof MinecraftInstrument) { + nbsNote.setInstrument(((MinecraftInstrument) note.getInstrument()).nbsId()); + } else if (note.getInstrument() instanceof NbsCustomInstrument) { + final NbsCustomInstrument customInstrument = (NbsCustomInstrument) note.getInstrument(); + if (!newSong.getCustomInstruments().contains(customInstrument)) { + newSong.getCustomInstruments().add(customInstrument); + } + nbsNote.setInstrument((short) (newSong.getVanillaInstrumentCount() + newSong.getCustomInstruments().indexOf(customInstrument))); + } else { + continue; + } + nbsNote.setKey((byte) Math.max(NbsDefinitions.NBS_LOWEST_KEY, Math.min(NbsDefinitions.NBS_HIGHEST_KEY, note.getNbsKey()))); + nbsNote.setVelocity((byte) Math.round(note.getVolume() * 100F)); + nbsNote.setPanning((short) (Math.round(note.getPanning() * 100F) + NbsDefinitions.CENTER_PANNING)); + nbsNote.setPitch((short) Math.round(note.getFractionalKeyPart() * 100F)); + + final NbsLayer nbsLayer = newSong.getLayers().computeIfAbsent(i, k -> new NbsLayer()); + nbsLayer.getNotes().put(tick, nbsNote); + } + } + newSong.getCustomInstruments().replaceAll(NbsCustomInstrument::copy); + + if (song.getTempoEvents().getTicks().size() > 1) { + final NbsCustomInstrument tempoChangerInstrument = new NbsCustomInstrument(); + tempoChangerInstrument.setName(NbsDefinitions.TEMPO_CHANGER_CUSTOM_INSTRUMENT_NAME); + final short instrumentId = (short) (newSong.getVanillaInstrumentCount() + newSong.getCustomInstruments().size()); + newSong.getCustomInstruments().add(tempoChangerInstrument); + + final NbsLayer tempoChangerLayer = new NbsLayer(); + tempoChangerLayer.setName(NbsDefinitions.TEMPO_CHANGER_CUSTOM_INSTRUMENT_NAME); + tempoChangerLayer.setVolume((byte) 0); + newSong.getLayers().put(newSong.getLayers().size(), tempoChangerLayer); + + for (int tempoEventTick : song.getTempoEvents().getTicks()) { + final float tps = song.getTempoEvents().get(tempoEventTick); + final NbsNote tempoChangerNote = new NbsNote(); + tempoChangerNote.setInstrument(instrumentId); + tempoChangerNote.setKey((byte) NbsDefinitions.F_SHARP_4_NBS_KEY); + tempoChangerNote.setPitch((short) Math.round(tps * 15F)); + tempoChangerLayer.getNotes().put(tempoEventTick, tempoChangerNote); + } + } + + newSong.setLayerCount((short) newSong.getLayers().size()); + newSong.setSourceFileName(song.getFileName()); + + if (song instanceof NbsSong) { + final NbsSong nbsSong = (NbsSong) song; + newSong.setAutoSave(nbsSong.isAutoSave()); + newSong.setAutoSaveInterval(nbsSong.getAutoSaveInterval()); + newSong.setTimeSignature(nbsSong.getTimeSignature()); + newSong.setMinutesSpent(nbsSong.getMinutesSpent()); + newSong.setLeftClicks(nbsSong.getLeftClicks()); + newSong.setRightClicks(nbsSong.getRightClicks()); + newSong.setNoteBlocksAdded(nbsSong.getNoteBlocksAdded()); + newSong.setNoteBlocksRemoved(nbsSong.getNoteBlocksRemoved()); + newSong.setSourceFileName(nbsSong.getSourceFileName()); + newSong.setLoop(nbsSong.isLoop()); + newSong.setMaxLoopCount(nbsSong.getMaxLoopCount()); + newSong.setLoopStartTick(nbsSong.getLoopStartTick()); + } else if (song instanceof McSp2Song) { + final McSp2Song mcSp2Song = (McSp2Song) song; + newSong.setAutoSave(mcSp2Song.getAutoSaveInterval() != 0); + newSong.setAutoSaveInterval((byte) mcSp2Song.getAutoSaveInterval()); + newSong.setMinutesSpent(mcSp2Song.getMinutesSpent()); + newSong.setLeftClicks(mcSp2Song.getLeftClicks()); + newSong.setRightClicks(mcSp2Song.getRightClicks()); + newSong.setNoteBlocksAdded(mcSp2Song.getNoteBlocksAdded()); + newSong.setNoteBlocksRemoved(mcSp2Song.getNoteBlocksRemoved()); + } + + return newSong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsDefinitions.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsDefinitions.java index e1e0ccf..b594164 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsDefinitions.java +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsDefinitions.java @@ -21,11 +21,18 @@ public class NbsDefinitions { + public static final int NBS_LOWEST_MIDI_KEY = 21; + public static final int NBS_HIGHEST_MIDI_KEY = 108; public static final int NBS_LOWEST_KEY = 0; public static final int NBS_HIGHEST_KEY = 87; + public static final int F_SHARP_4_NBS_KEY = 45; - public static final int KEYS_PER_OCTAVE = 12; public static final int PITCHES_PER_KEY = 100; + public static final int PITCHES_PER_OCTAVE = 1200; + + public static final int CENTER_PANNING = 100; + + public static final String TEMPO_CHANGER_CUSTOM_INSTRUMENT_NAME = "Tempo Changer"; /** * Calculates the effective pitch of a note. (100 = 1 key, 1200 = 1 octave) @@ -33,7 +40,7 @@ public class NbsDefinitions { * @param note The NBS note * @return The effective pitch of the note */ - public static int getPitch(final NbsNote note) { + public static int getEffectivePitch(final NbsNote note) { final byte key = note.getKey(); final short pitch = note.getPitch(); return key * PITCHES_PER_KEY + pitch; @@ -45,19 +52,8 @@ public static int getPitch(final NbsNote note) { * @param note The NBS note * @return The effective key of the note */ - public static int getKey(final NbsNote note) { - return (int) ((float) getPitch(note) / PITCHES_PER_KEY); - } - - /** - * Applies the pitch value to the key and updates the note. This results in the pitch value of the note being set to the smallest possible value (-99 - 99). - * - * @param note The NBS note - */ - public static void applyPitchToKey(final NbsNote note) { - final int pitch = getPitch(note); - note.setKey((byte) getKey(note)); - note.setPitch((short) (pitch % PITCHES_PER_KEY)); + public static int getEffectiveKey(final NbsNote note) { + return (int) ((float) getEffectivePitch(note) / PITCHES_PER_KEY); } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsIo.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsIo.java new file mode 100644 index 0000000..3f15243 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsIo.java @@ -0,0 +1,296 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.nbs; + +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.nbs.model.NbsCustomInstrument; +import net.raphimc.noteblocklib.format.nbs.model.NbsLayer; +import net.raphimc.noteblocklib.format.nbs.model.NbsNote; +import net.raphimc.noteblocklib.format.nbs.model.NbsSong; +import net.raphimc.noteblocklib.model.Note; + +import java.io.*; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class NbsIo { + + private static final int BUFFER_SIZE = 1024 * 1024; + + public static NbsSong readSong(final InputStream is, final String fileName) throws IOException { + final LittleEndianDataInputStream dis = new LittleEndianDataInputStream(new BufferedInputStream(is, BUFFER_SIZE)); + final NbsSong song = new NbsSong(fileName); + + final short length = dis.readShort(); + if (length == 0) { + song.setVersion(dis.readByte()); + song.setVanillaInstrumentCount(dis.readUnsignedByte()); + if (song.getVersion() >= 3) { + song.setLength(dis.readShort()); + } else { + song.setLength((short) -1); + } + } else { + song.setLength(length); + song.setVersion((byte) 0); + song.setVanillaInstrumentCount(10); + } + + if (song.getVersion() < 0 || song.getVersion() > 5) { + throw new IllegalStateException("Unsupported NBS version: " + song.getVersion()); + } + + song.setLayerCount(dis.readShort()); + song.setTitle(readString(dis)); + song.setAuthor(readString(dis)); + song.setOriginalAuthor(readString(dis)); + song.setDescription(readString(dis)); + song.setTempo(dis.readShort()); + song.setAutoSave(dis.readBoolean()); + song.setAutoSaveInterval(dis.readByte()); + song.setTimeSignature(dis.readByte()); + song.setMinutesSpent(dis.readInt()); + song.setLeftClicks(dis.readInt()); + song.setRightClicks(dis.readInt()); + song.setNoteBlocksAdded(dis.readInt()); + song.setNoteBlocksRemoved(dis.readInt()); + song.setSourceFileName(readString(dis)); + + if (song.getVersion() >= 4) { + song.setLoop(dis.readBoolean()); + song.setMaxLoopCount(dis.readByte()); + song.setLoopStartTick(dis.readShort()); + } + + final Map layers = song.getLayers(); + final List customInstruments = song.getCustomInstruments(); + + int tick = -1; + while (true) { + final short jumpTicks = dis.readShort(); + if (jumpTicks == 0) break; + tick += jumpTicks; + + int layer = -1; + while (true) { + final short jumpLayers = dis.readShort(); + if (jumpLayers == 0) break; + layer += jumpLayers; + + final NbsNote note = new NbsNote(); + note.setInstrument((short) dis.readUnsignedByte()); + note.setKey(dis.readByte()); + if (song.getVersion() >= 4) { + note.setVelocity(dis.readByte()); + note.setPanning((short) dis.readUnsignedByte()); + note.setPitch(dis.readShort()); + } + layers.computeIfAbsent(layer, k -> new NbsLayer()).getNotes().put(tick, note); + } + } + + if (dis.available() > 0) { + for (int i = 0; i < song.getLayerCount(); i++) { + final NbsLayer layer = layers.computeIfAbsent(i, k -> new NbsLayer()); + layer.setName(readString(dis)); + if (song.getVersion() >= 4) { + layer.setLocked(dis.readBoolean()); + } + layer.setVolume(dis.readByte()); + if (song.getVersion() >= 2) { + layer.setPanning((short) dis.readUnsignedByte()); + } + } + } + + if (dis.available() > 0) { + final int customInstrumentsAmount = dis.readUnsignedByte(); + for (int i = 0; i < customInstrumentsAmount; i++) { + final NbsCustomInstrument customInstrument = new NbsCustomInstrument(); + customInstrument.setName(readString(dis)); + customInstrument.setSoundFilePath(readString(dis)); + customInstrument.setPitch(dis.readByte()); + customInstrument.setPressKey(dis.readBoolean()); + customInstruments.add(customInstrument); + } + } + + { // Fill generalized song structure with data + final Map customInstrumentMap = new IdentityHashMap<>(customInstruments.size()); // Cache map to avoid creating new instances for each note + for (NbsCustomInstrument customInstrument : customInstruments) { + customInstrumentMap.put(customInstrument, customInstrument.copy().setPitch((byte) NbsDefinitions.F_SHARP_4_NBS_KEY)); + } + + song.getTempoEvents().set(0, song.getTempo() / 100F); + for (NbsLayer layer : layers.values()) { + for (Map.Entry noteEntry : layer.getNotes().entrySet()) { + final NbsNote nbsNote = noteEntry.getValue(); + + final Note note = new Note(); + note.setNbsKey((float) NbsDefinitions.getEffectivePitch(nbsNote) / NbsDefinitions.PITCHES_PER_KEY); + note.setVolume((layer.getVolume() / 100F) * (nbsNote.getVelocity() / 100F)); + if (layer.getPanning() == NbsDefinitions.CENTER_PANNING) { // Special case + note.setPanning((nbsNote.getPanning() - NbsDefinitions.CENTER_PANNING) / 100F); + } else { + note.setPanning(((layer.getPanning() - NbsDefinitions.CENTER_PANNING) + (nbsNote.getPanning() - NbsDefinitions.CENTER_PANNING)) / 200F); + } + + if (nbsNote.getInstrument() < song.getVanillaInstrumentCount()) { + note.setInstrument(MinecraftInstrument.fromNbsId((byte) nbsNote.getInstrument())); + } else { + final NbsCustomInstrument nbsCustomInstrument = customInstruments.get(nbsNote.getInstrument() - song.getVanillaInstrumentCount()); + if (song.getVersion() >= 4 && NbsDefinitions.TEMPO_CHANGER_CUSTOM_INSTRUMENT_NAME.equals(nbsCustomInstrument.getName())) { + song.getTempoEvents().set(noteEntry.getKey(), Math.abs(nbsNote.getPitch() / 15F)); + continue; + } + + final int pitchModifier = nbsCustomInstrument.getPitch() - NbsDefinitions.F_SHARP_4_NBS_KEY; + if (pitchModifier != 0) { // Pre-apply pitch modifier to note to make it easier for player implementations + note.setNbsKey(note.getNbsKey() + pitchModifier); + note.setInstrument(customInstrumentMap.get(nbsCustomInstrument)); // Use custom instrument with no pitch modifier, because the pitch modifier is already applied to the note + } else { + note.setInstrument(nbsCustomInstrument); + } + } + + song.getNotes().add(noteEntry.getKey(), note); + } + } + } + + return song; + } + + public static void writeSong(final NbsSong song, final OutputStream os) throws IOException { + if (song.getVersion() < 0 || song.getVersion() > 5) { + throw new IllegalArgumentException("Unsupported NBS version: " + song.getVersion()); + } + if (song.getLayerCount() > song.getLayers().size()) { + throw new IllegalArgumentException("Layer count must be less than or equal to the amount of layers"); + } + + final LittleEndianDataOutputStream dos = new LittleEndianDataOutputStream(new BufferedOutputStream(os, BUFFER_SIZE)); + + if (song.getVersion() == 0) { + dos.writeShort(song.getLength()); + } else { + dos.writeShort(0); + dos.writeByte(song.getVersion()); + dos.writeByte(song.getVanillaInstrumentCount()); + if (song.getVersion() >= 3) { + dos.writeShort(song.getLength()); + } + } + + dos.writeShort(song.getLayerCount()); + writeString(dos, song.getTitleOr("")); + writeString(dos, song.getAuthorOr("")); + writeString(dos, song.getOriginalAuthorOr("")); + writeString(dos, song.getDescriptionOr("")); + dos.writeShort(song.getTempo()); + dos.writeBoolean(song.isAutoSave()); + dos.writeByte(song.getAutoSaveInterval()); + dos.writeByte(song.getTimeSignature()); + dos.writeInt(song.getMinutesSpent()); + dos.writeInt(song.getLeftClicks()); + dos.writeInt(song.getRightClicks()); + dos.writeInt(song.getNoteBlocksAdded()); + dos.writeInt(song.getNoteBlocksRemoved()); + writeString(dos, song.getSourceFileNameOr("")); + + if (song.getVersion() >= 4) { + dos.writeBoolean(song.isLoop()); + dos.writeByte(song.getMaxLoopCount()); + dos.writeShort(song.getLoopStartTick()); + } + + final Map> notes = new TreeMap<>(); + for (Map.Entry layerEntry : song.getLayers().entrySet()) { + for (Map.Entry noteEntry : layerEntry.getValue().getNotes().entrySet()) { + notes.computeIfAbsent(noteEntry.getKey(), k -> new TreeMap<>()).put(layerEntry.getKey(), noteEntry.getValue()); + } + } + + int lastTick = -1; + for (Map.Entry> tickEntry : notes.entrySet()) { + dos.writeShort(tickEntry.getKey() - lastTick); + lastTick = tickEntry.getKey(); + + int lastLayer = -1; + for (Map.Entry layerEntry : tickEntry.getValue().entrySet()) { + dos.writeShort(layerEntry.getKey() - lastLayer); + lastLayer = layerEntry.getKey(); + + final NbsNote note = layerEntry.getValue(); + dos.writeByte(note.getInstrument()); + dos.writeByte(note.getKey()); + if (song.getVersion() >= 4) { + dos.writeByte(note.getVelocity()); + dos.writeByte(note.getPanning()); + dos.writeShort(note.getPitch()); + } + } + dos.writeShort(0); + } + dos.writeShort(0); + + for (int i = 0; i < song.getLayerCount(); i++) { + final NbsLayer layer = song.getLayers().get(i); + writeString(dos, layer.getNameOr("")); + if (song.getVersion() >= 4) { + dos.writeBoolean(layer.isLocked()); + } + dos.writeByte(layer.getVolume()); + if (song.getVersion() >= 2) { + dos.writeByte(layer.getPanning()); + } + } + + dos.writeByte(song.getCustomInstruments().size()); + for (NbsCustomInstrument customInstrument : song.getCustomInstruments()) { + writeString(dos, customInstrument.getNameOr("")); + writeString(dos, customInstrument.getSoundFilePathOr("")); + dos.writeByte(customInstrument.getPitch()); + dos.writeBoolean(customInstrument.isPressKey()); + } + + dos.flush(); + } + + private static String readString(final LittleEndianDataInputStream dis) throws IOException { + int length = dis.readInt(); + final StringBuilder builder = new StringBuilder(length); + while (length > 0) { + builder.append((char) dis.readByte()); + length--; + } + return builder.toString(); + } + + private static void writeString(final LittleEndianDataOutputStream dos, final String string) throws IOException { + dos.writeInt(string.length()); + for (char c : string.toCharArray()) { + dos.writeByte(c); + } + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsParser.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsParser.java deleted file mode 100644 index a1b52d2..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsParser.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.nbs; - -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; -import net.raphimc.noteblocklib.format.nbs.model.NbsData; -import net.raphimc.noteblocklib.format.nbs.model.NbsHeader; -import net.raphimc.noteblocklib.util.Instrument; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -public class NbsParser { - - public static NbsSong read(final byte[] bytes, final String fileName) throws IOException { - final LittleEndianDataInputStream dis = new LittleEndianDataInputStream(new ByteArrayInputStream(bytes)); - - final NbsHeader header = new NbsHeader(dis); - final NbsData data = new NbsData(header, dis); - - return new NbsSong(fileName, header, data); - } - - public static byte[] write(final NbsSong song) throws IOException { - final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - final LittleEndianDataOutputStream dos = new LittleEndianDataOutputStream(bytes); - - song.getHeader().setVanillaInstrumentCount(Instrument.values().length); - song.getHeader().write(dos); - song.getData().write(song.getHeader(), dos); - - return bytes.toByteArray(); - } - - public static String readString(final LittleEndianDataInputStream dis) throws IOException { - int length = dis.readInt(); - final StringBuilder builder = new StringBuilder(length); - while (length > 0) { - builder.append((char) dis.readByte()); - length--; - } - return builder.toString(); - } - - public static void writeString(final LittleEndianDataOutputStream dos, final String string) throws IOException { - dos.writeInt(string.length()); - for (final char c : string.toCharArray()) { - dos.writeByte(c); - } - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsSong.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsSong.java deleted file mode 100644 index cb654b9..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/NbsSong.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.nbs; - -import net.raphimc.noteblocklib.format.SongFormat; -import net.raphimc.noteblocklib.format.nbs.model.NbsData; -import net.raphimc.noteblocklib.format.nbs.model.NbsHeader; -import net.raphimc.noteblocklib.format.nbs.model.NbsLayer; -import net.raphimc.noteblocklib.format.nbs.model.NbsNote; -import net.raphimc.noteblocklib.model.Song; -import net.raphimc.noteblocklib.model.SongView; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public class NbsSong extends Song { - - public NbsSong(final String fileName, final NbsHeader header, final NbsData data) { - super(SongFormat.NBS, fileName, header, data); - } - - @Override - protected SongView createView() { - final String title = this.getHeader().getTitle().isEmpty() ? this.fileName == null ? "NBS Song" : this.fileName : this.getHeader().getTitle(); - - final Map> notes = new TreeMap<>(); - for (NbsLayer layer : this.getData().getLayers()) { - for (Map.Entry note : layer.getNotesAtTick().entrySet()) { - notes.computeIfAbsent(note.getKey(), k -> new ArrayList<>()).add(note.getValue()); - } - } - - return new SongView<>(title, this.getHeader().getSpeed() / 100F, notes); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsCustomInstrument.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsCustomInstrument.java index 0a12ece..f90cb16 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsCustomInstrument.java +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsCustomInstrument.java @@ -17,16 +17,12 @@ */ package net.raphimc.noteblocklib.format.nbs.model; -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; +import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; +import net.raphimc.noteblocklib.model.instrument.Instrument; -import java.io.IOException; import java.util.Objects; -import static net.raphimc.noteblocklib.format.nbs.NbsParser.readString; -import static net.raphimc.noteblocklib.format.nbs.NbsParser.writeString; - -public class NbsCustomInstrument { +public class NbsCustomInstrument implements Instrument { /** * @since v0 @@ -36,7 +32,7 @@ public class NbsCustomInstrument { /** * @since v0 */ - private String soundFileName; + private String soundFilePath; /** * @since v0 @@ -48,25 +44,8 @@ public class NbsCustomInstrument { */ private boolean pressKey; - public NbsCustomInstrument(final LittleEndianDataInputStream dis) throws IOException { - this.name = readString(dis); - this.soundFileName = readString(dis); - this.pitch = dis.readByte(); - this.pressKey = dis.readBoolean(); - } - - public NbsCustomInstrument(final String name, final String soundFileName, final byte pitch, final boolean pressKey) { - this.name = name; - this.soundFileName = soundFileName; - this.pitch = pitch; - this.pressKey = pressKey; - } - - public void write(final LittleEndianDataOutputStream dos) throws IOException { - writeString(dos, this.name); - writeString(dos, this.soundFileName); - dos.writeByte(this.pitch); - dos.writeBoolean(this.pressKey); + public NbsCustomInstrument() { + this.pitch = NbsDefinitions.F_SHARP_4_NBS_KEY; } /** @@ -77,28 +56,58 @@ public String getName() { return this.name; } + /** + * @return The name of the instrument. + * @param fallback The fallback value if the name is not set. + * @since v0 + */ + public String getNameOr(final String fallback) { + return this.name == null ? fallback : this.name; + } + /** * @param name The name of the instrument. + * @return this + * @since v0 + */ + public NbsCustomInstrument setName(final String name) { + if (name != null && !name.isEmpty()) { + this.name = name; + } else { + this.name = null; + } + return this; + } + + /** + * @return The sound file of the instrument. * @since v0 */ - public void setName(final String name) { - this.name = name; + public String getSoundFilePath() { + return this.soundFilePath; } /** - * @return The sound file of the instrument (just the file name, not the path). + * @return The sound file of the instrument. + * @param fallback The fallback value if the sound file path is not set. * @since v0 */ - public String getSoundFileName() { - return this.soundFileName; + public String getSoundFilePathOr(final String fallback) { + return this.soundFilePath == null ? fallback : this.soundFilePath; } /** - * @param soundFileName The sound file of the instrument (just the file name, not the path). + * @param soundFilePath The sound file of the instrument. + * @return this * @since v0 */ - public void setSoundFileName(final String soundFileName) { - this.soundFileName = soundFileName; + public NbsCustomInstrument setSoundFilePath(final String soundFilePath) { + if (soundFilePath != null && !soundFilePath.isEmpty()) { + this.soundFilePath = soundFilePath; + } else { + this.soundFilePath = null; + } + return this; } /** @@ -111,10 +120,12 @@ public byte getPitch() { /** * @param pitch The pitch of the sound file. Just like the note blocks, this ranges from 0-87. Default is 45 (F#4). + * @return this * @since v0 */ - public void setPitch(final byte pitch) { + public NbsCustomInstrument setPitch(final byte pitch) { this.pitch = pitch; + return this; } /** @@ -127,23 +138,33 @@ public boolean isPressKey() { /** * @param pressKey Whether the piano should automatically press keys with this instrument when the marker passes them. + * @return this * @since v0 */ - public void setPressKey(final boolean pressKey) { + public NbsCustomInstrument setPressKey(final boolean pressKey) { this.pressKey = pressKey; + return this; + } + + public NbsCustomInstrument copy() { + final NbsCustomInstrument copyCustomInstrument = new NbsCustomInstrument(); + copyCustomInstrument.setName(this.name); + copyCustomInstrument.setSoundFilePath(this.soundFilePath); + copyCustomInstrument.setPitch(this.pitch); + copyCustomInstrument.setPressKey(this.pressKey); + return copyCustomInstrument; } @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NbsCustomInstrument that = (NbsCustomInstrument) o; - return pitch == that.pitch && pressKey == that.pressKey && Objects.equals(name, that.name) && Objects.equals(soundFileName, that.soundFileName); + return pitch == that.pitch && pressKey == that.pressKey && Objects.equals(name, that.name) && Objects.equals(soundFilePath, that.soundFilePath); } @Override public int hashCode() { - return Objects.hash(name, soundFileName, pitch, pressKey); + return Objects.hash(name, soundFilePath, pitch, pressKey); } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsData.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsData.java deleted file mode 100644 index 6d9a720..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsData.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.nbs.model; - -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; -import net.raphimc.noteblocklib.model.*; -import net.raphimc.noteblocklib.util.SongUtil; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public class NbsData implements Data { - - /** - * @since v0 - */ - private List layers; - - /** - * @since v0 - */ - private List customInstruments; - - public NbsData(final NbsHeader header, final LittleEndianDataInputStream dis) throws IOException { - this.layers = new ArrayList<>(header.getLayerCount()); - this.customInstruments = new ArrayList<>(); - - for (int i = 0; i < header.getLayerCount(); i++) { - layers.add(new NbsLayer()); - } - - int tick = -1; - while (true) { - final short jumpTicks = dis.readShort(); - if (jumpTicks == 0) break; - tick += jumpTicks; - - int layer = -1; - while (true) { - final short jumpLayers = dis.readShort(); - if (jumpLayers == 0) break; - layer += jumpLayers; - while (this.layers.size() <= layer) { - this.layers.add(new NbsLayer()); - } - - this.layers.get(layer).getNotesAtTick().put(tick, new NbsNote(header, this.layers.get(layer), dis)); - } - } - - if (dis.available() > 0) { - for (int i = 0; i < header.getLayerCount(); i++) { - final NbsLayer layer = new NbsLayer(header, dis); - layer.setNotesAtTick(this.layers.get(i).getNotesAtTick()); - this.layers.set(i, layer); - - for (NbsNote note : layer.getNotesAtTick().values()) { - note.setLayer(layer); - } - } - } - - if (dis.available() > 0) { - final int customInstrumentsAmount = dis.readUnsignedByte(); - for (int i = 0; i < customInstrumentsAmount; i++) { - this.customInstruments.add(new NbsCustomInstrument(dis)); - } - - for (NbsLayer layer : this.layers) { - for (NbsNote note : layer.getNotesAtTick().values()) { - note.resolveCustomInstrument(header, this); - } - } - } - } - - public NbsData(final List layers, final List customInstruments) { - this.layers = layers; - this.customInstruments = customInstruments; - } - - public NbsData(final SongView songView) { - this.layers = new ArrayList<>(); - this.customInstruments = new ArrayList<>(); - - for (Map.Entry> entry : songView.getNotes().entrySet()) { - for (int i = 0; i < entry.getValue().size(); i++) { - final N note = entry.getValue().get(i); - final NbsLayer layer; - if (this.layers.size() <= i) { - this.layers.add(layer = new NbsLayer()); - } else { - layer = this.layers.get(i); - } - if (note instanceof NbsNote) { - final NbsNote clonedNote = (NbsNote) note.clone(); - clonedNote.setLayer(layer); - layer.getNotesAtTick().put(entry.getKey(), clonedNote); - } else { - final NbsNote nbsNote = new NbsNote(layer, note.getInstrument(), note.getKey()); - if (note instanceof NoteWithVolume) { - final NoteWithVolume noteWithVolume = (NoteWithVolume) note; - nbsNote.setVolume(noteWithVolume.getVolume()); - } - if (note instanceof NoteWithPanning) { - final NoteWithPanning noteWithPanning = (NoteWithPanning) note; - nbsNote.setPanning(noteWithPanning.getPanning()); - } - - layer.getNotesAtTick().put(entry.getKey(), nbsNote); - } - } - } - - this.customInstruments.addAll(SongUtil.getUsedCustomInstruments(songView)); - } - - public void write(final NbsHeader header, final LittleEndianDataOutputStream dos) throws IOException { - final Map> notes = new TreeMap<>(); - for (NbsLayer layer : this.layers) { - for (Map.Entry note : layer.getNotesAtTick().entrySet()) { - notes.computeIfAbsent(note.getKey(), k -> new ArrayList<>()).add(note.getValue()); - } - } - - int lastTick = -1; - for (Map.Entry> entry : notes.entrySet()) { - dos.writeShort(entry.getKey() - lastTick); - lastTick = entry.getKey(); - - int lastLayer = -1; - for (NbsNote note : entry.getValue()) { - if (!this.layers.contains(note.getLayer())) { - throw new IllegalArgumentException("Note layer not found in NbsData layers list"); - } - dos.writeShort(this.layers.indexOf(note.getLayer()) - lastLayer); - lastLayer = this.layers.indexOf(note.getLayer()); - note.write(header, this, dos); - } - dos.writeShort(0); - } - dos.writeShort(0); - - for (int i = 0; i < header.getLayerCount(); i++) { - this.layers.get(i).write(header, dos); - } - - dos.writeByte(this.customInstruments.size()); - for (NbsCustomInstrument customInstrument : this.customInstruments) { - customInstrument.write(dos); - } - } - - /** - * @return The layers of this song - * @since v0 - */ - public List getLayers() { - return this.layers; - } - - /** - * @param layers The layers of this song - * @since v0 - */ - public void setLayers(final List layers) { - this.layers = layers; - } - - /** - * @return The custom instruments of this song - * @since v0 - */ - public List getCustomInstruments() { - return this.customInstruments; - } - - /** - * @param customInstruments The custom instruments of this song - * @since v0 - */ - public void setCustomInstruments(final List customInstruments) { - this.customInstruments = customInstruments; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsHeader.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsHeader.java deleted file mode 100644 index dc6d32e..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsHeader.java +++ /dev/null @@ -1,617 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.nbs.model; - -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; -import net.raphimc.noteblocklib.model.Header; -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.model.SongView; -import net.raphimc.noteblocklib.util.Instrument; - -import java.io.IOException; -import java.util.List; - -import static net.raphimc.noteblocklib.format.nbs.NbsParser.readString; -import static net.raphimc.noteblocklib.format.nbs.NbsParser.writeString; - -public class NbsHeader implements Header { - - /** - * @since v0 and {@literal >}= v3 - */ - private short length; - - /** - * @since v0 - */ - private byte version; - - /** - * @since v0 - */ - private int vanillaInstrumentCount; - - /** - * @since v0 - */ - private short layerCount; - - /** - * @since v0 - */ - private String title = ""; - - /** - * @since v0 - */ - private String author = ""; - - /** - * @since v0 - */ - private String originalAuthor = ""; - - /** - * @since v0 - */ - private String description = ""; - - /** - * @since v0 - */ - private short speed; - - /** - * @since v0 - */ - private boolean autoSave; - - /** - * @since v0 - */ - private byte autoSaveInterval; - - /** - * @since v0 - */ - private byte timeSignature; - - /** - * @since v0 - */ - private int minutesSpent; - - /** - * @since v0 - */ - private int leftClicks; - - /** - * @since v0 - */ - private int rightClicks; - - /** - * @since v0 - */ - private int noteBlocksAdded; - - /** - * @since v0 - */ - private int noteBlocksRemoved; - - /** - * @since v0 - */ - private String sourceFileName = ""; - - /** - * @since v4 - */ - private boolean loop; - - /** - * @since v4 - */ - private byte maxLoopCount; - - /** - * @since v4 - */ - private short loopStartTick; - - public NbsHeader(final LittleEndianDataInputStream dis) throws IOException { - final short length = dis.readShort(); - if (length == 0) { - this.version = dis.readByte(); - this.vanillaInstrumentCount = dis.readUnsignedByte(); - if (this.version >= 3) { - this.length = dis.readShort(); - } else { - this.length = -1; - } - } else { - this.length = length; - this.version = 0; - this.vanillaInstrumentCount = 10; - } - - if (this.version < 0 || this.version > 5) { - throw new IllegalStateException("Unsupported NBS version: " + this.version); - } - - this.layerCount = dis.readShort(); - this.title = readString(dis); - this.author = readString(dis); - this.originalAuthor = readString(dis); - this.description = readString(dis); - this.speed = dis.readShort(); - this.autoSave = dis.readBoolean(); - this.autoSaveInterval = dis.readByte(); - this.timeSignature = dis.readByte(); - this.minutesSpent = dis.readInt(); - this.leftClicks = dis.readInt(); - this.rightClicks = dis.readInt(); - this.noteBlocksAdded = dis.readInt(); - this.noteBlocksRemoved = dis.readInt(); - this.sourceFileName = readString(dis); - - if (this.version >= 4) { - this.loop = dis.readBoolean(); - this.maxLoopCount = dis.readByte(); - this.loopStartTick = dis.readShort(); - } - } - - public NbsHeader(final short length, final byte version, final int vanillaInstrumentCount, final short layerCount, final String title, final String author, final String originalAuthor, final String description, final short speed, final boolean autoSave, final byte autoSaveInterval, final byte timeSignature, final int minutesSpent, final int leftClicks, final int rightClicks, final int noteBlocksAdded, final int noteBlocksRemoved, final String sourceFileName, final boolean loop, final byte maxLoopCount, final short loopStartTick) { - this(length, version, vanillaInstrumentCount, layerCount, title, author, originalAuthor, description, speed, autoSave, autoSaveInterval, timeSignature, minutesSpent, leftClicks, rightClicks, noteBlocksAdded, noteBlocksRemoved, sourceFileName); - - this.loop = loop; - this.maxLoopCount = maxLoopCount; - this.loopStartTick = loopStartTick; - } - - public NbsHeader(final short length, final byte version, final int vanillaInstrumentCount, final short layerCount, final String title, final String author, final String originalAuthor, final String description, final short speed, final boolean autoSave, final byte autoSaveInterval, final byte timeSignature, final int minutesSpent, final int leftClicks, final int rightClicks, final int noteBlocksAdded, final int noteBlocksRemoved, final String sourceFileName) { - this.length = length; - this.version = version; - this.vanillaInstrumentCount = vanillaInstrumentCount; - this.layerCount = layerCount; - this.title = title; - this.author = author; - this.originalAuthor = originalAuthor; - this.description = description; - this.speed = speed; - this.autoSave = autoSave; - this.autoSaveInterval = autoSaveInterval; - this.timeSignature = timeSignature; - this.minutesSpent = minutesSpent; - this.leftClicks = leftClicks; - this.rightClicks = rightClicks; - this.noteBlocksAdded = noteBlocksAdded; - this.noteBlocksRemoved = noteBlocksRemoved; - this.sourceFileName = sourceFileName; - } - - public NbsHeader() { - } - - public NbsHeader(final SongView songView) { - this.version = 4; - this.vanillaInstrumentCount = Instrument.values().length; - this.title = songView.getTitle(); - this.length = (short) songView.getLength(); - this.speed = (short) (songView.getSpeed() * 100F); - this.author = "NoteBlockLib"; - this.description = "Created with NoteBlockLib"; - this.layerCount = (short) songView.getNotes().values().stream().mapToInt(List::size).max().orElse(0); - } - - public void write(final LittleEndianDataOutputStream dos) throws IOException { - if (this.version == 0) { - dos.writeShort(this.length); - } else { - dos.writeShort(0); - dos.writeByte(this.version); - dos.writeByte(this.vanillaInstrumentCount); - if (this.version >= 3) { - dos.writeShort(this.length); - } - } - - dos.writeShort(this.layerCount); - writeString(dos, this.title); - writeString(dos, this.author); - writeString(dos, this.originalAuthor); - writeString(dos, this.description); - dos.writeShort(this.speed); - dos.writeBoolean(this.autoSave); - dos.writeByte(this.autoSaveInterval); - dos.writeByte(this.timeSignature); - dos.writeInt(this.minutesSpent); - dos.writeInt(this.leftClicks); - dos.writeInt(this.rightClicks); - dos.writeInt(this.noteBlocksAdded); - dos.writeInt(this.noteBlocksRemoved); - writeString(dos, this.sourceFileName); - - if (this.version >= 4) { - dos.writeBoolean(this.loop); - dos.writeByte(this.maxLoopCount); - dos.writeShort(this.loopStartTick); - } - } - - /** - * Can be -1 if the nbsVersion did not support this field - * - * @return The length of the song, measured in ticks. Divide this by the tempo to get the length of the song in seconds. - * @since v0 and {@literal >}= v3 - */ - public short getLength() { - return this.length; - } - - /** - * @param length The length of the song, measured in ticks. Divide this by the tempo to get the length of the song in seconds. - * @since v0 and {@literal >}= v3 - */ - public void setLength(final short length) { - this.length = length; - } - - /** - * @return The version of the NBS format. - * @since v0 - */ - public byte getVersion() { - return this.version; - } - - /** - * @param version The version of the NBS format. - * @since v0 - */ - public void setVersion(final byte version) { - this.version = version; - } - - /** - * @return The version of the NBS format. - * @since v0 - */ - @Deprecated - public byte getNbsVersion() { - return this.version; - } - - /** - * @param version The version of the NBS format. - * @since v0 - */ - @Deprecated - public void setNbsVersion(final byte version) { - this.version = version; - } - - /** - * @return Amount of default instruments when the song was saved. This is needed to determine at what index custom instruments start. - * @since v0 - */ - public int getVanillaInstrumentCount() { - return this.vanillaInstrumentCount; - } - - /** - * @param vanillaInstrumentCount Amount of default instruments when the song was saved. This is needed to determine at what index custom instruments start. - * @since v0 - */ - public void setVanillaInstrumentCount(final int vanillaInstrumentCount) { - this.vanillaInstrumentCount = vanillaInstrumentCount; - } - - /** - * @return The last layer with at least one note block in it, or the last layer that has had its name, volume or stereo changed. - * @since v0 - */ - public short getLayerCount() { - return this.layerCount; - } - - /** - * @param layerCount The last layer with at least one note block in it, or the last layer that has had its name, volume or stereo changed. - * @since v0 - */ - public void setLayerCount(final short layerCount) { - this.layerCount = layerCount; - } - - /** - * @return The name of the song. - * @since v0 - */ - public String getTitle() { - return this.title; - } - - /** - * @param title The name of the song. - * @since v0 - */ - public void setTitle(final String title) { - this.title = title; - } - - /** - * @return The author of the song. - * @since v0 - */ - public String getAuthor() { - return this.author; - } - - /** - * @param author The author of the song. - * @since v0 - */ - public void setAuthor(final String author) { - this.author = author; - } - - /** - * @return The original author of the song. - * @since v0 - */ - public String getOriginalAuthor() { - return this.originalAuthor; - } - - /** - * @param originalAuthor The original author of the song. - * @since v0 - */ - public void setOriginalAuthor(final String originalAuthor) { - this.originalAuthor = originalAuthor; - } - - /** - * @return The description of the song. - * @since v0 - */ - public String getDescription() { - return this.description; - } - - /** - * @param description The description of the song. - * @since v0 - */ - public void setDescription(final String description) { - this.description = description; - } - - /** - * @return The tempo of the song multiplied by 100 (for example, 1225 instead of 12.25). Measured in ticks per second. - * @since v0 - */ - public short getSpeed() { - return this.speed; - } - - /** - * @param speed The tempo of the song multiplied by 100 (for example, 1225 instead of 12.25). Measured in ticks per second. - * @since v0 - */ - public void setSpeed(final short speed) { - this.speed = speed; - } - - /** - * @return Whether auto-saving has been enabled (0 or 1). As of NBS version 4 this value is still saved to the file, but no longer used in the program. - * @since v0 - */ - public boolean isAutoSave() { - return this.autoSave; - } - - /** - * @param autoSave Whether auto-saving has been enabled (0 or 1). As of NBS version 4 this value is still saved to the file, but no longer used in the program. - * @since v0 - */ - public void setAutoSave(final boolean autoSave) { - this.autoSave = autoSave; - } - - /** - * @return The amount of minutes between each auto-save (if it has been enabled) (1-60). As of NBS version 4 this value is still saved to the file, but no longer used in the program. - * @since v0 - */ - public byte getAutoSaveInterval() { - return this.autoSaveInterval; - } - - /** - * @param autoSaveInterval The amount of minutes between each auto-save (if it has been enabled) (1-60). As of NBS version 4 this value is still saved to the file, but no longer used in the program. - * @since v0 - */ - public void setAutoSaveInterval(final byte autoSaveInterval) { - this.autoSaveInterval = autoSaveInterval; - } - - /** - * @return The time signature of the song. If this is 3, then the signature is 3/4. Default is 4. This value ranges from 2-8. - * @since v0 - */ - public byte getTimeSignature() { - return this.timeSignature; - } - - /** - * @param timeSignature The time signature of the song. If this is 3, then the signature is 3/4. Default is 4. This value ranges from 2-8. - * @since v0 - */ - public void setTimeSignature(final byte timeSignature) { - this.timeSignature = timeSignature; - } - - /** - * @return Amount of minutes spent on the project. - * @since v0 - */ - public int getMinutesSpent() { - return this.minutesSpent; - } - - /** - * @param minutesSpent Amount of minutes spent on the project. - * @since v0 - */ - public void setMinutesSpent(final int minutesSpent) { - this.minutesSpent = minutesSpent; - } - - /** - * @return Amount of times the user has left-clicked. - * @since v0 - */ - public int getLeftClicks() { - return this.leftClicks; - } - - /** - * @param leftClicks Amount of times the user has left-clicked. - * @since v0 - */ - public void setLeftClicks(final int leftClicks) { - this.leftClicks = leftClicks; - } - - /** - * @return Amount of times the user has right-clicked. - * @since v0 - */ - public int getRightClicks() { - return this.rightClicks; - } - - /** - * @param rightClicks Amount of times the user has right-clicked. - * @since v0 - */ - public void setRightClicks(final int rightClicks) { - this.rightClicks = rightClicks; - } - - /** - * @return Amount of times the user has added a note block. - * @since v0 - */ - public int getNoteBlocksAdded() { - return this.noteBlocksAdded; - } - - /** - * @param noteBlocksAdded Amount of times the user has added a note block. - * @since v0 - */ - public void setNoteBlocksAdded(final int noteBlocksAdded) { - this.noteBlocksAdded = noteBlocksAdded; - } - - /** - * @return Amount of times the user has removed a note block. - * @since v0 - */ - public int getNoteBlocksRemoved() { - return this.noteBlocksRemoved; - } - - /** - * @param noteBlocksRemoved Amount of times the user has removed a note block. - * @since v0 - */ - public void setNoteBlocksRemoved(final int noteBlocksRemoved) { - this.noteBlocksRemoved = noteBlocksRemoved; - } - - /** - * @return If the song has been imported from a .mid or .schematic file, that file name is stored here (only the name of the file, not the path). - * @since v0 - */ - public String getSourceFileName() { - return this.sourceFileName; - } - - /** - * @param sourceFileName If the song has been imported from a .mid or .schematic file, that file name is stored here (only the name of the file, not the path). - * @since v0 - */ - public void setSourceFileName(final String sourceFileName) { - this.sourceFileName = sourceFileName; - } - - /** - * @return Whether looping is on or off. - * @since v4 - */ - public boolean isLoop() { - return this.loop; - } - - /** - * @param loop Whether looping is on or off. - * @since v4 - */ - public void setLoop(final boolean loop) { - this.loop = loop; - } - - /** - * @return 0 = infinite. Other values mean the amount of times the song loops. - * @since v4 - */ - public byte getMaxLoopCount() { - return this.maxLoopCount; - } - - /** - * @param maxLoopCount 0 = infinite. Other values mean the amount of times the song loops. - * @since v4 - */ - public void setMaxLoopCount(final byte maxLoopCount) { - this.maxLoopCount = maxLoopCount; - } - - /** - * @return Determines which part of the song (in ticks) it loops back to. - * @since v4 - */ - public short getLoopStartTick() { - return this.loopStartTick; - } - - /** - * @param loopStartTick Determines which part of the song (in ticks) it loops back to. - * @since v4 - */ - public void setLoopStartTick(final short loopStartTick) { - this.loopStartTick = loopStartTick; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsLayer.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsLayer.java index 5499501..0d37533 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsLayer.java +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsLayer.java @@ -17,106 +17,80 @@ */ package net.raphimc.noteblocklib.format.nbs.model; -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; +import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; -import java.io.IOException; +import java.util.HashMap; import java.util.Map; -import java.util.TreeMap; - -import static net.raphimc.noteblocklib.format.nbs.NbsParser.readString; -import static net.raphimc.noteblocklib.format.nbs.NbsParser.writeString; public class NbsLayer { /** * @since v0 */ - private Map notesAtTick = new TreeMap<>(); + private final Map notes = new HashMap<>(); /** * @since v0 */ - private String name = ""; + private String name; /** * @since v0 */ - private byte volume = 100; + private byte volume; /** * @since v2 */ - private short panning = 100; + private short panning; /** * @since v4 */ - private boolean locked = false; - - public NbsLayer(final NbsHeader header, final LittleEndianDataInputStream dis) throws IOException { - this.name = readString(dis); - if (header.getVersion() >= 4) { - this.locked = dis.readBoolean(); - } - this.volume = dis.readByte(); - if (header.getVersion() >= 2) { - this.panning = (short) dis.readUnsignedByte(); - } - } - - public NbsLayer(final Map notesAtTick, final String name, final byte volume, final short panning, final boolean locked) { - this.notesAtTick = notesAtTick; - this.name = name; - this.volume = volume; - this.panning = panning; - this.locked = locked; - } + private boolean locked; public NbsLayer() { - } - - public void write(final NbsHeader header, final LittleEndianDataOutputStream dos) throws IOException { - writeString(dos, this.name); - if (header.getVersion() >= 4) { - dos.writeBoolean(this.locked); - } - dos.writeByte(this.volume); - if (header.getVersion() >= 2) { - dos.writeByte(this.panning); - } + this.volume = 100; + this.panning = NbsDefinitions.CENTER_PANNING; } /** * @return A map of all notes in this layer, with the tick as the key. * @since v0 */ - public Map getNotesAtTick() { - return this.notesAtTick; + public Map getNotes() { + return this.notes; } /** - * @param notesAtTick A map of all notes in this layer, with the tick as the key. + * @return The name of the layer. * @since v0 */ - public void setNotesAtTick(final Map notesAtTick) { - this.notesAtTick = notesAtTick; + public String getName() { + return this.name; } /** * @return The name of the layer. + * @param fallback The fallback value if the layer name is not set. * @since v0 */ - public String getName() { - return this.name; + public String getNameOr(final String fallback) { + return this.name == null ? fallback : this.name; } /** * @param name The name of the layer. + * @return this * @since v0 */ - public void setName(final String name) { - this.name = name; + public NbsLayer setName(final String name) { + if (name != null && !name.isEmpty()) { + this.name = name; + } else { + this.name = null; + } + return this; } /** @@ -129,10 +103,12 @@ public byte getVolume() { /** * @param volume The volume of the layer (percentage). Ranges from 0-100. + * @return this * @since v0 */ - public void setVolume(final byte volume) { + public NbsLayer setVolume(final byte volume) { this.volume = volume; + return this; } /** @@ -145,10 +121,12 @@ public short getPanning() { /** * @param panning How much this layer should be panned to the left/right. 0 is 2 blocks right, 100 is center, 200 is 2 blocks left. + * @return this * @since v2 */ - public void setPanning(final short panning) { + public NbsLayer setPanning(final short panning) { this.panning = panning; + return this; } /** @@ -161,10 +139,26 @@ public boolean isLocked() { /** * @param locked Whether this layer should be marked as locked. + * @return this * @since v4 */ - public void setLocked(final boolean locked) { + public NbsLayer setLocked(final boolean locked) { this.locked = locked; + return this; + } + + public NbsLayer copy() { + final NbsLayer copyLayer = new NbsLayer(); + copyLayer.setName(this.name); + copyLayer.setVolume(this.volume); + copyLayer.setPanning(this.panning); + copyLayer.setLocked(this.locked); + final Map notes = this.getNotes(); + final Map copyNotes = copyLayer.getNotes(); + for (final Map.Entry entry : notes.entrySet()) { + copyNotes.put(entry.getKey(), entry.getValue().copy()); + } + return copyLayer; } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsNote.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsNote.java index 0876d83..5cf68bb 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsNote.java +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsNote.java @@ -17,206 +17,112 @@ */ package net.raphimc.noteblocklib.format.nbs.model; -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.model.NoteWithPanning; -import net.raphimc.noteblocklib.model.NoteWithVolume; -import net.raphimc.noteblocklib.util.Instrument; +import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; -import java.io.IOException; import java.util.Objects; -public class NbsNote extends Note implements NoteWithVolume, NoteWithPanning { +public class NbsNote { /** * @since v0 */ - private NbsLayer layer; + private short instrument; /** * @since v0 */ - private NbsCustomInstrument customInstrument; + private byte key; /** * @since v4 */ - private byte velocity = 100; + private byte velocity; /** * @since v4 */ - private short panning = 100; + private short panning; /** * @since v4 */ - private short pitch = 0; + private short pitch; - // For internal use - private int rawInstrumentId; - - public NbsNote(final NbsHeader header, final NbsLayer layer, final LittleEndianDataInputStream dis) throws IOException { - super(null, (byte) 0); - - this.rawInstrumentId = dis.readUnsignedByte(); - if (this.rawInstrumentId < header.getVanillaInstrumentCount()) { - this.instrument = Instrument.fromNbsId((byte) this.rawInstrumentId); - } - this.key = dis.readByte(); - - if (header.getVersion() >= 4) { - this.velocity = dis.readByte(); - this.panning = (short) dis.readUnsignedByte(); - this.pitch = dis.readShort(); - } - - this.layer = layer; - } - - public NbsNote(final NbsLayer layer, final Instrument instrument, final byte key, final byte velocity, final short panning, final short pitch, final NbsCustomInstrument customInstrument) { - this(layer, instrument, key, velocity, panning, pitch); - if (instrument != null && customInstrument != null) { - throw new IllegalArgumentException("Cannot set both instrument and custom instrument"); - } - - this.customInstrument = customInstrument; - } - - public NbsNote(final NbsLayer layer, final Instrument instrument, final byte key, final byte velocity, final short panning, final short pitch) { - this(layer, instrument, key); - - this.velocity = velocity; - this.panning = panning; - this.pitch = pitch; - } - - public NbsNote(final NbsLayer layer, final Instrument instrument, final byte key) { - super(instrument, key); - - this.layer = layer; - } - - public NbsNote(final Instrument instrument, final byte key) { - super(instrument, key); - } - - public void write(final NbsHeader header, final NbsData data, final LittleEndianDataOutputStream dos) throws IOException { - if (this.customInstrument != null) { - if (!data.getCustomInstruments().contains(this.customInstrument)) { - throw new IllegalArgumentException("Custom instrument not found in NBS data custom instruments list"); - } - dos.writeByte((byte) (header.getVanillaInstrumentCount() + data.getCustomInstruments().indexOf(this.customInstrument))); - } else { - dos.writeByte(this.instrument.nbsId()); - } - dos.writeByte(this.key); - - if (header.getVersion() >= 4) { - dos.writeByte(this.velocity); - dos.writeByte(this.panning); - dos.writeShort(this.pitch); - } - } - - // For internal use - void resolveCustomInstrument(final NbsHeader header, final NbsData data) { - if (this.rawInstrumentId >= header.getVanillaInstrumentCount()) { - this.customInstrument = data.getCustomInstruments().get(this.rawInstrumentId - header.getVanillaInstrumentCount()); - } - } - - @Override - public void setInstrument(final Instrument instrument) { - super.setInstrument(instrument); - this.customInstrument = null; + public NbsNote() { + this.velocity = 100; + this.panning = NbsDefinitions.CENTER_PANNING; } /** - * This value is excluded from equals and hashcode. - * - * @return The NBS layer this note is in. + * @return The instrument of the note block. This is 0-15, or higher if the song uses custom instruments. * @since v0 */ - public NbsLayer getLayer() { - return this.layer; + public short getInstrument() { + return this.instrument; } /** - * This value is excluded from equals and hashcode. - * - * @param layer The NBS layer this note is in. The layer has to be added to the layer list of the {@link NbsData} class. + * @param instrument The instrument of the note block. This is 0-15, or higher if the song uses custom instruments. + * @return this * @since v0 */ - public void setLayer(final NbsLayer layer) { - this.layer = layer; + public NbsNote setInstrument(final short instrument) { + this.instrument = instrument; + return this; } /** - * @return The custom instrument of the note if set or else null. + * @return The key of the note block, from 0-87, where 0 is A0 and 87 is C8. 33-57 is within the 2-octave limit. * @since v0 */ - public NbsCustomInstrument getCustomInstrument() { - return this.customInstrument; + public byte getKey() { + return this.key; } /** - * @param customInstrument The custom instrument of the note. If null, the note will use the {@link #instrument} value instead. - * The custom instrument has to be added to the custom instrument list of the {@link NbsData} class. + * @param key The key of the note block, from 0-87, where 0 is A0 and 87 is C8. 33-57 is within the 2-octave limit. + * @return this * @since v0 */ - public void setCustomInstrument(final NbsCustomInstrument customInstrument) { - this.customInstrument = customInstrument; - this.instrument = null; + public NbsNote setKey(final byte key) { + this.key = key; + return this; } /** - * @return The velocity/volume of the note, from 0% to 100%. Factors in the layer's volume. + * @return The velocity/volume of the note block, from 0% to 100%. * @since v4 */ - @Override - public float getVolume() { - final float layerVolume = this.layer != null ? this.layer.getVolume() : 100F; - final float noteVolume = this.velocity; - return (layerVolume * noteVolume) / 100F; + public byte getVelocity() { + return this.velocity; } /** - * @param volume The velocity/volume of the note, from 0% to 100%. Does not change the layer's volume. + * @param velocity The velocity/volume of the note block, from 0% to 100%. + * @return this * @since v4 */ - @Override - public void setVolume(final float volume) { - this.velocity = (byte) volume; - } - - public byte getRawVelocity() { - return this.velocity; + public NbsNote setVelocity(final byte velocity) { + this.velocity = velocity; + return this; } /** - * @return The stereo position of the note block. (-100 is 2 blocks right, 0 is center, 100 is 2 blocks left). Factors in the layer's panning. + * @return The stereo position of the note block, from 0-200. 0 is 2 blocks right, 100 is center, 200 is 2 blocks left. * @since v4 */ - @Override - public float getPanning() { - final float layerPanning = this.layer != null ? (this.layer.getPanning() - 100) : 0F; - final float notePanning = this.panning - 100; - return (layerPanning + notePanning) / 2F; + public short getPanning() { + return this.panning; } /** - * @param panning The stereo position of the note block. (-100 is 2 blocks right, 0 is center, 100 is 2 blocks left). Does not change the layer's panning. + * @param panning The stereo position of the note block, from 0-200. 0 is 2 blocks right, 100 is center, 200 is 2 blocks left. + * @return this * @since v4 */ - @Override - public void setPanning(final float panning) { - this.panning = (short) (panning + 100); - } - - public short getRawPanning() { - return this.panning; + public NbsNote setPanning(final short panning) { + this.panning = panning; + return this; } /** @@ -231,30 +137,38 @@ public short getPitch() { } /** + * 100 = 1 key
+ * 1200 = 1 octave + * * @param pitch The fine pitch of the note block, from -32,768 to 32,767 cents (but the max in Note Block Studio is limited to -1200 and +1200). 0 is no fine-tuning. ±100 cents is a single semitone difference. + * @return this * @since v4 */ - public void setPitch(final short pitch) { + public NbsNote setPitch(final short pitch) { this.pitch = pitch; + return this; } - @Override - public NbsNote clone() { - return new NbsNote(this.layer, this.instrument, this.key, this.velocity, this.panning, this.pitch, this.customInstrument); + public NbsNote copy() { + final NbsNote copyNote = new NbsNote(); + copyNote.setInstrument(this.instrument); + copyNote.setKey(this.key); + copyNote.setVelocity(this.velocity); + copyNote.setPanning(this.panning); + copyNote.setPitch(this.pitch); + return copyNote; } @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; NbsNote nbsNote = (NbsNote) o; - return velocity == nbsNote.velocity && panning == nbsNote.panning && pitch == nbsNote.pitch && Objects.equals(customInstrument, nbsNote.customInstrument); + return instrument == nbsNote.instrument && key == nbsNote.key && velocity == nbsNote.velocity && panning == nbsNote.panning && pitch == nbsNote.pitch; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), customInstrument, velocity, panning, pitch); + return Objects.hash(instrument, key, velocity, panning, pitch); } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsSong.java b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsSong.java new file mode 100644 index 0000000..fdab4db --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/nbs/model/NbsSong.java @@ -0,0 +1,504 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.nbs.model; + +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.SongFormat; +import net.raphimc.noteblocklib.model.Song; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class NbsSong extends Song { + + /** + * @since v0 and {@literal >}= v3 + */ + private short length; + + /** + * @since v0 + */ + private byte version; + + /** + * @since v0 + */ + private int vanillaInstrumentCount; + + /** + * @since v0 + */ + private short layerCount; + + /** + * @since v0 + */ + private short tempo; + + /** + * @since v0 + */ + private boolean autoSave; + + /** + * @since v0 + */ + private byte autoSaveInterval; + + /** + * @since v0 + */ + private byte timeSignature; + + /** + * @since v0 + */ + private int minutesSpent; + + /** + * @since v0 + */ + private int leftClicks; + + /** + * @since v0 + */ + private int rightClicks; + + /** + * @since v0 + */ + private int noteBlocksAdded; + + /** + * @since v0 + */ + private int noteBlocksRemoved; + + /** + * @since v0 + */ + private String sourceFileName; + + /** + * @since v4 + */ + private boolean loop; + + /** + * @since v4 + */ + private byte maxLoopCount; + + /** + * @since v4 + */ + private short loopStartTick; + + /** + * @since v0 + */ + private final Map layers = new HashMap<>(); + + /** + * @since v0 + */ + private final List customInstruments = new ArrayList<>(); + + public NbsSong() { + this(null); + } + + public NbsSong(final String fileName) { + super(SongFormat.NBS, fileName); + this.version = 5; + this.vanillaInstrumentCount = MinecraftInstrument.values().length; + } + + /** + * @return The length of the song, measured in ticks. Divide this by the tempo to get the length of the song in seconds. Can be -1 if the nbsVersion did not support this field + * @since v0 and {@literal >}= v3 + */ + public short getLength() { + return this.length; + } + + /** + * @param length The length of the song, measured in ticks. Divide this by the tempo to get the length of the song in seconds. + * @return this + * @since v0 and {@literal >}= v3 + */ + public NbsSong setLength(final short length) { + this.length = length; + return this; + } + + /** + * @return The version of the NBS format. + * @since v0 + */ + public byte getVersion() { + return this.version; + } + + /** + * @param version The version of the NBS format. + * @return this + * @since v0 + */ + public NbsSong setVersion(final byte version) { + this.version = version; + return this; + } + + /** + * @return Amount of default instruments when the song was saved. This is needed to determine at what index custom instruments start. + * @since v0 + */ + public int getVanillaInstrumentCount() { + return this.vanillaInstrumentCount; + } + + /** + * @param vanillaInstrumentCount Amount of default instruments when the song was saved. This is needed to determine at what index custom instruments start. + * @return this + * @since v0 + */ + public NbsSong setVanillaInstrumentCount(final int vanillaInstrumentCount) { + this.vanillaInstrumentCount = vanillaInstrumentCount; + return this; + } + + /** + * @return The last layer with at least one note block in it, or the last layer that has had its name, volume or stereo changed. + * @since v0 + */ + public short getLayerCount() { + return this.layerCount; + } + + /** + * @param layerCount The last layer with at least one note block in it, or the last layer that has had its name, volume or stereo changed. + * @return this + * @since v0 + */ + public NbsSong setLayerCount(final short layerCount) { + this.layerCount = layerCount; + return this; + } + + /** + * @return The tempo of the song multiplied by 100 (for example, 1225 instead of 12.25). Measured in ticks per second. + * @since v0 + */ + public short getTempo() { + return this.tempo; + } + + /** + * @param tempo The tempo of the song multiplied by 100 (for example, 1225 instead of 12.25). Measured in ticks per second. + * @return this + * @since v0 + */ + public NbsSong setTempo(final short tempo) { + this.tempo = tempo; + return this; + } + + /** + * @return Whether auto-saving has been enabled (0 or 1). As of NBS version 4 this value is still saved to the file, but no longer used in the program. + * @since v0 + */ + public boolean isAutoSave() { + return this.autoSave; + } + + /** + * @param autoSave Whether auto-saving has been enabled (0 or 1). As of NBS version 4 this value is still saved to the file, but no longer used in the program. + * @return this + * @since v0 + */ + public NbsSong setAutoSave(final boolean autoSave) { + this.autoSave = autoSave; + return this; + } + + /** + * @return The amount of minutes between each auto-save (if it has been enabled) (1-60). As of NBS version 4 this value is still saved to the file, but no longer used in the program. + * @since v0 + */ + public byte getAutoSaveInterval() { + return this.autoSaveInterval; + } + + /** + * @param autoSaveInterval The amount of minutes between each auto-save (if it has been enabled) (1-60). As of NBS version 4 this value is still saved to the file, but no longer used in the program. + * @return this + * @since v0 + */ + public NbsSong setAutoSaveInterval(final byte autoSaveInterval) { + this.autoSaveInterval = autoSaveInterval; + return this; + } + + /** + * @return The time signature of the song. If this is 3, then the signature is 3/4. Default is 4. This value ranges from 2-8. + * @since v0 + */ + public byte getTimeSignature() { + return this.timeSignature; + } + + /** + * @param timeSignature The time signature of the song. If this is 3, then the signature is 3/4. Default is 4. This value ranges from 2-8. + * @return this + * @since v0 + */ + public NbsSong setTimeSignature(final byte timeSignature) { + this.timeSignature = timeSignature; + return this; + } + + /** + * @return Amount of minutes spent on the project. + * @since v0 + */ + public int getMinutesSpent() { + return this.minutesSpent; + } + + /** + * @param minutesSpent Amount of minutes spent on the project. + * @return this + * @since v0 + */ + public NbsSong setMinutesSpent(final int minutesSpent) { + this.minutesSpent = minutesSpent; + return this; + } + + /** + * @return Amount of times the user has left-clicked. + * @since v0 + */ + public int getLeftClicks() { + return this.leftClicks; + } + + /** + * @param leftClicks Amount of times the user has left-clicked. + * @return this + * @since v0 + */ + public NbsSong setLeftClicks(final int leftClicks) { + this.leftClicks = leftClicks; + return this; + } + + /** + * @return Amount of times the user has right-clicked. + * @since v0 + */ + public int getRightClicks() { + return this.rightClicks; + } + + /** + * @param rightClicks Amount of times the user has right-clicked. + * @return this + * @since v0 + */ + public NbsSong setRightClicks(final int rightClicks) { + this.rightClicks = rightClicks; + return this; + } + + /** + * @return Amount of times the user has added a note block. + * @since v0 + */ + public int getNoteBlocksAdded() { + return this.noteBlocksAdded; + } + + /** + * @param noteBlocksAdded Amount of times the user has added a note block. + * @return this + * @since v0 + */ + public NbsSong setNoteBlocksAdded(final int noteBlocksAdded) { + this.noteBlocksAdded = noteBlocksAdded; + return this; + } + + /** + * @return Amount of times the user has removed a note block. + * @since v0 + */ + public int getNoteBlocksRemoved() { + return this.noteBlocksRemoved; + } + + /** + * @param noteBlocksRemoved Amount of times the user has removed a note block. + * @return this + * @since v0 + */ + public NbsSong setNoteBlocksRemoved(final int noteBlocksRemoved) { + this.noteBlocksRemoved = noteBlocksRemoved; + return this; + } + + /** + * @return If the song has been imported from a .mid or .schematic file, that file name is stored here (only the name of the file, not the path). + * @since v0 + */ + public String getSourceFileName() { + return this.sourceFileName; + } + + /** + * @return If the song has been imported from a .mid or .schematic file, that file name is stored here (only the name of the file, not the path). + * @param fallback The fallback value if the source file name is not set. + * @since v0 + */ + public String getSourceFileNameOr(final String fallback) { + return this.sourceFileName == null ? fallback : this.sourceFileName; + } + + /** + * @param sourceFileName If the song has been imported from a .mid or .schematic file, that file name is stored here (only the name of the file, not the path). + * @return this + * @since v0 + */ + public NbsSong setSourceFileName(final String sourceFileName) { + if (sourceFileName != null && !sourceFileName.isEmpty()) { + this.sourceFileName = sourceFileName; + } else { + this.sourceFileName = null; + } + return this; + } + + /** + * @return Whether looping is on or off. + * @since v4 + */ + public boolean isLoop() { + return this.loop; + } + + /** + * @param loop Whether looping is on or off. + * @return this + * @since v4 + */ + public NbsSong setLoop(final boolean loop) { + this.loop = loop; + return this; + } + + /** + * @return 0 = infinite. Other values mean the amount of times the song loops. + * @since v4 + */ + public byte getMaxLoopCount() { + return this.maxLoopCount; + } + + /** + * @param maxLoopCount 0 = infinite. Other values mean the amount of times the song loops. + * @return this + * @since v4 + */ + public NbsSong setMaxLoopCount(final byte maxLoopCount) { + this.maxLoopCount = maxLoopCount; + return this; + } + + /** + * @return Determines which part of the song (in ticks) it loops back to. + * @since v4 + */ + public short getLoopStartTick() { + return this.loopStartTick; + } + + /** + * @param loopStartTick Determines which part of the song (in ticks) it loops back to. + * @return this + * @since v4 + */ + public NbsSong setLoopStartTick(final short loopStartTick) { + this.loopStartTick = loopStartTick; + return this; + } + + /** + * @return The layers of this song + * @since v0 + */ + public Map getLayers() { + return this.layers; + } + + /** + * @return The custom instruments of this song + * @since v0 + */ + public List getCustomInstruments() { + return this.customInstruments; + } + + @Override + public NbsSong copy() { + final NbsSong copySong = new NbsSong(this.getFileName()); + copySong.copyGeneralData(this); + copySong.setVersion(this.getVersion()); + copySong.setVanillaInstrumentCount(this.getVanillaInstrumentCount()); + copySong.setLayerCount(this.getLayerCount()); + copySong.setTempo(this.getTempo()); + copySong.setAutoSave(this.isAutoSave()); + copySong.setAutoSaveInterval(this.getAutoSaveInterval()); + copySong.setTimeSignature(this.getTimeSignature()); + copySong.setMinutesSpent(this.getMinutesSpent()); + copySong.setLeftClicks(this.getLeftClicks()); + copySong.setRightClicks(this.getRightClicks()); + copySong.setNoteBlocksAdded(this.getNoteBlocksAdded()); + copySong.setNoteBlocksRemoved(this.getNoteBlocksRemoved()); + copySong.setSourceFileName(this.getSourceFileName()); + copySong.setLoop(this.isLoop()); + copySong.setMaxLoopCount(this.getMaxLoopCount()); + copySong.setLoopStartTick(this.getLoopStartTick()); + final Map layers = this.getLayers(); + final Map copyLayers = copySong.getLayers(); + for (final Map.Entry entry : layers.entrySet()) { + copyLayers.put(entry.getKey(), entry.getValue().copy()); + } + final List customInstruments = this.getCustomInstruments(); + final List copyCustomInstruments = copySong.getCustomInstruments(); + for (final NbsCustomInstrument customInstrument : customInstruments) { + copyCustomInstruments.add(customInstrument.copy()); + } + return copySong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtConverter.java b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtConverter.java new file mode 100644 index 0000000..1b13f8a --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtConverter.java @@ -0,0 +1,59 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.txt; + +import net.raphimc.noteblocklib.data.MinecraftDefinitions; +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.txt.model.TxtNote; +import net.raphimc.noteblocklib.format.txt.model.TxtSong; +import net.raphimc.noteblocklib.model.Note; +import net.raphimc.noteblocklib.model.Song; +import net.raphimc.noteblocklib.util.SongResampler; + +import java.util.ArrayList; + +public class TxtConverter { + + /** + * Creates a new TXT song from the general data of the given song (Also copies some format specific fields if applicable). + * + * @param song The song + * @return The new TXT song + */ + public static TxtSong createSong(Song song) { + song = song.copy(); + SongResampler.changeTickSpeed(song, TxtDefinitions.TEMPO); + + final TxtSong newSong = new TxtSong(); + newSong.copyGeneralData(song); + + for (int tick : song.getNotes().getTicks()) { + for (Note note : song.getNotes().get(tick)) { + if (note.getInstrument() instanceof MinecraftInstrument && note.getVolume() > 0) { + final TxtNote txtNote = new TxtNote(); + txtNote.setInstrument(((MinecraftInstrument) note.getInstrument()).mcId()); + txtNote.setKey((byte) Math.max(MinecraftDefinitions.MC_LOWEST_KEY, Math.min(MinecraftDefinitions.MC_HIGHEST_KEY, note.getMcKey()))); + newSong.getTxtNotes().computeIfAbsent(tick, k -> new ArrayList<>()).add(txtNote); + } + } + } + + return newSong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtDefinitions.java b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtDefinitions.java new file mode 100644 index 0000000..36dd3f8 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtDefinitions.java @@ -0,0 +1,24 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.txt; + +public class TxtDefinitions { + + public static final int TEMPO = 20; + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtIo.java b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtIo.java new file mode 100644 index 0000000..b639eb8 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtIo.java @@ -0,0 +1,95 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.txt; + +import net.raphimc.noteblocklib.data.MinecraftInstrument; +import net.raphimc.noteblocklib.format.txt.model.TxtNote; +import net.raphimc.noteblocklib.format.txt.model.TxtSong; +import net.raphimc.noteblocklib.model.Note; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class TxtIo { + + private static final int BUFFER_SIZE = 1024 * 1024; + + public static TxtSong readSong(final InputStream is, final String fileName) throws IOException { + final BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8), BUFFER_SIZE); + final TxtSong song = new TxtSong(fileName); + + final Map> notes = song.getTxtNotes(); + while (true) { + final String line = reader.readLine(); + if (line == null) break; + if (line.isEmpty()) continue; + + if (line.startsWith("// Name: ")) { + song.setTitle(line.substring(9)); + } else if (line.startsWith("// Author: ")) { + song.setAuthor(line.substring(11)); + } else { + final String[] split = line.split(":"); + final int tick = Integer.parseInt(split[0]); + final byte key = Byte.parseByte(split[1]); + final byte instrument = Byte.parseByte(split[2]); + + final TxtNote note = new TxtNote(); + note.setInstrument(instrument); + note.setKey(key); + notes.computeIfAbsent(tick, k -> new ArrayList<>()).add(note); + } + } + + { // Fill generalized song structure with data + song.getTempoEvents().set(0, TxtDefinitions.TEMPO); + for (Map.Entry> entry : notes.entrySet()) { + for (TxtNote txtNote : entry.getValue()) { + final Note note = new Note(); + note.setInstrument(MinecraftInstrument.fromMcId(txtNote.getInstrument())); + note.setMcKey(txtNote.getKey()); + song.getNotes().add(entry.getKey(), note); + } + } + } + + return song; + } + + public static void writeSong(final TxtSong song, final OutputStream os) throws IOException { + final OutputStreamWriter writer = new OutputStreamWriter(new BufferedOutputStream(os, BUFFER_SIZE), StandardCharsets.UTF_8); + if (song.getTitle() != null) { + writer.write("// Name: " + song.getTitle() + "\n"); + } + if (song.getAuthor() != null) { + writer.write("// Author: " + song.getAuthor() + "\n"); + } + + for (Map.Entry> entry : song.getTxtNotes().entrySet()) { + for (TxtNote txtNote : entry.getValue()) { + writer.write(entry.getKey() + ":" + txtNote.getKey() + ":" + txtNote.getInstrument() + "\n"); + } + } + + writer.flush(); + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtParser.java b/src/main/java/net/raphimc/noteblocklib/format/txt/TxtParser.java deleted file mode 100644 index 4c16a01..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/txt/TxtParser.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.txt; - -import net.raphimc.noteblocklib.format.txt.model.TxtData; -import net.raphimc.noteblocklib.format.txt.model.TxtHeader; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; - -public class TxtParser { - - public static TxtSong read(final byte[] bytes, final String fileName) { - final Scanner scanner = new Scanner(new ByteArrayInputStream(bytes)); - - final TxtHeader header = new TxtHeader(scanner); - final TxtData data = new TxtData(scanner); - - return new TxtSong(fileName, header, data); - } - - public static byte[] write(final TxtSong song) { - final StringBuilder builder = new StringBuilder(); - - song.getHeader().write(builder); - song.getData().write(builder); - - return builder.toString().getBytes(StandardCharsets.UTF_8); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtData.java b/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtData.java deleted file mode 100644 index 872cb12..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtData.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.txt.model; - -import net.raphimc.noteblocklib.model.NotemapData; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Scanner; - -public class TxtData extends NotemapData { - - public TxtData(final Scanner scanner) { - scanner.useDelimiter("[:\r\n]+"); - while (scanner.hasNext("\\d+")) { - this.notes.computeIfAbsent(scanner.nextInt(), k -> new ArrayList<>()).add(new TxtNote(scanner)); - } - } - - public void write(final StringBuilder builder) { - for (final Map.Entry> entry : this.notes.entrySet()) { - for (final TxtNote note : entry.getValue()) { - builder.append(entry.getKey()).append(":"); - note.write(builder); - builder.append("\n"); - } - } - } - - public TxtData(final Map> notes) { - super(notes); - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtHeader.java b/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtHeader.java deleted file mode 100644 index 2bd3636..0000000 --- a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtHeader.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.format.txt.model; - -import net.raphimc.noteblocklib.model.Header; - -import java.util.Scanner; - -public class TxtHeader implements Header { - - /** - * @since v2 - */ - private float speed = 20F; - - public TxtHeader(final Scanner scanner) { - if (scanner.hasNext("#{3}\\d+")) { - this.speed = Float.parseFloat(scanner.skip("#{3}").next("\\d+(|\\.\\d+)")); - } - } - - public void write(final StringBuilder builder) { - builder.append("###").append(this.speed).append('\n'); - } - - public TxtHeader(final float speed) { - this.speed = speed; - } - - public TxtHeader() { - } - - /** - * @return The speed of the song in ticks per second. - * @since v2 - */ - public float getSpeed() { - return this.speed; - } - - /** - * @param speed The speed of the song in ticks per second. - * @since v2 - */ - public void setSpeed(final float speed) { - this.speed = speed; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtNote.java b/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtNote.java index 6c190d9..8757b01 100644 --- a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtNote.java +++ b/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtNote.java @@ -17,39 +17,48 @@ */ package net.raphimc.noteblocklib.format.txt.model; -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.util.Instrument; -import net.raphimc.noteblocklib.util.MinecraftDefinitions; +import java.util.Objects; -import java.util.Scanner; +public class TxtNote { -public class TxtNote extends Note { + private byte instrument; + private byte key; - public TxtNote(final Scanner scanner) { - this(scanner.nextByte(), Instrument.fromMcId(scanner.nextByte())); + public byte getInstrument() { + return this.instrument; } - public TxtNote(final byte key, final Instrument instrument) { - super(instrument, key); + public TxtNote setInstrument(final byte instrument) { + this.instrument = instrument; + return this; } - public void write(final StringBuilder builder) { - builder.append(this.key).append(":").append(this.instrument.mcId()); + public byte getKey() { + return this.key; } - @Override - public byte getKey() { - return (byte) (super.getKey() + MinecraftDefinitions.MC_LOWEST_KEY); + public TxtNote setKey(final byte key) { + this.key = key; + return this; + } + + public TxtNote copy() { + final TxtNote copyNote = new TxtNote(); + copyNote.setInstrument(this.getInstrument()); + copyNote.setKey(this.getKey()); + return copyNote; } @Override - public void setKey(final byte key) { - super.setKey((byte) (key - MinecraftDefinitions.MC_LOWEST_KEY)); + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TxtNote txtNote = (TxtNote) o; + return instrument == txtNote.instrument && key == txtNote.key; } @Override - public TxtNote clone() { - return new TxtNote(this.key, this.instrument); + public int hashCode() { + return Objects.hash(instrument, key); } } diff --git a/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtSong.java b/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtSong.java new file mode 100644 index 0000000..928e9f3 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/format/txt/model/TxtSong.java @@ -0,0 +1,63 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.format.txt.model; + +import net.raphimc.noteblocklib.format.SongFormat; +import net.raphimc.noteblocklib.model.Song; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TxtSong extends Song { + + private final Map> notes = new HashMap<>(); + + public TxtSong() { + this(null); + } + + public TxtSong(final String fileName) { + super(SongFormat.TXT, fileName); + } + + /** + * @return A map of all notes, with the tick as the key. + */ + public Map> getTxtNotes() { + return this.notes; + } + + @Override + public TxtSong copy() { + final TxtSong copySong = new TxtSong(this.getFileName()); + copySong.copyGeneralData(this); + final Map> notes = this.getTxtNotes(); + final Map> copyNotes = copySong.getTxtNotes(); + for (Map.Entry> entry : notes.entrySet()) { + final List copyNoteList = new ArrayList<>(entry.getValue().size()); + for (TxtNote note : entry.getValue()) { + copyNoteList.add(note.copy()); + } + copyNotes.put(entry.getKey(), copyNoteList); + } + return copySong; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/model/NoteWithVolume.java b/src/main/java/net/raphimc/noteblocklib/model/GenericSong.java similarity index 76% rename from src/main/java/net/raphimc/noteblocklib/model/NoteWithVolume.java rename to src/main/java/net/raphimc/noteblocklib/model/GenericSong.java index 35a3c67..84678cf 100644 --- a/src/main/java/net/raphimc/noteblocklib/model/NoteWithVolume.java +++ b/src/main/java/net/raphimc/noteblocklib/model/GenericSong.java @@ -17,16 +17,17 @@ */ package net.raphimc.noteblocklib.model; -public interface NoteWithVolume { +public class GenericSong extends Song { - /** - * @return The velocity/volume of the note, from 0% to 100%. - */ - float getVolume(); + public GenericSong() { + super(null, null); + } - /** - * @param volume The velocity/volume of the note, from 0% to 100%. - */ - void setVolume(final float volume); + @Override + public Song copy() { + final GenericSong copySong = new GenericSong(); + copySong.copyGeneralData(this); + return copySong; + } } diff --git a/src/main/java/net/raphimc/noteblocklib/model/Note.java b/src/main/java/net/raphimc/noteblocklib/model/Note.java index 01c5ebe..c116823 100644 --- a/src/main/java/net/raphimc/noteblocklib/model/Note.java +++ b/src/main/java/net/raphimc/noteblocklib/model/Note.java @@ -17,65 +17,183 @@ */ package net.raphimc.noteblocklib.model; -import net.raphimc.noteblocklib.util.Instrument; +import net.raphimc.noteblocklib.data.Constants; +import net.raphimc.noteblocklib.data.MinecraftDefinitions; +import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; +import net.raphimc.noteblocklib.model.instrument.Instrument; import java.util.Objects; -public abstract class Note implements Cloneable { +public class Note { - protected Instrument instrument; - protected byte key; + private Instrument instrument; + private float midiKey; + private float volume; + private float panning; - public Note(final Instrument instrument, final byte key) { - this.instrument = instrument; - this.key = key; + public Note() { + this.volume = 1F; + this.panning = 0F; + } + + /** + * @return If the note is outside the vanilla Minecraft octave range. + */ + public boolean isOutsideMinecraftOctaveRange() { + return this.midiKey < MinecraftDefinitions.MC_LOWEST_MIDI_KEY || this.midiKey > MinecraftDefinitions.MC_HIGHEST_MIDI_KEY; } /** - * @return The instrument of the note. Null if the note uses a custom instrument. + * @return The instrument of the note. Default Minecraft instruments are stored in {@link net.raphimc.noteblocklib.data.MinecraftInstrument}. */ public Instrument getInstrument() { return this.instrument; } /** - * @param instrument The instrument of the note. Null if the note uses a custom instrument. + * @param instrument The instrument of the note. Default Minecraft instruments are stored in {@link net.raphimc.noteblocklib.data.MinecraftInstrument}. + * @return this */ - public void setInstrument(final Instrument instrument) { + public Note setInstrument(final Instrument instrument) { this.instrument = instrument; + return this; + } + + /** + * @return The MIDI key of the note (21 = A0, 108 = C8). Fractional part is the fine-pitch. + */ + public float getMidiKey() { + return this.midiKey; + } + + /** + * @param midiKey The MIDI key of the note (21 = A0, 108 = C8). Fractional part is the fine-pitch. + * @return this + */ + public Note setMidiKey(final float midiKey) { + this.midiKey = midiKey; + return this; + } + + /** + * Calling this has the same effect as calling {@link #getMidiKey()}. + * + * @return The key of the note in the Minecraft Note Block range (0-24). The center key (F#4) is 12. May be out of range, if the note has not been transposed. + */ + public int getMcKey() { + return Math.round(this.midiKey - MinecraftDefinitions.MC_LOWEST_MIDI_KEY); + } + + /** + * Calling this has the same effect as calling {@link #setMidiKey(float)}. + * + * @param mcKey The key of the note in the Minecraft Note Block range (0-24). The center key (F#4) is 12. + * @return this + */ + public Note setMcKey(final int mcKey) { + this.midiKey = mcKey + MinecraftDefinitions.MC_LOWEST_MIDI_KEY; + return this; + } + + /** + * Calling this has the same effect as calling {@link #getMidiKey()}. + * + * @return The key of the note in the Minecraft Note Block Studio range (0-87). The center key (F#4) is 45. May be out of range, if the note has not been transposed. + */ + public int getNbsKey() { + return Math.round(this.midiKey - NbsDefinitions.NBS_LOWEST_MIDI_KEY); + } + + /** + * Calling this has the same effect as calling {@link #setMidiKey(float)}. + * + * @param nbsKey The key of the note in the Minecraft Note Block Studio range (0-87). The center key (F#4) is 45. + * @return this + */ + public Note setNbsKey(final float nbsKey) { + this.midiKey = nbsKey + NbsDefinitions.NBS_LOWEST_MIDI_KEY; + return this; + } + + /** + * @return The fractional part of the note key (-0.5F = 50% lower, 0.0F = normal, 0.5F = 50% higher). Only useful if you need the key as an integer and the fine-pitch separately. + */ + public float getFractionalKeyPart() { + final int roundedKey = Math.round(this.midiKey); + return this.midiKey - roundedKey; + } + + /** + * Calling this has the same effect as calling {@link #getMidiKey()}. + * + * @return The pitch of the note to use when playing the sample. (1.0F = normal speed, 2.0F = double speed, 0.5F = half speed). The center key (F#4) is 1.0F. + */ + public float getPitch() { + return (float) Math.pow(2D, (double) (this.midiKey - Constants.F_SHARP_4_MIDI_KEY) / Constants.KEYS_PER_OCTAVE); + } + + /** + * Calling this has the same effect as calling {@link #setMidiKey(float)}. + * + * @param pitch The pitch of the note to use when playing the sample. (1.0F = normal speed, 2.0F = double speed, 0.5F = half speed). The center key (F#4) is 1.0F. + * @return this + */ + public Note setPitch(final float pitch) { + this.midiKey = (float) (Constants.F_SHARP_4_MIDI_KEY + Constants.KEYS_PER_OCTAVE * Math.log(pitch) / Math.log(2D)); + return this; + } + + /** + * @return The volume of the note. (0.0F = 0%, 1.0F = 100%) + */ + public float getVolume() { + return this.volume; } /** - * @return The key of the note, from 0-87, where 0 is A0 and 87 is C8. 33-57 is within the 2-octave limit. + * @param volume The volume of the note. (0.0F = 0%, 1.0F = 100%) + * @return this */ - public byte getKey() { - return this.key; + public Note setVolume(final float volume) { + this.volume = volume; + return this; } /** - * @param key The key of the note, from 0-87, where 0 is A0 and 87 is C8. 33-57 is within the 2-octave limit. + * @return The panning of the note. (-1.0F = 100% left, 0.0F = 0% center, 1.0F = 100% right) */ - public void setKey(final byte key) { - this.key = key; + public float getPanning() { + return this.panning; } - public byte getRawKey() { - return this.key; + /** + * @param panning The panning of the note. (-1.0F = 100% left, 0.0F = 0% center, 1.0F = 100% right) + * @return this + */ + public Note setPanning(final float panning) { + this.panning = panning; + return this; } - public abstract Note clone(); + public Note copy() { + final Note copyNote = new Note(); + copyNote.instrument = this.instrument.copy(); + copyNote.midiKey = this.midiKey; + copyNote.volume = this.volume; + copyNote.panning = this.panning; + return copyNote; + } @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Note note = (Note) o; - return key == note.key && instrument == note.instrument; + return midiKey == note.midiKey && Float.compare(volume, note.volume) == 0 && Float.compare(panning, note.panning) == 0 && Objects.equals(instrument, note.instrument); } @Override public int hashCode() { - return Objects.hash(instrument, key); + return Objects.hash(instrument, midiKey, volume, panning); } } diff --git a/src/main/java/net/raphimc/noteblocklib/model/NotemapData.java b/src/main/java/net/raphimc/noteblocklib/model/NotemapData.java deleted file mode 100644 index 71489e0..0000000 --- a/src/main/java/net/raphimc/noteblocklib/model/NotemapData.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.model; - -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public abstract class NotemapData implements Data { - - protected Map> notes; - - public NotemapData() { - this.notes = new TreeMap<>(); - } - - public NotemapData(final Map> notes) { - this.notes = notes; - } - - public Map> getNotes() { - return this.notes; - } - - public void setNotes(final Map> notes) { - this.notes = notes; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/model/Notes.java b/src/main/java/net/raphimc/noteblocklib/model/Notes.java new file mode 100644 index 0000000..2cbe0fb --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/model/Notes.java @@ -0,0 +1,193 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.model; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class Notes { + + private final Map> notes = new HashMap<>(); + + private int lastTick; + private boolean recomputeLastTick = true; + + public List get(final int tick) { + return this.notes.get(tick); + } + + public List getOrEmpty(final int tick) { + return this.notes.getOrDefault(tick, new ArrayList<>()); + } + + public void set(final int tick, final List notes) { + if (notes != null) { + if (this.notes.put(tick, notes) == null) { + this.recomputeLastTick = true; + } + } else { + if (this.notes.remove(tick) != null) { + this.recomputeLastTick = true; + } + } + } + + public void add(final int tick, final Note note) { + this.notes.computeIfAbsent(tick, k -> { + this.recomputeLastTick = true; + return new ArrayList<>(); + }).add(note); + } + + public void add(final int tick, final List notes) { + this.notes.computeIfAbsent(tick, k -> { + this.recomputeLastTick = true; + return new ArrayList<>(); + }).addAll(notes); + } + + public Set getTicks() { + return Collections.unmodifiableSet(this.notes.keySet()); + } + + public void clearTick(final int tick) { + this.notes.remove(tick); + } + + public void clear() { + this.notes.clear(); + } + + /** + * Applies the given consumer to all notes.
+ * Use cases for this method can be for example to transpose all notes in a song to be within the minecraft octave range. + * + * @param noteConsumer The consumer + */ + public void forEach(final Consumer noteConsumer) { + this.notes.values().stream().flatMap(Collection::stream).forEach(noteConsumer); + } + + /** + * Applies the given predicate to all notes.
+ * The predicate can return true to break the iteration.
+ * Use cases for this method can be for example to check if any note is outside the minecraft octave range. + * + * @param notePredicate The predicate + * @return True if the predicate returned true for any note + */ + public boolean testEach(final Predicate notePredicate) { + for (List list : this.notes.values()) { + for (Note note : list) { + if (notePredicate.test(note)) { + return true; + } + } + } + return false; + } + + /** + * Removes all notes which match the given predicate. + * + * @param notePredicate The predicate + */ + public void removeIf(final Predicate notePredicate) { + for (List list : this.notes.values()) { + list.removeIf(notePredicate); + } + this.compact(); + } + + /** + * Removes duplicate notes which are on the same tick.
+ * Useful when handling large MIDI files with a lot of duplicate notes. + */ + public void removeDoubleNotes() { + for (List list : this.notes.values()) { + final Set set = new HashSet<>(list); + list.clear(); + list.addAll(set); + } + } + + /** + * Removes all notes which have a volume of 0. + */ + public void removeSilentNotes() { + this.removeSilentNotes(0F); + } + + /** + * Removes all notes which have a volume lower than or equal the given threshold. + * + * @param threshold The threshold (0.0 - 1.0) + */ + public void removeSilentNotes(final float threshold) { + this.removeIf(note -> note.getVolume() <= threshold); + this.compact(); + } + + /** + * Removes empty note lists from the notes map. + */ + public void compact() { + this.notes.entrySet().removeIf(entry -> entry.getValue().isEmpty()); + this.recomputeLastTick = true; + } + + /** + * @return The last tick in the song. + */ + public int getLastTick() { + if (this.recomputeLastTick) { + this.compact(); + this.lastTick = this.notes.keySet().stream().max(Integer::compareTo).orElse(0); + this.recomputeLastTick = false; + } + return this.lastTick; + } + + /** + * @return The length of the song in ticks. + */ + public int getLengthInTicks() { + return this.getLastTick() + 1; + } + + /** + * @return The total amount of notes in a song. + */ + public int getNoteCount() { + return this.notes.values().stream().mapToInt(List::size).sum(); + } + + public Notes copy() { + final Notes copyNotes = new Notes(); + for (Map.Entry> entry : this.notes.entrySet()) { + final List noteList = new ArrayList<>(); + for (Note note : entry.getValue()) { + noteList.add(note.copy()); + } + copyNotes.notes.put(entry.getKey(), noteList); + } + return copyNotes; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/model/Song.java b/src/main/java/net/raphimc/noteblocklib/model/Song.java index 82b9445..e96dd9e 100644 --- a/src/main/java/net/raphimc/noteblocklib/model/Song.java +++ b/src/main/java/net/raphimc/noteblocklib/model/Song.java @@ -19,51 +19,188 @@ import net.raphimc.noteblocklib.format.SongFormat; -public abstract class Song, N extends Note> { +import java.util.Set; +import java.util.TreeSet; - private final SongFormat format; - protected final String fileName; - private final H header; - private final D data; +public abstract class Song { - private SongView view; + private final SongFormat format; + private Notes notes = new Notes(); + private TempoEvents tempoEvents = new TempoEvents(); + private final String fileName; + private String title; + private String author; + private String originalAuthor; + private String description; - public Song(final SongFormat format, final String fileName, final H header, final D data) { + protected Song(final SongFormat format, final String fileName) { this.format = format; this.fileName = fileName; - this.header = header; - this.data = data; + } + + public int getLengthInMilliseconds() { + return this.tickToMilliseconds(this.notes.getLengthInTicks()); + } + + public int getLengthInSeconds() { + return (int) Math.ceil(this.getLengthInMilliseconds() / 1000F); + } + + public String getHumanReadableLength() { + final int length = this.getLengthInSeconds(); + return String.format("%02d:%02d:%02d", length / 3600, (length / 60) % 60, length % 60); + } + + public int tickToMilliseconds(final int tick) { + final Set tempoEventTicks = new TreeSet<>(this.tempoEvents.getTicks()); + tempoEventTicks.add(tick); + + int lastTick = 0; + float totalMilliseconds = 0; + for (int tempoTick : tempoEventTicks) { + if (tempoTick > tick) { + break; + } + + final float tps = this.tempoEvents.getEffectiveTempo(lastTick); + final int ticksInSegment = tempoTick - lastTick; + final float segmentMilliseconds = (ticksInSegment / tps) * 1000F; + totalMilliseconds += segmentMilliseconds; + lastTick = tempoTick; + } - this.view = this.createView(); + return (int) Math.ceil(totalMilliseconds); } - protected abstract SongView createView(); + public int millisecondsToTick(final int milliseconds) { + final Set tempoEventTicks = new TreeSet<>(this.tempoEvents.getTicks()); + tempoEventTicks.add(this.notes.getLengthInTicks()); - public void refreshView() { - this.view = this.createView(); + int lastTick = 0; + float totalMilliseconds = 0; + for (int tempoTick : tempoEventTicks) { + final float tps = this.tempoEvents.getEffectiveTempo(lastTick); + final int ticksInSegment = tempoTick - lastTick; + final float segmentMilliseconds = (ticksInSegment / tps) * 1000F; + + if (totalMilliseconds + segmentMilliseconds >= milliseconds) { + final float remainingMilliseconds = milliseconds - totalMilliseconds; + final int ticksToAdd = Math.round((remainingMilliseconds / 1000F) * tps); + return lastTick + ticksToAdd; + } + + totalMilliseconds += segmentMilliseconds; + lastTick = tempoTick; + } + + return this.notes.getLengthInTicks(); } public SongFormat getFormat() { return this.format; } - public H getHeader() { - return this.header; + public Notes getNotes() { + return this.notes; + } + + public TempoEvents getTempoEvents() { + return this.tempoEvents; + } + + public String getFileName() { + return this.fileName; + } + + public String getFileNameOr(final String fallback) { + return this.fileName == null ? fallback : this.fileName; + } + + public String getTitle() { + return this.title; + } + + public String getTitleOr(final String fallback) { + return this.title == null ? fallback : this.title; + } + + public Song setTitle(final String title) { + if (title != null && !title.isEmpty()) { + this.title = title; + } else { + this.title = null; + } + return this; + } + + public String getAuthor() { + return this.author; + } + + public String getAuthorOr(final String fallback) { + return this.author == null ? fallback : this.author; + } + + public Song setAuthor(final String author) { + if (author != null && !author.isEmpty()) { + this.author = author; + } else { + this.author = null; + } + return this; + } + + public String getOriginalAuthor() { + return this.originalAuthor; + } + + public String getOriginalAuthorOr(final String fallback) { + return this.originalAuthor == null ? fallback : this.originalAuthor; + } + + public Song setOriginalAuthor(final String originalAuthor) { + if (originalAuthor != null && !originalAuthor.isEmpty()) { + this.originalAuthor = originalAuthor; + } else { + this.originalAuthor = null; + } + return this; + } + + public String getDescription() { + return this.description; + } + + public String getDescriptionOr(final String fallback) { + return this.description == null ? fallback : this.description; + } + + public Song setDescription(final String description) { + if (description != null && !description.isEmpty()) { + this.description = description; + } else { + this.description = null; + } + return this; } - public D getData() { - return this.data; + public String getTitleOrFileName() { + return this.title == null ? this.fileName : this.title; } - /** - * Returns an abstracted, generalized and unified view of this song.
- * Any changes made to this view will not be reflected in the original song data.
- * The view may be recreated by using {@link #refreshView()}. - * - * @return The song view - */ - public SongView getView() { - return this.view; + public String getTitleOrFileNameOr(final String fallback) { + return this.getTitleOrFileName() == null ? fallback : this.getTitleOrFileName(); } + public void copyGeneralData(final Song song) { + this.notes = song.getNotes().copy(); + this.tempoEvents = song.getTempoEvents().copy(); + this.setTitle(song.getTitle()); + this.setAuthor(song.getAuthor()); + this.setOriginalAuthor(song.getOriginalAuthor()); + this.setDescription(song.getDescription()); + } + + public abstract Song copy(); + } diff --git a/src/main/java/net/raphimc/noteblocklib/model/SongView.java b/src/main/java/net/raphimc/noteblocklib/model/SongView.java deleted file mode 100644 index 730fa47..0000000 --- a/src/main/java/net/raphimc/noteblocklib/model/SongView.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.model; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.function.BinaryOperator; -import java.util.stream.Collectors; - -public class SongView implements Cloneable { - - private String title; - private int length; - private float speed; - private Map> notes; - - public SongView(final String title, final float speed, final Map> notes) { - this.title = title; - this.speed = speed; - this.notes = notes; - - this.recalculateLength(); - } - - private SongView(final String title, final float speed, final int length, final Map> notes) { - this.title = title; - this.speed = speed; - this.length = length; - this.notes = notes; - } - - /** - * @return The title of the song - */ - public String getTitle() { - return this.title; - } - - public void setTitle(final String title) { - this.title = title; - } - - /** - * @return The length of the song, measured in ticks. - */ - public int getLength() { - return this.length; - } - - public void recalculateLength() { - this.length = this.notes.keySet().stream().mapToInt(i -> i).max().orElse(-1) + 1; - } - - /** - * @return The tempo of the song, measured in ticks per second. - */ - public float getSpeed() { - return this.speed; - } - - public void setSpeed(final float speed) { - this.speed = speed; - } - - public List getNotesAtTick(final int tick) { - return this.notes.getOrDefault(tick, Collections.emptyList()); - } - - public void setNotesAtTick(final int tick, final List notes) { - this.notes.put(tick, notes); - } - - public Map> getNotes() { - return this.notes; - } - - public void setNotes(final Map> notes) { - this.notes = notes; - } - - @Override - public SongView clone() { - final Map> notes = this.notes.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream() - .map(n -> (N) n.clone()) - .collect(Collectors.toList()), throwingMerger(), TreeMap::new) - ); - return new SongView<>(this.title, this.speed, this.length, notes); - } - - private static BinaryOperator throwingMerger() { - return (u, v) -> { - throw new IllegalStateException(String.format("Duplicate key %s", u)); - }; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/model/TempoEvents.java b/src/main/java/net/raphimc/noteblocklib/model/TempoEvents.java new file mode 100644 index 0000000..4617093 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocklib/model/TempoEvents.java @@ -0,0 +1,91 @@ +/* + * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib + * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.noteblocklib.model; + +import java.util.Collections; +import java.util.SortedSet; +import java.util.TreeMap; + +public class TempoEvents { + + private static final float DEFAULT_TEMPO = 20F; + + private final TreeMap tempoEvents = new TreeMap<>(); + + public TempoEvents() { + this.tempoEvents.put(0, DEFAULT_TEMPO); + } + + public float get(final int tick) { + return this.tempoEvents.getOrDefault(tick, 0F); + } + + public float getEffectiveTempo(final int tick) { + final Float tempo = this.tempoEvents.get(tick); + if (tempo != null) { + return tempo; + } else { + return this.tempoEvents.floorEntry(tick).getValue(); + } + } + + public void set(final int tick, final float tempo) { + this.tempoEvents.put(tick, tempo); + } + + public SortedSet getTicks() { + return Collections.unmodifiableSortedSet(this.tempoEvents.navigableKeySet()); + } + + public void remove(final int tick) { + if (tick == 0) { + throw new IllegalArgumentException("Cannot remove the initial tempo event"); + } + + this.tempoEvents.remove(tick); + } + + public void clear() { + this.tempoEvents.clear(); + this.tempoEvents.put(0, DEFAULT_TEMPO); + } + + /** + * @return A float[] with 2 elements, the first element is the slowest tempo, the second element is the fastest tempo. + */ + public float[] getTempoRange() { + final float minTempo = this.tempoEvents.values().stream().min(Float::compareTo).orElse(0F); + final float maxTempo = this.tempoEvents.values().stream().max(Float::compareTo).orElse(0F); + return new float[]{minTempo, maxTempo}; + } + + /** + * @return A human readable tempo range string. + */ + public String getHumanReadableTempoRange() { + final float[] tempoRange = this.getTempoRange(); + return tempoRange[0] == tempoRange[1] ? String.format("%.2f", tempoRange[0]) : String.format("%.2f", tempoRange[0]) + " - " + String.format("%.2f", tempoRange[1]); + } + + public TempoEvents copy() { + final TempoEvents copyTempoEvents = new TempoEvents(); + copyTempoEvents.tempoEvents.putAll(this.tempoEvents); + return copyTempoEvents; + } + +} diff --git a/src/main/java/net/raphimc/noteblocklib/model/Data.java b/src/main/java/net/raphimc/noteblocklib/model/event/Event.java similarity index 89% rename from src/main/java/net/raphimc/noteblocklib/model/Data.java rename to src/main/java/net/raphimc/noteblocklib/model/event/Event.java index 2abd7c6..c4c4e44 100644 --- a/src/main/java/net/raphimc/noteblocklib/model/Data.java +++ b/src/main/java/net/raphimc/noteblocklib/model/event/Event.java @@ -15,7 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.model; +package net.raphimc.noteblocklib.model.event; + +public interface Event { + + Event copy(); -public interface Data { } diff --git a/src/main/java/net/raphimc/noteblocklib/model/Header.java b/src/main/java/net/raphimc/noteblocklib/model/instrument/Instrument.java similarity index 88% rename from src/main/java/net/raphimc/noteblocklib/model/Header.java rename to src/main/java/net/raphimc/noteblocklib/model/instrument/Instrument.java index 95105ec..8b558b6 100644 --- a/src/main/java/net/raphimc/noteblocklib/model/Header.java +++ b/src/main/java/net/raphimc/noteblocklib/model/instrument/Instrument.java @@ -15,7 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.model; +package net.raphimc.noteblocklib.model.instrument; + +public interface Instrument { + + Instrument copy(); -public interface Header { } diff --git a/src/main/java/net/raphimc/noteblocklib/player/FullNoteConsumer.java b/src/main/java/net/raphimc/noteblocklib/player/FullNoteConsumer.java deleted file mode 100644 index dbee0c1..0000000 --- a/src/main/java/net/raphimc/noteblocklib/player/FullNoteConsumer.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.player; - -import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; -import net.raphimc.noteblocklib.format.nbs.model.NbsCustomInstrument; -import net.raphimc.noteblocklib.format.nbs.model.NbsNote; -import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.model.NoteWithPanning; -import net.raphimc.noteblocklib.model.NoteWithVolume; -import net.raphimc.noteblocklib.util.Instrument; -import net.raphimc.noteblocklib.util.MinecraftDefinitions; - -public interface FullNoteConsumer extends NoteConsumer { - - @Override - default void playNote(final Note note) { - final float volume; - if (note instanceof NoteWithVolume) { - final NoteWithVolume noteWithVolume = (NoteWithVolume) note; - volume = noteWithVolume.getVolume(); - } else { - volume = 100F; - } - if (volume <= 0) return; - - final float panning; - if (note instanceof NoteWithPanning) { - final NoteWithPanning noteWithPanning = (NoteWithPanning) note; - panning = noteWithPanning.getPanning(); - } else { - panning = 0F; - } - - final float pitch; - if (note instanceof NbsNote) { - NbsNote nbsNote = (NbsNote) note; - if (nbsNote.getCustomInstrument() != null) { - nbsNote = nbsNote.clone(); - nbsNote.setKey((byte) (nbsNote.getKey() + nbsNote.getCustomInstrument().getPitch() - 45)); - } - pitch = MinecraftDefinitions.nbsPitchToMcPitch(NbsDefinitions.getPitch(nbsNote)); - } else { - pitch = MinecraftDefinitions.mcKeyToMcPitch(MinecraftDefinitions.nbsKeyToMcKey(note.getKey())); - } - - final float playerVolume = volume / 100F; - final float playerPanning = panning / 100F; - if (note.getInstrument() != null) { - this.playNote(note.getInstrument(), pitch, playerVolume, playerPanning); - } else if (note instanceof NbsNote && ((NbsNote) note).getCustomInstrument() != null) { - this.playCustomNote(((NbsNote) note).getCustomInstrument(), pitch, playerVolume, playerPanning); - } - } - - /** - * Plays a note with the given instrument, volume, pitch and panning. - * - * @param instrument The instrument of the note - * @param pitch The pitch of the note - * @param volume The volume of the note (0.0 to 1.0) - * @param panning The panning of the note (-1.0 to 1.0) - */ - void playNote(final Instrument instrument, final float pitch, final float volume, final float panning); - - /** - * Plays a note with the given custom instrument, volume, pitch and panning. The pitch offset of the custom instrument is already applied to the note. - * - * @param customInstrument The custom instrument of the note - * @param pitch The pitch of the note - * @param volume The volume of the note (0.0 to 1.0) - * @param panning The panning of the note (-1.0 to 1.0) - */ - default void playCustomNote(final NbsCustomInstrument customInstrument, final float pitch, final float volume, final float panning) { - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/player/SongPlayer.java b/src/main/java/net/raphimc/noteblocklib/player/SongPlayer.java index bf1d124..3ee6f00 100644 --- a/src/main/java/net/raphimc/noteblocklib/player/SongPlayer.java +++ b/src/main/java/net/raphimc/noteblocklib/player/SongPlayer.java @@ -17,91 +17,186 @@ */ package net.raphimc.noteblocklib.player; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import net.raphimc.noteblocklib.model.SongView; +import net.raphimc.noteblocklib.model.Note; +import net.raphimc.noteblocklib.model.Song; +import net.raphimc.noteblocklib.util.TimerHack; +import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class SongPlayer { - - private final SongView songView; - protected final SongPlayerCallback callback; +public abstract class SongPlayer { + private Song song; private ScheduledExecutorService scheduler; + private ScheduledFuture tickTask; + private float ticksPerSecond; private int tick; private boolean paused; - public SongPlayer(final SongView songView, final SongPlayerCallback callback) { - this.songView = songView; - this.callback = callback; + public SongPlayer(final Song song) { + this.song = song; + } + + /** + * Starts playing the song from the beginning. + */ + public void start() { + this.start(0); + } + + /** + * Starts playing the song from the beginning. + * @param delay The delay in milliseconds before starting the song. + */ + public void start(final int delay) { + if (this.isRunning()) this.stop(); + + this.ticksPerSecond = this.song.getTempoEvents().get(0); + this.tick = 0; + + TimerHack.ensureRunning(); + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + final Thread thread = new Thread(r, "NoteBlockLib Song Player - " + this.song.getTitleOrFileNameOr("No Title")); + thread.setPriority(Thread.NORM_PRIORITY + 1); + thread.setDaemon(true); + return thread; + }); + this.createTickTask(TimeUnit.MILLISECONDS.toNanos(delay)); } - public SongView getSongView() { - return this.songView; + /** + * Stops playing the song. + */ + public void stop() { + if (!this.isRunning()) return; + + this.scheduler.shutdownNow(); + try { + this.scheduler.awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + this.scheduler = null; + this.tickTask = null; + this.paused = false; } + /** + * @return Whether the player is in the running state (playing or paused). + */ public boolean isRunning() { return this.scheduler != null && !this.scheduler.isTerminated(); } + /** + * @return The song that is being played. + */ + public Song getSong() { + return this.song; + } + + /** + * Sets the song that should be played.
+ * Can be called in {@link #onFinished()}. + * @param song The song to play. + */ + protected void setSong(final Song song) { + this.song = song; + } + + /** + * @return The current tempo in ticks per second. + */ + public float getCurrentTicksPerSecond() { + return this.ticksPerSecond; + } + + /** + * @return The current tick. + */ public int getTick() { return this.tick; } + /** + * Sets the current tick. + * @param tick The tick to set. + */ public void setTick(final int tick) { this.tick = tick; } - public void play() { - this.paused = false; - if (this.isRunning()) this.stop(); - - this.scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("Song Player - " + this.songView.getTitle()).setDaemon(true).build()); - this.scheduler.scheduleAtFixedRate(this::tick, 0, (long) (1_000_000_000D / this.songView.getSpeed()), TimeUnit.NANOSECONDS); + /** + * @return The current playback position in milliseconds. + */ + public int getMillisecondPosition() { + return this.song.tickToMilliseconds(this.tick); } - public void setPaused(final boolean paused) { - this.paused = paused; + /** + * Sets the current playback position in milliseconds. + * @param milliseconds The time to set the playback position to. + */ + public void setMillisecondPosition(final int milliseconds) { + this.tick = this.song.millisecondsToTick(milliseconds); } + /** + * @return Whether the player is paused. + */ public boolean isPaused() { return this.paused; } - public void stop() { - if (!this.isRunning()) return; + /** + * Pauses or resumes the player. + * @param paused Whether the player should be paused. + */ + public void setPaused(final boolean paused) { + this.paused = paused; + } - this.scheduler.shutdownNow(); - try { - this.scheduler.awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException ignored) { + /** + * Create the internal tick task. + * @param initialDelay The initial delay in nanoseconds. + */ + protected void createTickTask(final long initialDelay) { + if (this.tickTask != null) { + this.tickTask.cancel(false); } - this.scheduler = null; - this.tick = 0; - this.paused = false; + this.tickTask = this.scheduler.scheduleAtFixedRate(this::tick, initialDelay, (long) (1_000_000_000D / this.ticksPerSecond), TimeUnit.NANOSECONDS); } + /** + * Called every tick to play the notes. + */ protected void tick() { try { - if (this.paused || !this.callback.shouldTick()) { + if (!this.preTick()) { return; } + try { + if (this.paused) { + return; + } - if (this.tick >= this.songView.getLength()) { - if (this.callback.shouldLoop()) { - this.tick = -this.callback.getLoopDelay(); - } else { - this.callback.onFinished(); + this.playNotes(this.song.getNotes().getOrEmpty(this.tick)); + + this.tick++; + if (this.tick >= this.song.getNotes().getLengthInTicks()) { this.stop(); + this.onFinished(); return; } + if (this.ticksPerSecond != this.song.getTempoEvents().getEffectiveTempo(this.tick)) { + this.ticksPerSecond = this.song.getTempoEvents().getEffectiveTempo(this.tick); + this.createTickTask((long) (1_000_000_000D / this.ticksPerSecond)); + } + } finally { + this.postTick(); } - - this.callback.playNotes(this.songView.getNotesAtTick(this.tick)); - - this.tick++; } catch (Throwable e) { if (e.getCause() instanceof InterruptedException) return; @@ -110,4 +205,30 @@ protected void tick() { } } + /** + * Called before each tick (Even when paused). + * @return Whether the tick should be executed. + */ + protected boolean preTick() { + return true; + } + + /** + * Plays the notes. + * @param notes The notes to play. + */ + protected abstract void playNotes(final List notes); + + /** + * Called when the song has finished playing. + */ + protected void onFinished() { + } + + /** + * Called after each tick. + */ + protected void postTick() { + } + } diff --git a/src/main/java/net/raphimc/noteblocklib/util/Instrument.java b/src/main/java/net/raphimc/noteblocklib/util/Instrument.java deleted file mode 100644 index 55b886d..0000000 --- a/src/main/java/net/raphimc/noteblocklib/util/Instrument.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.util; - -public enum Instrument { - - HARP(0, 0, 0, "block.note_block.harp"), - BASS(1, 4, 4, "block.note_block.bass"), - BASS_DRUM(2, 1, 1, "block.note_block.basedrum"), - SNARE(3, 2, 2, "block.note_block.snare"), - HAT(4, 3, 3, "block.note_block.hat"), - GUITAR(5, 7, 8, "block.note_block.guitar"), - FLUTE(6, 5, 6, "block.note_block.flute"), - BELL(7, 6, 5, "block.note_block.bell"), - CHIME(8, 8, 7, "block.note_block.chime"), - XYLOPHONE(9, 9, 9, "block.note_block.xylophone"), - IRON_XYLOPHONE(10, 10, 10, "block.note_block.iron_xylophone"), - COW_BELL(11, 11, 11, "block.note_block.cow_bell"), - DIDGERIDOO(12, 12, 12, "block.note_block.didgeridoo"), - BIT(13, 13, 13, "block.note_block.bit"), - BANJO(14, 14, 14, "block.note_block.banjo"), - PLING(15, 15, 15, "block.note_block.pling"); - - private final byte nbsId; - private final byte mcId; - private final byte mcbeId; - private final String mcSoundName; - - Instrument(final int nbsId, final int mcId, final int mcbeId, final String mcSoundName) { - this.nbsId = (byte) nbsId; - this.mcId = (byte) mcId; - this.mcbeId = (byte) mcbeId; - this.mcSoundName = mcSoundName; - } - - public byte nbsId() { - return this.nbsId; - } - - public byte mcId() { - return this.mcId; - } - - public byte mcbeId() { - return this.mcbeId; - } - - public String mcSoundName() { - return this.mcSoundName; - } - - public static Instrument fromNbsId(final byte nbsId) { - for (final Instrument instrument : Instrument.values()) { - if (instrument.nbsId == nbsId) { - return instrument; - } - } - return null; - } - - public static Instrument fromMcId(final byte mcId) { - for (final Instrument instrument : Instrument.values()) { - if (instrument.mcId == mcId) { - return instrument; - } - } - return null; - } - - public static Instrument fromMcbeId(final byte mcbeId) { - for (final Instrument instrument : Instrument.values()) { - if (instrument.mcbeId == mcbeId) { - return instrument; - } - } - return null; - } - - public static Instrument fromMcSoundName(final String mcSoundName) { - for (final Instrument instrument : Instrument.values()) { - if (instrument.mcSoundName.equals(mcSoundName)) { - return instrument; - } - } - return null; - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/util/MinecraftDefinitions.java b/src/main/java/net/raphimc/noteblocklib/util/MinecraftDefinitions.java deleted file mode 100644 index 8a42a2c..0000000 --- a/src/main/java/net/raphimc/noteblocklib/util/MinecraftDefinitions.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib - * Copyright (C) 2022-2025 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package net.raphimc.noteblocklib.util; - -import net.raphimc.noteblocklib.format.nbs.NbsDefinitions; -import net.raphimc.noteblocklib.format.nbs.model.NbsNote; -import net.raphimc.noteblocklib.model.Note; - -import java.util.HashMap; -import java.util.Map; - -public class MinecraftDefinitions { - - public static final int MC_LOWEST_KEY = 33; - public static final int MC_HIGHEST_KEY = 57; - public static final int MC_KEYS = NbsDefinitions.KEYS_PER_OCTAVE * 2; - - // Instrument -> [lower shifts, upper shifts] - private static final Map INSTRUMENT_SHIFTS = new HashMap<>(); - - static { - INSTRUMENT_SHIFTS.put(Instrument.HARP, new Instrument[][]{new Instrument[]{Instrument.BASS}, new Instrument[]{Instrument.BELL}}); - INSTRUMENT_SHIFTS.put(Instrument.BASS, new Instrument[][]{new Instrument[0], new Instrument[]{Instrument.HARP, Instrument.BELL}}); - INSTRUMENT_SHIFTS.put(Instrument.BASS_DRUM, new Instrument[][]{new Instrument[0], new Instrument[]{Instrument.SNARE}}); - INSTRUMENT_SHIFTS.put(Instrument.SNARE, new Instrument[][]{new Instrument[]{Instrument.BASS_DRUM}, new Instrument[]{Instrument.HAT}}); - INSTRUMENT_SHIFTS.put(Instrument.HAT, new Instrument[][]{new Instrument[]{Instrument.BASS_DRUM}, new Instrument[]{Instrument.CHIME}}); - INSTRUMENT_SHIFTS.put(Instrument.GUITAR, new Instrument[][]{new Instrument[]{Instrument.BASS}, new Instrument[]{Instrument.COW_BELL, Instrument.XYLOPHONE}}); - INSTRUMENT_SHIFTS.put(Instrument.FLUTE, new Instrument[][]{new Instrument[]{Instrument.DIDGERIDOO}, new Instrument[]{Instrument.BELL, Instrument.CHIME}}); - INSTRUMENT_SHIFTS.put(Instrument.BELL, new Instrument[][]{new Instrument[]{Instrument.HARP}, new Instrument[0]}); - INSTRUMENT_SHIFTS.put(Instrument.CHIME, new Instrument[][]{new Instrument[]{Instrument.BELL}, new Instrument[0]}); - INSTRUMENT_SHIFTS.put(Instrument.XYLOPHONE, new Instrument[][]{new Instrument[]{Instrument.COW_BELL}, new Instrument[]{Instrument.CHIME}}); - INSTRUMENT_SHIFTS.put(Instrument.IRON_XYLOPHONE, new Instrument[][]{new Instrument[]{Instrument.BASS}, new Instrument[]{Instrument.BELL}}); - INSTRUMENT_SHIFTS.put(Instrument.COW_BELL, new Instrument[][]{new Instrument[]{Instrument.GUITAR}, new Instrument[]{Instrument.XYLOPHONE}}); - INSTRUMENT_SHIFTS.put(Instrument.DIDGERIDOO, new Instrument[][]{new Instrument[]{Instrument.BASS}, new Instrument[]{Instrument.FLUTE, Instrument.BELL}}); - INSTRUMENT_SHIFTS.put(Instrument.BIT, new Instrument[][]{new Instrument[]{Instrument.DIDGERIDOO}, new Instrument[]{Instrument.BELL}}); - INSTRUMENT_SHIFTS.put(Instrument.BANJO, new Instrument[][]{new Instrument[]{Instrument.DIDGERIDOO}, new Instrument[]{Instrument.BELL}}); - INSTRUMENT_SHIFTS.put(Instrument.PLING, new Instrument[][]{new Instrument[]{Instrument.BASS}, new Instrument[]{Instrument.BELL}}); - } - - /** - * @param mcKey The key of the note in the minecraft id system - * @return The pitch of the note (Between 0 and 2 for input between 0 and 24) - */ - public static float mcKeyToMcPitch(final int mcKey) { - return (float) Math.pow(2D, (double) (mcKey - NbsDefinitions.KEYS_PER_OCTAVE) / NbsDefinitions.KEYS_PER_OCTAVE); - } - - /** - * @param mcPitch The pitch of the note (Between 0 and 2) - * @return The key of the note in the minecraft id system (Between 0 and 24 if input between 0 and 2) - */ - public static int mcPitchToMcKey(final float mcPitch) { - return (int) Math.round(Math.log(mcPitch) / Math.log(2D) * NbsDefinitions.KEYS_PER_OCTAVE + NbsDefinitions.KEYS_PER_OCTAVE); - } - - /** - * @param nbsPitch The pitch of the note (Between 0 and 2400) - * @return The pitch of the note (Between 0 and 2 for input between 0 and 2400) - */ - public static float nbsPitchToMcPitch(final int nbsPitch) { - return (float) Math.pow(2D, ((nbsPitch / 100F) - MC_LOWEST_KEY - NbsDefinitions.KEYS_PER_OCTAVE) / NbsDefinitions.KEYS_PER_OCTAVE); - } - - /** - * Converts a key from the NBS system to the minecraft system - * - * @param nbsKey The key of the note in the NBS system - * @return The key of the note in the minecraft id system - */ - public static int nbsKeyToMcKey(final int nbsKey) { - return nbsKey - MC_LOWEST_KEY; - } - - /** - * Converts a key from the minecraft system to the NBS system - * - * @param mcKey The key of the note in the minecraft id system - * @return The key of the note in the NBS system - */ - public static int mcKeyToNbsKey(final int mcKey) { - return mcKey + MC_LOWEST_KEY; - } - - /** - * @param nbsKey The key of the note in the NBS system - * @return If the note is outside the minecraft octave range - */ - public static boolean isOutsideOctaveRange(final int nbsKey) { - return nbsKey < MC_LOWEST_KEY || nbsKey > MC_HIGHEST_KEY; - } - - /** - * Clamps the key of the note to fall within minecraft octave range.
- * Any key below 33 will be set to 33, and any key above 57 will be set to 57. - * - * @param note The note to clamp - */ - public static void clampNoteKey(final Note note) { - if (note instanceof NbsNote) { - NbsDefinitions.applyPitchToKey((NbsNote) note); - } - note.setKey((byte) Math.max(MC_LOWEST_KEY, Math.min(MC_HIGHEST_KEY, note.getKey()))); - ensureNbsPitchWithinOctaveRange(note); - } - - /** - * Transposes the key of the note to fall within minecraft octave range.
- * Any key below 33 will be transposed up an octave, and any key above 57 will be transposed down an octave. - * - * @param note The note to transpose - */ - public static void transposeNoteKey(final Note note) { - transposeNoteKey(note, NbsDefinitions.KEYS_PER_OCTAVE); - } - - /** - * Transposes the key of the note to fall within minecraft octave range.
- * Any key below 33 will be transposed up by transposeAmount, and any key above 57 will be transposed down by transposeAmount. - * - * @param note The note to transpose - * @param transposeAmount The amount of keys to transpose by - */ - public static void transposeNoteKey(final Note note, final int transposeAmount) { - if (note instanceof NbsNote) { - NbsDefinitions.applyPitchToKey((NbsNote) note); - } - byte nbsKey = note.getKey(); - while (nbsKey < MC_LOWEST_KEY) { - nbsKey += transposeAmount; - } - while (nbsKey > MC_HIGHEST_KEY) { - nbsKey -= transposeAmount; - } - note.setKey(nbsKey); - ensureNbsPitchWithinOctaveRange(note); - } - - /** - * "Transposes" the key of the note by shifting the instrument to a higher or lower sounding one.
- * This often sounds the best of the three methods as it keeps the musical key the same and only changes the instrument.
- * The note might still be slightly outside of the minecraft octave range. Use one of the other methods to fix this. Clamp is recommended. - * - * @param note The note to transpose - */ - public static void instrumentShiftNote(final Note note) { - Instrument instrument = note.getInstrument(); - if (instrument == null) { // Custom instrument - return; - } - - final Instrument[][] shifts = INSTRUMENT_SHIFTS.get(instrument); - if (shifts == null) { // No shifts defined for this instrument - return; - } - - if (note instanceof NbsNote) { - NbsDefinitions.applyPitchToKey((NbsNote) note); - } - - byte nbsKey = note.getKey(); - int downShifts = 0; - while (nbsKey < MC_LOWEST_KEY && downShifts < shifts[0].length) { - instrument = shifts[0][downShifts++]; - nbsKey += MinecraftDefinitions.MC_KEYS; - } - - int upShifts = 0; - while (nbsKey > MC_HIGHEST_KEY && upShifts < shifts[1].length) { - instrument = shifts[1][upShifts++]; - nbsKey -= MinecraftDefinitions.MC_KEYS; - } - - note.setInstrument(instrument); - note.setKey(nbsKey); - ensureNbsPitchWithinOctaveRange(note); - } - - /** - * Returns the octave delta to use as a suffix for the sound name and modifies the note key to be within the Minecraft octave range.
- * The octave delta value has to be appended to the sound name to play the note at the correct pitch. (For example: "block.note_block.harp_" + octaveDelta)
- * Link to the resource pack: Extended Notes - * - * @param note The note to modify - * @return The octave delta to use as a suffix for the sound name - */ - public static int applyExtendedNotesResourcePack(final Note note) { - int octavesDelta = 0; - while (note.getKey() < MC_LOWEST_KEY) { - note.setKey((byte) (note.getKey() + MC_KEYS)); - octavesDelta--; - } - while (note.getKey() > MC_HIGHEST_KEY) { - note.setKey((byte) (note.getKey() - MC_KEYS)); - octavesDelta++; - } - if (note instanceof NbsNote) { - final NbsNote nbsNote = (NbsNote) note; - if (nbsNote.getPitch() != 0) { - if (nbsNote.getPitch() < 0 && nbsNote.getKey() == MC_LOWEST_KEY) { - nbsNote.setKey((byte) (nbsNote.getKey() + MC_KEYS)); - octavesDelta--; - } else if (nbsNote.getPitch() > 0 && nbsNote.getKey() == MC_HIGHEST_KEY) { - nbsNote.setKey((byte) (nbsNote.getKey() - MC_KEYS)); - octavesDelta++; - } - } - } - - return octavesDelta; - } - - private static void ensureNbsPitchWithinOctaveRange(final Note note) { - if (note instanceof NbsNote) { - final NbsNote nbsNote = (NbsNote) note; - if (nbsNote.getPitch() != 0) { - if (nbsNote.getPitch() < 0 && nbsNote.getKey() == MinecraftDefinitions.MC_LOWEST_KEY) { - nbsNote.setPitch((short) 0); - } else if (nbsNote.getPitch() > 0 && nbsNote.getKey() == MinecraftDefinitions.MC_HIGHEST_KEY) { - nbsNote.setPitch((short) 0); - } - } - } - } - -} diff --git a/src/main/java/net/raphimc/noteblocklib/util/SongResampler.java b/src/main/java/net/raphimc/noteblocklib/util/SongResampler.java index e3366d0..a5d41a1 100644 --- a/src/main/java/net/raphimc/noteblocklib/util/SongResampler.java +++ b/src/main/java/net/raphimc/noteblocklib/util/SongResampler.java @@ -17,10 +17,8 @@ */ package net.raphimc.noteblocklib.util; -import net.raphimc.noteblocklib.format.nbs.NbsSong; -import net.raphimc.noteblocklib.format.nbs.model.NbsNote; import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.model.SongView; +import net.raphimc.noteblocklib.model.Song; import java.util.*; @@ -30,108 +28,63 @@ public class SongResampler { * Changes the tick speed (sample rate) of a song, without changing the musical speed or length.
* Changing the speed to a lower one than original will result in a loss of timing precision. * - * @param songView The song view - * @param newSpeed The new tick speed (Ticks per second) - * @param The note type + * @param song The song + * @param newTempo The new tick speed (Ticks per second) */ - public static void changeTickSpeed(final SongView songView, final float newSpeed) { - final float divider = songView.getSpeed() / newSpeed; + public static void changeTickSpeed(final Song song, final float newTempo) { + precomputeTempoEvents(song); // Ensure song has static tempo + + final float divider = song.getTempoEvents().get(0) / newTempo; if (divider == 1F) return; - final Map> newNotes = new TreeMap<>(); - for (Map.Entry> entry : songView.getNotes().entrySet()) { - newNotes.computeIfAbsent(Math.round(entry.getKey() / divider), k -> new ArrayList<>()).addAll(entry.getValue()); + final Map> newNotes = new HashMap<>(); + for (int tick : song.getNotes().getTicks()) { + newNotes.computeIfAbsent(Math.round(tick / divider), k -> new ArrayList<>()).addAll(song.getNotes().get(tick)); } - songView.setNotes(newNotes); - songView.setSpeed(newSpeed); - songView.recalculateLength(); - } - - /** - * Applies the undocumented tempo changers from Note Block Studio.
- * Only updates the song view. - * - * @param song The song - */ - public static void applyNbsTempoChangers(final NbsSong song) { - applyNbsTempoChangers(song, song.getView()); + song.getNotes().clear(); + for (Map.Entry> entry : newNotes.entrySet()) { + song.getNotes().set(entry.getKey(), entry.getValue()); + } + song.getTempoEvents().set(0, newTempo); } /** - * Applies the undocumented tempo changers from Note Block Studio. - * + * Converts a song with dynamic tempo changes into one with a static tempo. This allows the song to be played in players which don't support dynamic tempo changes. * @param song The song - * @param view The song view to modify */ - public static void applyNbsTempoChangers(final NbsSong song, final SongView view) { - if (song.getHeader().getVersion() < 4) return; - - final boolean hasTempoChanger = song.getData().getCustomInstruments().stream().anyMatch(ci -> ci.getName().equals("Tempo Changer")); - if (!hasTempoChanger) return; - - final List tempoEvents = new ArrayList<>(); - for (Map.Entry> entry : view.getNotes().entrySet()) { - for (NbsNote note : entry.getValue()) { - if (note.getCustomInstrument() != null && note.getCustomInstrument().getName().equals("Tempo Changer")) { - tempoEvents.add(new TempoEvent(entry.getKey(), Math.abs(note.getPitch()) / 15F)); - entry.getValue().remove(note); - break; - } - } + public static void precomputeTempoEvents(final Song song) { + if (song.getTempoEvents().getTicks().size() <= 1) { + return; // Already static tempo } - if (tempoEvents.isEmpty()) return; - if (tempoEvents.get(0).getTick() != 0) { - tempoEvents.add(0, new TempoEvent(0, view.getSpeed())); - } - tempoEvents.sort(Comparator.comparingInt(TempoEvent::getTick)); - - final Map> newNotes = new TreeMap<>(); - final float newSpeed = tempoEvents.stream().map(TempoEvent::getTicksPerSecond).max(Float::compareTo).orElse(view.getSpeed()); + final float newTempo = song.getTempoEvents().getTempoRange()[1]; // Highest tempo in song + final Map> newNotes = new HashMap<>(); + final Set ticks = new TreeSet<>(song.getNotes().getTicks()); + ticks.addAll(song.getTempoEvents().getTicks()); - double milliTime = 0; int lastTick = 0; - float millisPerTick = tempoEvents.get(0).getMillisPerTick(); - int tempoEventIdx = 1; - for (Map.Entry> entry : view.getNotes().entrySet()) { - while (tempoEventIdx < tempoEvents.size() && entry.getKey() > tempoEvents.get(tempoEventIdx).getTick()) { - final TempoEvent tempoEvent = tempoEvents.get(tempoEventIdx++); - milliTime += (tempoEvent.getTick() - lastTick) * millisPerTick; - lastTick = tempoEvent.getTick(); - millisPerTick = tempoEvent.getMillisPerTick(); + float totalMilliseconds = 0; + for (int tick : ticks) { + final float tps = song.getTempoEvents().getEffectiveTempo(lastTick); + final int ticksInSegment = tick - lastTick; + final float segmentMilliseconds = (ticksInSegment / tps) * 1000F; + totalMilliseconds += segmentMilliseconds; + lastTick = tick; + + final List notes = song.getNotes().get(tick); + if (notes != null) { + final int newTick = Math.round(newTempo * totalMilliseconds / 1000F); + newNotes.computeIfAbsent(newTick, k -> new ArrayList<>()).addAll(notes); } - milliTime += (entry.getKey() - lastTick) * millisPerTick; - lastTick = entry.getKey(); - - newNotes.computeIfAbsent((int) Math.round(milliTime * newSpeed / 1000D), k -> new ArrayList<>()).addAll(entry.getValue()); - } - - view.setNotes(newNotes); - view.setSpeed(newSpeed); - view.recalculateLength(); - } - - private static class TempoEvent { - private final int tick; - private final float ticksPerSecond; - - public TempoEvent(final int tick, final float ticksPerSecond) { - this.tick = tick; - this.ticksPerSecond = ticksPerSecond; - } - - public int getTick() { - return this.tick; - } - - public float getTicksPerSecond() { - return this.ticksPerSecond; } - public float getMillisPerTick() { - return 1000F / this.ticksPerSecond; + song.getNotes().clear(); + for (Map.Entry> entry : newNotes.entrySet()) { + song.getNotes().set(entry.getKey(), entry.getValue()); } + song.getTempoEvents().clear(); + song.getTempoEvents().set(0, newTempo); } } diff --git a/src/main/java/net/raphimc/noteblocklib/util/SongUtil.java b/src/main/java/net/raphimc/noteblocklib/util/SongUtil.java index 2481f49..1587fa1 100644 --- a/src/main/java/net/raphimc/noteblocklib/util/SongUtil.java +++ b/src/main/java/net/raphimc/noteblocklib/util/SongUtil.java @@ -17,150 +17,58 @@ */ package net.raphimc.noteblocklib.util; +import net.raphimc.noteblocklib.data.MinecraftInstrument; import net.raphimc.noteblocklib.format.nbs.model.NbsCustomInstrument; -import net.raphimc.noteblocklib.format.nbs.model.NbsNote; import net.raphimc.noteblocklib.model.Note; -import net.raphimc.noteblocklib.model.NoteWithVolume; -import net.raphimc.noteblocklib.model.SongView; +import net.raphimc.noteblocklib.model.Song; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.Collectors; +import java.text.DecimalFormat; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; public class SongUtil { - /** - * Applies the given consumer to all notes in the song view.
- * This method will also modify the notes of the original song as the view references the original song notes.
- * Use cases for this method can be for example to transpose all notes in a song to be within the minecraft octave range. - * - * @param songView The song view - * @param noteConsumer The note consumer - * @param The note type - */ - public static void applyToAllNotes(final SongView songView, final Consumer noteConsumer) { - songView.getNotes().values().stream().flatMap(Collection::stream).forEach(noteConsumer); - } - - /** - * Applies the given predicate to all notes in the song view.
- * The predicate can return true to break the iteration. - * - * @param songView The song view - * @param notePredicate The note predicate - * @param The note type - */ - public static void iterateAllNotes(final SongView songView, final Predicate notePredicate) { - for (N note : songView.getNotes().values().stream().flatMap(Collection::stream).collect(Collectors.toList())) { - if (notePredicate.test(note)) { - break; - } - } - } + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); /** - * Removes all notes which match the given predicate. - * - * @param songView The song view - * @param notePredicate The predicate - * @param The note type + * @param song The song + * @return True if the song contains notes which are outside the vanilla Minecraft octave range. */ - public static void removeNotesIf(final SongView songView, final Predicate notePredicate) { - for (List list : songView.getNotes().values()) { - list.removeIf(notePredicate); - } - } - - /** - * Removes duplicate notes which are on the same tick.
- * Useful when handling large MIDI files with a lot of duplicate notes. - * - * @param songView The song view - * @param The note type - */ - public static void removeDoubleNotes(final SongView songView) { - for (List list : songView.getNotes().values()) { - final Set set = new HashSet<>(list); - list.clear(); - list.addAll(set); - } - } - - /** - * Removes all notes which have a volume of 0. - * - * @param songView The song view - * @param The note type - */ - public static void removeSilentNotes(final SongView songView) { - removeSilentNotes(songView, 0F); - } - - /** - * Removes all notes which have a volume lower than or equal the given threshold. - * - * @param songView The song view - * @param threshold The threshold - * @param The note type - */ - public static void removeSilentNotes(final SongView songView, final float threshold) { - removeNotesIf(songView, note -> { - if (note instanceof NoteWithVolume) { - return ((NoteWithVolume) note).getVolume() <= threshold; - } else { - return false; - } - }); + public static boolean hasOutsideMinecraftOctaveRangeNotes(final Song song) { + return song.getNotes().testEach(Note::isOutsideMinecraftOctaveRange); } /** * Returns a list of all used vanilla instruments in a song. * - * @param songView The song view - * @param The note type + * @param song The song * @return The used instruments */ - public static Set getUsedVanillaInstruments(final SongView songView) { - final Set usedInstruments = EnumSet.noneOf(Instrument.class); - iterateAllNotes(songView, note -> { - if (note.getInstrument() != null) { - usedInstruments.add(note.getInstrument()); + public static Set getUsedVanillaInstruments(final Song song) { + final Set usedInstruments = EnumSet.noneOf(MinecraftInstrument.class); + song.getNotes().forEach(note -> { + if (note.getInstrument() instanceof MinecraftInstrument) { + usedInstruments.add((MinecraftInstrument) note.getInstrument()); } - return false; }); - return usedInstruments; } /** - * Returns a list of all used custom instruments in a song. + * Returns a list of all used NBS custom instruments in a song. * - * @param songView The song view - * @param The note type + * @param song The song * @return The used custom instruments */ - public static Set getUsedCustomInstruments(final SongView songView) { + public static Set getUsedNbsCustomInstruments(final Song song) { final Set usedInstruments = new HashSet<>(); - iterateAllNotes(songView, note -> { - if (note instanceof NbsNote && ((NbsNote) note).getCustomInstrument() != null) { - usedInstruments.add(((NbsNote) note).getCustomInstrument()); + song.getNotes().forEach(note -> { + if (note.getInstrument() instanceof NbsCustomInstrument) { + usedInstruments.add((NbsCustomInstrument) note.getInstrument()); } - return false; }); - return usedInstruments; } - /** - * Returns the total amount of notes in a song. - * - * @param songView The song view - * @param The note type - * @return The note count - */ - public static long getNoteCount(final SongView songView) { - return songView.getNotes().values().stream().mapToLong(List::size).sum(); - } - } diff --git a/src/main/java/net/raphimc/noteblocklib/model/NoteWithPanning.java b/src/main/java/net/raphimc/noteblocklib/util/TimerHack.java similarity index 55% rename from src/main/java/net/raphimc/noteblocklib/model/NoteWithPanning.java rename to src/main/java/net/raphimc/noteblocklib/util/TimerHack.java index 1e5c06b..acefa2d 100644 --- a/src/main/java/net/raphimc/noteblocklib/model/NoteWithPanning.java +++ b/src/main/java/net/raphimc/noteblocklib/util/TimerHack.java @@ -15,18 +15,28 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocklib.model; +package net.raphimc.noteblocklib.util; -public interface NoteWithPanning { +public class TimerHack { - /** - * @return The panning of the note, from -100% (left) to 100% (right). Where 0% is centered. - */ - float getPanning(); + private static Thread THREAD; /** - * @param panning The panning of the note, from -100% (left) to 100% (right). Where 0% is centered. + * Starts a thread which indefinitely sleeps to force the JVM to enable high resolution timers on Windows. */ - void setPanning(final float panning); + public static synchronized void ensureRunning() { + if (THREAD == null) { + THREAD = new Thread(() -> { + while (true) { + try { + Thread.sleep(Integer.MAX_VALUE); + } catch (InterruptedException ignored) { + } + } + }, "NoteBlockLib-TimerHack"); + THREAD.setDaemon(true); + THREAD.start(); + } + } }