Skip to content

Commit 5f0570e

Browse files
committed
Added support for writing MCSP2 and TXT files
1 parent 788cfd3 commit 5f0570e

File tree

10 files changed

+270
-11
lines changed

10 files changed

+270
-11
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ To use NoteBlockLib in your application, check out the [Usage](#usage) section.
55
For a reference implementation of NoteBlockLib, check out [NoteBlockTool](https://github.com/RaphiMC/NoteBlockTool).
66

77
## Features
8-
- Reads .nbs, .mid, .txt, .mcsp, .mcsp2 and .notebot files
9-
- Can convert all of the above to .nbs
8+
- Supports reading .nbs, .mid, .txt, .mcsp, .mcsp2 and .notebot files
9+
- Supports writing .nbs, .txt and .mcsp2 files
10+
- Can convert all formats to .nbs
1011
- Offers an easy way to play note block songs in your application
1112
- Good MIDI importer
1213
- Supports most MIDI files

src/main/java/net/raphimc/noteblocklib/NoteBlockLib.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
import net.raphimc.noteblocklib.format.SongFormat;
2121
import net.raphimc.noteblocklib.format.futureclient.FutureClientIo;
2222
import net.raphimc.noteblocklib.format.mcsp.McSpIo;
23+
import net.raphimc.noteblocklib.format.mcsp2.McSp2Converter;
2324
import net.raphimc.noteblocklib.format.mcsp2.McSp2Io;
25+
import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song;
2426
import net.raphimc.noteblocklib.format.midi.MidiIo;
2527
import net.raphimc.noteblocklib.format.nbs.NbsConverter;
2628
import net.raphimc.noteblocklib.format.nbs.NbsIo;
2729
import net.raphimc.noteblocklib.format.nbs.model.NbsSong;
30+
import net.raphimc.noteblocklib.format.txt.TxtConverter;
2831
import net.raphimc.noteblocklib.format.txt.TxtIo;
32+
import net.raphimc.noteblocklib.format.txt.model.TxtSong;
2933
import net.raphimc.noteblocklib.model.Song;
3034

3135
import java.io.ByteArrayInputStream;
@@ -96,6 +100,10 @@ public static void writeSong(final Song song, final OutputStream os) throws Exce
96100
try {
97101
if (song instanceof NbsSong) {
98102
NbsIo.writeSong((NbsSong) song, os);
103+
} else if (song instanceof McSp2Song) {
104+
McSp2Io.writeSong((McSp2Song) song, os);
105+
} else if (song instanceof TxtSong) {
106+
TxtIo.writeSong((TxtSong) song, os);
99107
} else {
100108
throw new Exception("Unsupported song format for writing: " + song.getClass().getSimpleName());
101109
}
@@ -110,6 +118,10 @@ public static Song convertSong(final Song song, final SongFormat targetFormat) {
110118
switch (targetFormat) {
111119
case NBS:
112120
return NbsConverter.createSong(song);
121+
case MCSP2:
122+
return McSp2Converter.createSong(song);
123+
case TXT:
124+
return TxtConverter.createSong(song);
113125
default:
114126
throw new IllegalStateException("Unsupported target format: " + targetFormat);
115127
}

src/main/java/net/raphimc/noteblocklib/data/MinecraftDefinitions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public class MinecraftDefinitions {
2929

3030
public static final int MC_LOWEST_MIDI_KEY = 54;
3131
public static final int MC_HIGHEST_MIDI_KEY = 78;
32+
public static final int MC_LOWEST_KEY = 0;
33+
public static final int MC_HIGHEST_KEY = 24;
3234
public static final int MC_KEYS = Constants.KEYS_PER_OCTAVE * 2;
3335

3436
// Instrument -> [lower shifts, upper shifts]
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib
3+
* Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package net.raphimc.noteblocklib.format.mcsp2;
19+
20+
import net.raphimc.noteblocklib.data.MinecraftDefinitions;
21+
import net.raphimc.noteblocklib.data.MinecraftInstrument;
22+
import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Layer;
23+
import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Note;
24+
import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song;
25+
import net.raphimc.noteblocklib.format.nbs.model.NbsSong;
26+
import net.raphimc.noteblocklib.model.Note;
27+
import net.raphimc.noteblocklib.model.Song;
28+
import net.raphimc.noteblocklib.util.SongResampler;
29+
30+
import java.util.Arrays;
31+
import java.util.List;
32+
33+
public class McSp2Converter {
34+
35+
private static final List<MinecraftInstrument> SUPPORTED_INSTRUMENTS = Arrays.asList(MinecraftInstrument.HARP, MinecraftInstrument.BASS, MinecraftInstrument.BASS_DRUM, MinecraftInstrument.SNARE, MinecraftInstrument.HAT);
36+
37+
/**
38+
* Creates a new MCSP2 song from the general data of the given song (Also copies some format specific fields if applicable).
39+
*
40+
* @param song The song
41+
* @return The new MCSP2 song
42+
*/
43+
public static McSp2Song createSong(Song song) {
44+
song = song.copy();
45+
SongResampler.changeTickSpeed(song, Math.max(McSp2Definitions.MIN_TEMPO, Math.min(McSp2Definitions.MAX_TEMPO, Math.round(song.getTempoEvents().get(0)))));
46+
47+
final McSp2Song newSong = new McSp2Song();
48+
newSong.copyGeneralData(song);
49+
newSong.setTempo((int) song.getTempoEvents().get(0));
50+
51+
for (int tick : song.getNotes().getTicks()) {
52+
final List<Note> notes = song.getNotes().get(tick);
53+
for (int i = 0; i < notes.size(); i++) {
54+
final Note note = notes.get(i);
55+
if (note.getInstrument() instanceof MinecraftInstrument && SUPPORTED_INSTRUMENTS.contains((MinecraftInstrument) note.getInstrument()) && note.getVolume() > 0) {
56+
final McSp2Note mcSp2Note = new McSp2Note();
57+
mcSp2Note.setInstrument(((MinecraftInstrument) note.getInstrument()).nbsId());
58+
mcSp2Note.setKey((byte) Math.max(MinecraftDefinitions.MC_LOWEST_KEY, Math.min(MinecraftDefinitions.MC_HIGHEST_KEY, note.getMcKey())));
59+
60+
final McSp2Layer mcSp2Layer = newSong.getLayers().computeIfAbsent(i, k -> new McSp2Layer());
61+
mcSp2Layer.getNotes().put(tick, mcSp2Note);
62+
}
63+
}
64+
}
65+
66+
if (song instanceof McSp2Song) {
67+
final McSp2Song mcSp2Song = (McSp2Song) song;
68+
newSong.setAutoSaveInterval(mcSp2Song.getAutoSaveInterval());
69+
newSong.setAutoSaveInterval((byte) mcSp2Song.getAutoSaveInterval());
70+
newSong.setMinutesSpent(mcSp2Song.getMinutesSpent());
71+
newSong.setLeftClicks(mcSp2Song.getLeftClicks());
72+
newSong.setRightClicks(mcSp2Song.getRightClicks());
73+
newSong.setNoteBlocksAdded(mcSp2Song.getNoteBlocksAdded());
74+
newSong.setNoteBlocksRemoved(mcSp2Song.getNoteBlocksRemoved());
75+
} else if (song instanceof NbsSong) {
76+
final NbsSong nbsSong = (NbsSong) song;
77+
newSong.setAutoSaveInterval(nbsSong.isAutoSave() ? nbsSong.getAutoSaveInterval() : (byte) 0);
78+
newSong.setMinutesSpent(nbsSong.getMinutesSpent());
79+
newSong.setLeftClicks(nbsSong.getLeftClicks());
80+
newSong.setRightClicks(nbsSong.getRightClicks());
81+
newSong.setNoteBlocksAdded(nbsSong.getNoteBlocksAdded());
82+
newSong.setNoteBlocksRemoved(nbsSong.getNoteBlocksRemoved());
83+
}
84+
85+
return newSong;
86+
}
87+
88+
}

src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Definitions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
public class McSp2Definitions {
2323

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

src/main/java/net/raphimc/noteblocklib/format/mcsp2/McSp2Io.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
import net.raphimc.noteblocklib.format.mcsp2.model.McSp2Song;
2424
import net.raphimc.noteblocklib.model.Note;
2525

26-
import java.io.BufferedInputStream;
27-
import java.io.InputStream;
26+
import java.io.*;
2827
import java.nio.charset.StandardCharsets;
2928
import java.util.Map;
3029
import java.util.NoSuchElementException;
3130
import java.util.Scanner;
31+
import java.util.TreeMap;
3232
import java.util.regex.Matcher;
3333

3434
public class McSp2Io {
@@ -97,4 +97,58 @@ public static McSp2Song readSong(final InputStream is, final String fileName) {
9797
return song;
9898
}
9999

100+
public static void writeSong(final McSp2Song song, final OutputStream os) throws IOException {
101+
final OutputStreamWriter writer = new OutputStreamWriter(new BufferedOutputStream(os, BUFFER_SIZE), StandardCharsets.ISO_8859_1);
102+
writer.write("2");
103+
writer.write("|");
104+
writer.write(String.valueOf(song.getAutoSaveInterval()));
105+
writer.write("|");
106+
writer.write(song.getTitleOr("").replace("|", "_"));
107+
writer.write("|");
108+
writer.write(song.getAuthorOr("").replace("|", "_"));
109+
writer.write("|");
110+
writer.write(song.getOriginalAuthorOr("").replace("|", "_"));
111+
writer.write("|");
112+
113+
final Map<Integer, Map<Integer, McSp2Note>> notes = new TreeMap<>();
114+
for (Map.Entry<Integer, McSp2Layer> layerEntry : song.getLayers().entrySet()) {
115+
for (Map.Entry<Integer, McSp2Note> noteEntry : layerEntry.getValue().getNotes().entrySet()) {
116+
notes.computeIfAbsent(noteEntry.getKey(), k -> new TreeMap<>()).put(layerEntry.getKey(), noteEntry.getValue());
117+
}
118+
}
119+
120+
int lastTick = 0;
121+
for (Map.Entry<Integer, Map<Integer, McSp2Note>> tickEntry : notes.entrySet()) {
122+
writer.write("|");
123+
writer.write(String.valueOf(tickEntry.getKey() - lastTick));
124+
lastTick = tickEntry.getKey();
125+
126+
int lastLayer = 0;
127+
final StringBuilder noteData = new StringBuilder();
128+
for (Map.Entry<Integer, McSp2Note> layerEntry : tickEntry.getValue().entrySet()) {
129+
noteData.append(layerEntry.getKey() - lastLayer);
130+
noteData.append('>');
131+
noteData.append(layerEntry.getValue().getInstrumentAndKey());
132+
lastLayer = layerEntry.getKey();
133+
}
134+
writer.write("|");
135+
writer.write(noteData.toString());
136+
}
137+
writer.write("\n");
138+
139+
writer.write(String.valueOf(song.getTempo()));
140+
writer.write("|");
141+
writer.write(String.valueOf(song.getLeftClicks()));
142+
writer.write("|");
143+
writer.write(String.valueOf(song.getRightClicks()));
144+
writer.write("|");
145+
writer.write(String.valueOf(song.getNoteBlocksAdded()));
146+
writer.write("|");
147+
writer.write(String.valueOf(song.getNoteBlocksRemoved()));
148+
writer.write("|");
149+
writer.write(String.valueOf(song.getMinutesSpent()));
150+
151+
writer.flush();
152+
}
153+
100154
}

src/main/java/net/raphimc/noteblocklib/format/nbs/NbsConverter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ public static NbsSong createSong(final Song song) {
4646
final List<Note> notes = song.getNotes().get(tick);
4747
for (int i = 0; i < notes.size(); i++) {
4848
final Note note = notes.get(i);
49-
final NbsLayer nbsLayer = newSong.getLayers().computeIfAbsent(i, k -> new NbsLayer());
50-
5149
final NbsNote nbsNote = new NbsNote();
5250
if (note.getInstrument() instanceof MinecraftInstrument) {
5351
nbsNote.setInstrument(((MinecraftInstrument) note.getInstrument()).nbsId());
@@ -57,12 +55,15 @@ public static NbsSong createSong(final Song song) {
5755
newSong.getCustomInstruments().add(customInstrument);
5856
}
5957
nbsNote.setInstrument((short) (newSong.getVanillaInstrumentCount() + newSong.getCustomInstruments().indexOf(customInstrument)));
58+
} else {
59+
continue;
6060
}
6161
nbsNote.setKey((byte) Math.max(NbsDefinitions.NBS_LOWEST_KEY, Math.min(NbsDefinitions.NBS_HIGHEST_KEY, note.getNbsKey())));
6262
nbsNote.setVelocity((byte) Math.round(note.getVolume() * 100F));
6363
nbsNote.setPanning((short) (Math.round(note.getPanning() * 100F) + NbsDefinitions.CENTER_PANNING));
6464
nbsNote.setPitch((short) Math.round(note.getFractionalKeyPart() * 100F));
6565

66+
final NbsLayer nbsLayer = newSong.getLayers().computeIfAbsent(i, k -> new NbsLayer());
6667
nbsLayer.getNotes().put(tick, nbsNote);
6768
}
6869
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib
3+
* Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package net.raphimc.noteblocklib.format.txt;
19+
20+
import net.raphimc.noteblocklib.data.MinecraftDefinitions;
21+
import net.raphimc.noteblocklib.data.MinecraftInstrument;
22+
import net.raphimc.noteblocklib.format.txt.model.TxtNote;
23+
import net.raphimc.noteblocklib.format.txt.model.TxtSong;
24+
import net.raphimc.noteblocklib.model.Note;
25+
import net.raphimc.noteblocklib.model.Song;
26+
import net.raphimc.noteblocklib.util.SongResampler;
27+
28+
import java.util.ArrayList;
29+
30+
public class TxtConverter {
31+
32+
/**
33+
* Creates a new TXT song from the general data of the given song (Also copies some format specific fields if applicable).
34+
*
35+
* @param song The song
36+
* @return The new TXT song
37+
*/
38+
public static TxtSong createSong(Song song) {
39+
song = song.copy();
40+
SongResampler.changeTickSpeed(song, TxtDefinitions.TEMPO);
41+
42+
final TxtSong newSong = new TxtSong();
43+
newSong.copyGeneralData(song);
44+
45+
for (int tick : song.getNotes().getTicks()) {
46+
for (Note note : song.getNotes().get(tick)) {
47+
if (note.getInstrument() instanceof MinecraftInstrument && note.getVolume() > 0) {
48+
final TxtNote txtNote = new TxtNote();
49+
txtNote.setInstrument(((MinecraftInstrument) note.getInstrument()).mcId());
50+
txtNote.setKey((byte) Math.max(MinecraftDefinitions.MC_LOWEST_KEY, Math.min(MinecraftDefinitions.MC_HIGHEST_KEY, note.getMcKey())));
51+
newSong.getTxtNotes().computeIfAbsent(tick, k -> new ArrayList<>()).add(txtNote);
52+
}
53+
}
54+
}
55+
56+
return newSong;
57+
}
58+
59+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib
3+
* Copyright (C) 2022-2025 RK_01/RaphiMC and contributors
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package net.raphimc.noteblocklib.format.txt;
19+
20+
public class TxtDefinitions {
21+
22+
public static final int TEMPO = 20;
23+
24+
}

src/main/java/net/raphimc/noteblocklib/format/txt/TxtIo.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
import net.raphimc.noteblocklib.format.txt.model.TxtSong;
2323
import net.raphimc.noteblocklib.model.Note;
2424

25-
import java.io.BufferedReader;
26-
import java.io.IOException;
27-
import java.io.InputStream;
28-
import java.io.InputStreamReader;
25+
import java.io.*;
2926
import java.nio.charset.StandardCharsets;
3027
import java.util.ArrayList;
3128
import java.util.List;
@@ -63,7 +60,7 @@ public static TxtSong readSong(final InputStream is, final String fileName) thro
6360
}
6461

6562
{ // Fill generalized song structure with data
66-
song.getTempoEvents().set(0, 20);
63+
song.getTempoEvents().set(0, TxtDefinitions.TEMPO);
6764
for (Map.Entry<Integer, List<TxtNote>> entry : notes.entrySet()) {
6865
for (TxtNote txtNote : entry.getValue()) {
6966
final Note note = new Note();
@@ -77,4 +74,22 @@ public static TxtSong readSong(final InputStream is, final String fileName) thro
7774
return song;
7875
}
7976

77+
public static void writeSong(final TxtSong song, final OutputStream os) throws IOException {
78+
final OutputStreamWriter writer = new OutputStreamWriter(new BufferedOutputStream(os, BUFFER_SIZE), StandardCharsets.UTF_8);
79+
if (song.getTitle() != null) {
80+
writer.write("// Name: " + song.getTitle() + "\n");
81+
}
82+
if (song.getAuthor() != null) {
83+
writer.write("// Author: " + song.getAuthor() + "\n");
84+
}
85+
86+
for (Map.Entry<Integer, List<TxtNote>> entry : song.getTxtNotes().entrySet()) {
87+
for (TxtNote txtNote : entry.getValue()) {
88+
writer.write(entry.getKey() + ":" + txtNote.getKey() + ":" + txtNote.getInstrument() + "\n");
89+
}
90+
}
91+
92+
writer.flush();
93+
}
94+
8095
}

0 commit comments

Comments
 (0)