Skip to content

Commit

Permalink
Added support for writing MCSP2 and TXT files
Browse files Browse the repository at this point in the history
  • Loading branch information
RaphiMC committed Jan 14, 2025
1 parent 598535d commit dc18ca8
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 11 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, .mid, .txt, .mcsp, .mcsp2 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
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
import net.raphimc.noteblocklib.format.SongFormat;
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 java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -96,6 +100,10 @@ public static void writeSong(final Song song, final OutputStream os) throws Exce
try {
if (song instanceof NbsSong) {
NbsIo.writeSong((NbsSong) song, os);
} else if (song instanceof McSp2Song) {
McSp2Io.writeSong((McSp2Song) song, os);
} else if (song instanceof TxtSong) {
TxtIo.writeSong((TxtSong) song, os);
} else {
throw new Exception("Unsupported song format for writing: " + song.getClass().getSimpleName());
}
Expand All @@ -110,6 +118,10 @@ 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ 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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<MinecraftInstrument> 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<Note> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

public class McSp2Definitions {

public static final int MIN_TEMPO = 1;
public static final int MAX_TEMPO = 20;

public static final Pattern NOTE_DATA_PATTERN = Pattern.compile("(\\d+)?>(.)");
public static final String NOTE_DATA_MAPPING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!§½#£¤$%&/{[(])=}?\\+´`^~¨*'.;,:-_<µ€ÌìíÍïÏîÎóÓòÒöÖåÅäÄñÑõÕúÚùÙüûÜÛéÉèÈêÊë";

Expand Down
58 changes: 56 additions & 2 deletions src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Io.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song;
import net.raphimc.noteblocklib.model.Note;

import java.io.BufferedInputStream;
import java.io.InputStream;
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 {
Expand Down Expand Up @@ -97,4 +97,58 @@ public static McSp2Song readSong(final InputStream is, final String fileName) {
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<Integer, Map<Integer, McSp2Note>> notes = new TreeMap<>();
for (Map.Entry<Integer, McSp2Layer> layerEntry : song.getLayers().entrySet()) {
for (Map.Entry<Integer, McSp2Note> noteEntry : layerEntry.getValue().getNotes().entrySet()) {
notes.computeIfAbsent(noteEntry.getKey(), k -> new TreeMap<>()).put(layerEntry.getKey(), noteEntry.getValue());
}
}

int lastTick = 0;
for (Map.Entry<Integer, Map<Integer, McSp2Note>> 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<Integer, McSp2Note> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ public static NbsSong createSong(final Song song) {
final List<Note> notes = song.getNotes().get(tick);
for (int i = 0; i < notes.size(); i++) {
final Note note = notes.get(i);
final NbsLayer nbsLayer = newSong.getLayers().computeIfAbsent(i, k -> new NbsLayer());

final NbsNote nbsNote = new NbsNote();
if (note.getInstrument() instanceof MinecraftInstrument) {
nbsNote.setInstrument(((MinecraftInstrument) note.getInstrument()).nbsId());
Expand All @@ -57,12 +55,15 @@ public static NbsSong createSong(final Song song) {
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}

}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
package net.raphimc.noteblocklib.format.txt;

public class TxtDefinitions {

public static final int TEMPO = 20;

}
25 changes: 20 additions & 5 deletions src/main/java/net/raphimc/noteblocklib/format/txt/TxtIo.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
import net.raphimc.noteblocklib.format.txt.model.TxtSong;
import net.raphimc.noteblocklib.model.Note;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -63,7 +60,7 @@ public static TxtSong readSong(final InputStream is, final String fileName) thro
}

{ // Fill generalized song structure with data
song.getTempoEvents().set(0, 20);
song.getTempoEvents().set(0, TxtDefinitions.TEMPO);
for (Map.Entry<Integer, List<TxtNote>> entry : notes.entrySet()) {
for (TxtNote txtNote : entry.getValue()) {
final Note note = new Note();
Expand All @@ -77,4 +74,22 @@ public static TxtSong readSong(final InputStream is, final String fileName) thro
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<Integer, List<TxtNote>> entry : song.getTxtNotes().entrySet()) {
for (TxtNote txtNote : entry.getValue()) {
writer.write(entry.getKey() + ":" + txtNote.getKey() + ":" + txtNote.getInstrument() + "\n");
}
}

writer.flush();
}

}

0 comments on commit dc18ca8

Please sign in to comment.