Skip to content

Commit

Permalink
NoteBlockLib v3 rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
RaphiMC committed Jan 1, 2025
1 parent b05410a commit 2e70c66
Show file tree
Hide file tree
Showing 70 changed files with 3,393 additions and 3,591 deletions.
136 changes: 62 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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
- Reads .nbs, .mid, .txt, .mcsp, .mcsp2 and .notebot files
- Can convert all of the above to .nbs
- Offers an easy way to play note block songs in your application
- Good MIDI importer
Expand All @@ -14,7 +14,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
Expand All @@ -34,29 +34,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(<input>, [format])`` method.
Expand All @@ -67,13 +52,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(<song>, <output>)`` method.

### Creating a song
The easiest way to create a song is to create a SongView and then use the ``NoteBlockLib.createSongFromView(<view>, [format])`` method to create a Song from it.
Alternatively you can create a Song directly by using the ``new Song(null, <header>, <data>)`` 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(<song>, <format>)`` 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(<songView>, <callback>)`` 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.
Expand All @@ -84,104 +69,107 @@ 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<Integer, List<Note>> 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<Note> 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().setTempo(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<Note> 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);

// 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();
```
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
94 changes: 43 additions & 51 deletions src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,18 @@
*/
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.McSp2Io;
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.TxtIo;
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;
Expand All @@ -41,86 +39,80 @@

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);
} else if (song instanceof TxtSong) {
bytes = TxtParser.write((TxtSong) song);
NbsIo.writeSong((NbsSong) 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);
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) {
Expand Down
Loading

0 comments on commit 2e70c66

Please sign in to comment.