Skip to content

Commit e9f03aa

Browse files
committed
Added NBS sound stopper support
1 parent 119ce1e commit e9f03aa

9 files changed

Lines changed: 206 additions & 21 deletions

File tree

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

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import net.raphimc.noteblocklib.format.nbs.model.NbsNote;
2525
import net.raphimc.noteblocklib.format.nbs.model.NbsSong;
2626
import net.raphimc.noteblocklib.format.nbs.model.event.NbsShowSavePopupEvent;
27+
import net.raphimc.noteblocklib.format.nbs.model.event.NbsSoundStopperEvent;
2728
import net.raphimc.noteblocklib.format.nbs.model.event.NbsToggleBackgroundAccentEvent;
2829
import net.raphimc.noteblocklib.format.nbs.model.event.NbsToggleRainbowEvent;
2930
import net.raphimc.noteblocklib.model.event.Event;
@@ -32,6 +33,7 @@
3233
import net.raphimc.noteblocklib.util.MathUtil;
3334

3435
import java.util.List;
36+
import java.util.stream.Collectors;
3537

3638
public class NbsConverter {
3739

@@ -47,6 +49,14 @@ public static NbsSong createSong(final Song song) {
4749
newSong.setLength((short) song.getNotes().getLengthInTicks());
4850
newSong.setTempo((short) Math.round(song.getTempoEvents().get(0) * 100F));
4951

52+
final int maxNotesPerGroup = song.getNotes().getTicks().stream()
53+
.map(tick -> song.getNotes().get(tick))
54+
.mapToInt(notes -> notes.stream()
55+
.collect(Collectors.groupingBy(Note::getGroupId, Collectors.counting())).values().stream()
56+
.mapToInt(Long::intValue)
57+
.max().orElse(0))
58+
.max().orElse(0);
59+
5060
for (int tick : song.getNotes().getTicks()) {
5161
final List<Note> notes = song.getNotes().get(tick);
5262
for (int i = 0; i < notes.size(); i++) {
@@ -71,10 +81,38 @@ public static NbsSong createSong(final Song song) {
7181
nbsNote.setVelocity(Math.round(note.getVolume() * 100F));
7282
nbsNote.setPanning(Math.round(note.getPanning() * 100F) + NbsDefinitions.CENTER_PANNING);
7383

74-
final NbsLayer nbsLayer = newSong.getLayers().computeIfAbsent(i, k -> new NbsLayer());
75-
nbsLayer.getNotes().put(tick, nbsNote);
84+
NbsLayer nbsLayer;
85+
if (note.getGroupId() < 0) { // Ungrouped notes
86+
nbsLayer = newSong.getLayers().computeIfAbsent(i, k -> new NbsLayer());
87+
} else {
88+
if (maxNotesPerGroup == 1) { // 1:1 mapping of groups to layers possible
89+
nbsLayer = newSong.getLayers().computeIfAbsent(note.getGroupId(), k -> new NbsLayer());
90+
} else { // Multiple notes with the same group id, so we need to find an empty layer for each note
91+
nbsLayer = null;
92+
for (int offset = 0; offset < maxNotesPerGroup; offset++) {
93+
nbsLayer = newSong.getLayers().computeIfAbsent(note.getGroupId() * maxNotesPerGroup + offset, k -> new NbsLayer());
94+
if (!nbsLayer.getNotes().containsKey(tick)) {
95+
break;
96+
}
97+
}
98+
if (nbsLayer == null || nbsLayer.getNotes().containsKey(tick)) {
99+
throw new IllegalStateException("Couldn't find empty layer for note with group id " + note.getGroupId() + " at tick " + tick + " after checking " + maxNotesPerGroup + " layers, this should never happen");
100+
}
101+
}
102+
}
103+
if (nbsLayer.getNotes().put(tick, nbsNote) != null) {
104+
throw new IllegalStateException("Multiple notes at the same tick and layer after conversion, this should never happen");
105+
}
76106
}
77107
}
108+
// NBS does not allow for gaps in the layer ids, so we need to fill them with empty layers
109+
final int highestLayer = newSong.getLayers().keySet().stream().max(Integer::compareTo).orElse(0);
110+
for (int i = 0; i < highestLayer; i++) {
111+
if (!newSong.getLayers().containsKey(i)) {
112+
newSong.getLayers().put(i, new NbsLayer());
113+
}
114+
}
115+
78116
newSong.getCustomInstruments().replaceAll(NbsCustomInstrument::copy);
79117

80118
if (song.getTempoEvents().getTicks().size() > 1) {
@@ -89,6 +127,30 @@ public static NbsSong createSong(final Song song) {
89127
}
90128
}
91129

130+
if (song.getEvents().testEach(NbsSoundStopperEvent.class::isInstance)) {
131+
final int instrumentId = addCustomInstrument(newSong, NbsDefinitions.SOUND_STOPPER_CUSTOM_INSTRUMENT_NAME);
132+
final NbsLayer layer = addSilentLayer(newSong, NbsDefinitions.SOUND_STOPPER_CUSTOM_INSTRUMENT_NAME);
133+
for (int eventTick : song.getEvents().getTicks()) {
134+
for (Event event : song.getEvents().get(eventTick)) {
135+
if (event instanceof NbsSoundStopperEvent) {
136+
final NbsSoundStopperEvent soundStopperEvent = (NbsSoundStopperEvent) event;
137+
final short startLayer = (short) ((soundStopperEvent.getStartLayer() - 1) * maxNotesPerGroup + 1);
138+
final short endLayer = (short) (soundStopperEvent.getEndLayer() * maxNotesPerGroup);
139+
140+
final NbsNote note = new NbsNote();
141+
note.setInstrument(instrumentId);
142+
note.setKey(NbsDefinitions.F_SHARP_4_KEY);
143+
note.setPitch(startLayer);
144+
note.setPanning(((endLayer & 0xFF) + 100) & 0xFF);
145+
note.setVelocity(((endLayer >> 8) + 100) & 0xFF);
146+
if (layer.getNotes().put(eventTick, note) != null) {
147+
throw new IllegalStateException("Multiple sound stopper events at the same tick and layer after conversion");
148+
}
149+
}
150+
}
151+
}
152+
}
153+
92154
addEvents(song, newSong, NbsToggleRainbowEvent.class, NbsDefinitions.TOGGLE_RAINBOW_CUSTOM_INSTRUMENT_NAME);
93155
addEvents(song, newSong, NbsShowSavePopupEvent.class, NbsDefinitions.SHOW_SAVE_POPUP_CUSTOM_INSTRUMENT_NAME);
94156
addEvents(song, newSong, NbsToggleBackgroundAccentEvent.class, NbsDefinitions.TOGGLE_BACKGROUND_ACCENT_CUSTOM_INSTRUMENT_NAME);
@@ -134,7 +196,9 @@ private static void addEvents(final Song song, final NbsSong newSong, final Clas
134196
final NbsNote note = new NbsNote();
135197
note.setInstrument(instrumentId);
136198
note.setKey(NbsDefinitions.F_SHARP_4_KEY);
137-
layer.getNotes().put(eventTick, note);
199+
if (layer.getNotes().put(eventTick, note) != null) {
200+
throw new IllegalStateException("Multiple events at the same tick and layer after conversion");
201+
}
138202
}
139203
}
140204
}
@@ -152,7 +216,7 @@ private static int addCustomInstrument(final NbsSong song, final String name) {
152216
private static NbsLayer addSilentLayer(final NbsSong song, final String name) {
153217
final NbsLayer layer = new NbsLayer();
154218
layer.setName(name);
155-
layer.setVolume(0);
219+
layer.setStatus(NbsLayer.Status.LOCKED);
156220
song.getLayers().put(song.getLayers().size(), layer);
157221
return layer;
158222
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import net.raphimc.noteblocklib.format.nbs.model.NbsNote;
2727
import net.raphimc.noteblocklib.format.nbs.model.NbsSong;
2828
import net.raphimc.noteblocklib.format.nbs.model.event.NbsShowSavePopupEvent;
29+
import net.raphimc.noteblocklib.format.nbs.model.event.NbsSoundStopperEvent;
2930
import net.raphimc.noteblocklib.format.nbs.model.event.NbsToggleBackgroundAccentEvent;
3031
import net.raphimc.noteblocklib.format.nbs.model.event.NbsToggleRainbowEvent;
3132
import net.raphimc.noteblocklib.model.note.Note;
@@ -158,19 +159,15 @@ public static NbsSong readSong(final InputStream is, final String fileName) thro
158159

159160
song.getTempoEvents().set(0, song.getTempo() / 100F);
160161
final boolean hasSoloLayers = layers.values().stream().anyMatch(layer -> layer.getStatus() == NbsLayer.Status.SOLO);
161-
for (NbsLayer layer : layers.values()) {
162+
for (Map.Entry<Integer, NbsLayer> entry : layers.entrySet()) {
163+
final NbsLayer layer = entry.getValue();
162164
for (Map.Entry<Integer, NbsNote> noteEntry : layer.getNotes().entrySet()) {
163165
final NbsNote nbsNote = noteEntry.getValue();
164166

165167
final Note note = new Note();
168+
note.setGroupId(entry.getKey());
166169
final float effectiveKey = (float) (MathUtil.clamp(nbsNote.getKey(), LOWEST_KEY, HIGHEST_KEY) * PITCHES_PER_KEY + nbsNote.getPitch()) / PITCHES_PER_KEY;
167170
note.setMidiKey(MathUtil.clamp(LOWEST_MIDI_KEY + effectiveKey, MidiDefinitions.LOWEST_KEY, MidiDefinitions.HIGHEST_KEY));
168-
note.setVolume(MathUtil.clamp(Math.min(layer.getVolume() / 100F, 1F) * (nbsNote.getVelocity() / 100F), 0F, 1F));
169-
if (layer.getPanning() == CENTER_PANNING) { // Special case
170-
note.setPanning((nbsNote.getPanning() - CENTER_PANNING) / 100F);
171-
} else {
172-
note.setPanning(((layer.getPanning() - CENTER_PANNING) + (nbsNote.getPanning() - CENTER_PANNING)) / 200F);
173-
}
174171

175172
if (nbsNote.getInstrument() < song.getVanillaInstrumentCount()) {
176173
note.setInstrument(MinecraftInstrument.fromNbsId(nbsNote.getInstrument()));
@@ -188,7 +185,10 @@ public static NbsSong readSong(final InputStream is, final String fileName) thro
188185
}
189186
if (song.getVersion() >= 5) {
190187
if (SOUND_STOPPER_CUSTOM_INSTRUMENT_NAME.equals(nbsCustomInstrument.getName())) {
191-
continue; // TODO: Implement sound stopper support
188+
final short startLayer = (short) Math.max(nbsNote.getPitch(), 0);
189+
final short endLayer = (short) Math.max((short) (((nbsNote.getPanning() + 156) % 256) + ((nbsNote.getVelocity() + 156) % 256) * 256), startLayer);
190+
song.getEvents().add(noteEntry.getKey(), new NbsSoundStopperEvent(startLayer, endLayer));
191+
continue;
192192
}
193193
if (SHOW_SAVE_POPUP_CUSTOM_INSTRUMENT_NAME.equals(nbsCustomInstrument.getName())) {
194194
song.getEvents().add(noteEntry.getKey(), NbsShowSavePopupEvent.INSTANCE);
@@ -212,6 +212,13 @@ public static NbsSong readSong(final InputStream is, final String fileName) thro
212212
}
213213
}
214214

215+
note.setVolume(MathUtil.clamp(Math.min(layer.getVolume() / 100F, 1F) * (nbsNote.getVelocity() / 100F), 0F, 1F));
216+
if (layer.getPanning() == CENTER_PANNING) { // Special case
217+
note.setPanning(MathUtil.clamp((nbsNote.getPanning() - CENTER_PANNING) / 100F, -1F, 1F));
218+
} else {
219+
note.setPanning(MathUtil.clamp(((layer.getPanning() - CENTER_PANNING) + (nbsNote.getPanning() - CENTER_PANNING)) / 200F, -1F, 1F));
220+
}
221+
215222
if (layer.getStatus() == NbsLayer.Status.LOCKED) { // Locked layers are muted
216223
note.setVolume(0F);
217224
} else if (hasSoloLayers && layer.getStatus() != NbsLayer.Status.SOLO) { // Non-solo layers are muted if there are solo layers

src/main/java/net/raphimc/noteblocklib/format/nbs/model/event/NbsShowSavePopupEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class NbsShowSavePopupEvent implements NbsEvent {
2222
public static final NbsShowSavePopupEvent INSTANCE = new NbsShowSavePopupEvent();
2323

2424
@Override
25-
public NbsEvent copy() {
25+
public NbsShowSavePopupEvent copy() {
2626
return this;
2727
}
2828

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* This file is part of NoteBlockLib - https://github.com/RaphiMC/NoteBlockLib
3+
* Copyright (C) 2022-2026 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.nbs.model.event;
19+
20+
import net.raphimc.noteblocklib.model.note.Note;
21+
22+
import java.util.Objects;
23+
24+
/**
25+
* Event, which stops all sounds originating from the specified layers.
26+
*/
27+
public class NbsSoundStopperEvent implements NbsEvent {
28+
29+
private short startLayer;
30+
private short endLayer;
31+
32+
public NbsSoundStopperEvent(final short startLayer, final short endLayer) {
33+
this.setStartLayer(startLayer);
34+
this.setEndLayer(endLayer);
35+
}
36+
37+
/**
38+
* @return The start layer (1 indexed, inclusive) of the layer range whose sounds will be stopped. 0 = stop all layers
39+
*/
40+
public short getStartLayer() {
41+
return this.startLayer;
42+
}
43+
44+
/**
45+
* @param startLayer The start layer (1 indexed, inclusive) of the layer range whose sounds will be stopped. 0 = stop all layers
46+
*/
47+
public void setStartLayer(final short startLayer) {
48+
this.startLayer = startLayer;
49+
}
50+
51+
/**
52+
* @return The end layer (1 indexed, inclusive) of the layer range whose sounds will be stopped.
53+
*/
54+
public short getEndLayer() {
55+
return this.endLayer;
56+
}
57+
58+
/**
59+
* @param endLayer The end layer (1 indexed, inclusive) of the layer range whose sounds will be stopped.
60+
*/
61+
public void setEndLayer(final short endLayer) {
62+
this.endLayer = endLayer;
63+
}
64+
65+
/**
66+
* Checks if the specified note should be stopped by this event.
67+
*
68+
* @param note The note to check.
69+
* @return True if the note should be stopped by this event, false otherwise.
70+
*/
71+
public boolean shouldStop(final Note note) {
72+
if (this.startLayer == 0) {
73+
return true;
74+
}
75+
return note.getGroupId() >= (this.startLayer - 1) && note.getGroupId() <= (this.endLayer - 1);
76+
}
77+
78+
@Override
79+
public NbsSoundStopperEvent copy() {
80+
return new NbsSoundStopperEvent(this.startLayer, this.endLayer);
81+
}
82+
83+
@Override
84+
public boolean equals(final Object o) {
85+
if (o == null || getClass() != o.getClass()) return false;
86+
final NbsSoundStopperEvent that = (NbsSoundStopperEvent) o;
87+
return startLayer == that.startLayer && endLayer == that.endLayer;
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
return Objects.hash(startLayer, endLayer);
93+
}
94+
95+
}

src/main/java/net/raphimc/noteblocklib/format/nbs/model/event/NbsToggleBackgroundAccentEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class NbsToggleBackgroundAccentEvent implements NbsEvent {
2222
public static final NbsToggleBackgroundAccentEvent INSTANCE = new NbsToggleBackgroundAccentEvent();
2323

2424
@Override
25-
public NbsEvent copy() {
25+
public NbsToggleBackgroundAccentEvent copy() {
2626
return this;
2727
}
2828

src/main/java/net/raphimc/noteblocklib/format/nbs/model/event/NbsToggleRainbowEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class NbsToggleRainbowEvent implements NbsEvent {
2525
public static final NbsToggleRainbowEvent INSTANCE = new NbsToggleRainbowEvent();
2626

2727
@Override
28-
public NbsEvent copy() {
28+
public NbsToggleRainbowEvent copy() {
2929
return this;
3030
}
3131

src/main/java/net/raphimc/noteblocklib/model/event/Events.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public List<Event> get(final int tick) {
3030
}
3131

3232
public List<Event> getOrEmpty(final int tick) {
33-
return this.events.getOrDefault(tick, new ArrayList<>());
33+
return this.events.getOrDefault(tick, Collections.emptyList());
3434
}
3535

3636
public void set(final int tick, final List<Event> events) {

src/main/java/net/raphimc/noteblocklib/model/note/Note.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,34 @@
2626

2727
public class Note {
2828

29+
private int groupId;
2930
private Instrument instrument;
3031
private float midiKey;
3132
private float volume;
3233
private float panning;
3334

3435
public Note() {
36+
this.groupId = -1;
3537
this.volume = 1F;
3638
this.panning = 0F;
3739
}
3840

41+
/**
42+
* @return The group ID of the note. Groups are used in events to determine which notes are affected by the event. -1 means that the note is not in any group.
43+
*/
44+
public int getGroupId() {
45+
return this.groupId;
46+
}
47+
48+
/**
49+
* @param groupId The group ID of the note. Groups are used in events to determine which notes are affected by the event. -1 means that the note is not in any group.
50+
* @return this
51+
*/
52+
public Note setGroupId(final int groupId) {
53+
this.groupId = groupId;
54+
return this;
55+
}
56+
3957
/**
4058
* @return The instrument of the note. Default Minecraft instruments are stored in {@link MinecraftInstrument}.
4159
*/
@@ -189,6 +207,7 @@ public boolean isOutsideMinecraftOctaveRange() {
189207

190208
public Note copy() {
191209
final Note copyNote = new Note();
210+
copyNote.groupId = this.groupId;
192211
copyNote.instrument = this.instrument.copy();
193212
copyNote.midiKey = this.midiKey;
194213
copyNote.volume = this.volume;
@@ -197,15 +216,15 @@ public Note copy() {
197216
}
198217

199218
@Override
200-
public boolean equals(Object o) {
219+
public boolean equals(final Object o) {
201220
if (o == null || getClass() != o.getClass()) return false;
202-
Note note = (Note) o;
203-
return midiKey == note.midiKey && Float.compare(volume, note.volume) == 0 && Float.compare(panning, note.panning) == 0 && Objects.equals(instrument, note.instrument);
221+
final Note note = (Note) o;
222+
return groupId == note.groupId && Float.compare(midiKey, note.midiKey) == 0 && Float.compare(volume, note.volume) == 0 && Float.compare(panning, note.panning) == 0 && Objects.equals(instrument, note.instrument);
204223
}
205224

206225
@Override
207226
public int hashCode() {
208-
return Objects.hash(instrument, midiKey, volume, panning);
227+
return Objects.hash(groupId, instrument, midiKey, volume, panning);
209228
}
210229

211230
}

src/main/java/net/raphimc/noteblocklib/model/note/Notes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public List<Note> get(final int tick) {
3333
}
3434

3535
public List<Note> getOrEmpty(final int tick) {
36-
return this.notes.getOrDefault(tick, new ArrayList<>());
36+
return this.notes.getOrDefault(tick, Collections.emptyList());
3737
}
3838

3939
public void set(final int tick, final List<Note> notes) {

0 commit comments

Comments
 (0)