com.cryptomorin.xseries.NoteBlockMusic Maven / Gradle / Ivy
Show all versions of XSeries Show documentation
/*
* The MIT License (MIT)
*
* Copyright (c) 2024 Crypto Morin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.cryptomorin.xseries;
import com.google.common.base.Strings;
import org.bukkit.Instrument;
import org.bukkit.Location;
import org.bukkit.Note;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
/**
* NoteBlockMusic - Write music scripts for Minecraft.
* You can write small text scripts for Minecraft note blocks
* without needing to use any redstone or building to make your music.
* This class is independent of XSound.
*
* @author Crypto Morin
* @version 3.0.0
* @see Instrument
* @see Note
*/
public final class NoteBlockMusic {
/**
* A list of shortcuts for instruments.
* Full names are also cached.
*
* @since 1.0.0
*/
private static final Map INSTRUMENTS = new HashMap<>(50);
private static final Map INSTRUMENT_TO_SOUND = new EnumMap<>(Instrument.class);
static {
INSTRUMENT_TO_SOUND.put(Instrument.PIANO, XSound.BLOCK_NOTE_BLOCK_HARP);
INSTRUMENT_TO_SOUND.put(Instrument.BASS_DRUM, XSound.BLOCK_NOTE_BLOCK_BASEDRUM);
INSTRUMENT_TO_SOUND.put(Instrument.SNARE_DRUM, XSound.BLOCK_NOTE_BLOCK_SNARE);
INSTRUMENT_TO_SOUND.put(Instrument.STICKS, XSound.BLOCK_NOTE_BLOCK_HAT);
INSTRUMENT_TO_SOUND.put(Instrument.BASS_GUITAR, XSound.BLOCK_NOTE_BLOCK_BASS);
INSTRUMENT_TO_SOUND.put(Instrument.FLUTE, XSound.BLOCK_NOTE_BLOCK_FLUTE);
INSTRUMENT_TO_SOUND.put(Instrument.BELL, XSound.BLOCK_NOTE_BLOCK_BELL);
INSTRUMENT_TO_SOUND.put(Instrument.GUITAR, XSound.BLOCK_NOTE_BLOCK_GUITAR);
INSTRUMENT_TO_SOUND.put(Instrument.CHIME, XSound.BLOCK_NOTE_BLOCK_CHIME);
INSTRUMENT_TO_SOUND.put(Instrument.XYLOPHONE, XSound.BLOCK_NOTE_BLOCK_XYLOPHONE);
INSTRUMENT_TO_SOUND.put(Instrument.IRON_XYLOPHONE, XSound.BLOCK_NOTE_BLOCK_IRON_XYLOPHONE);
INSTRUMENT_TO_SOUND.put(Instrument.COW_BELL, XSound.BLOCK_NOTE_BLOCK_COW_BELL);
INSTRUMENT_TO_SOUND.put(Instrument.DIDGERIDOO, XSound.BLOCK_NOTE_BLOCK_DIDGERIDOO);
INSTRUMENT_TO_SOUND.put(Instrument.BIT, XSound.BLOCK_NOTE_BLOCK_BIT);
INSTRUMENT_TO_SOUND.put(Instrument.BANJO, XSound.BLOCK_NOTE_BLOCK_BANJO);
INSTRUMENT_TO_SOUND.put(Instrument.PLING, XSound.BLOCK_NOTE_BLOCK_PLING);
}
static {
// Based on their XSound equivalent:
INSTRUMENTS.put("HARP", Instrument.PIANO);
INSTRUMENTS.put("BASEDRUM", Instrument.BASS_DRUM);
INSTRUMENTS.put("BASE_DRUM", Instrument.BASS_DRUM);
INSTRUMENTS.put("SNARE", Instrument.SNARE_DRUM);
INSTRUMENTS.put("BASS", Instrument.BASS_GUITAR);
INSTRUMENTS.put("COWBELL", Instrument.COW_BELL);
// Add instrument shortcuts.
for (Instrument instrument : Instrument.values()) {
String name = instrument.name();
INSTRUMENTS.put(name, instrument);
StringBuilder alias = new StringBuilder(String.valueOf(name.charAt(0)));
int index = name.indexOf('_');
if (index != -1) alias.append(name.charAt(index + 1));
if (INSTRUMENTS.putIfAbsent(alias.toString(), instrument) != null) {
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (ch == '_') {
i++;
} else {
alias.append(ch);
if (INSTRUMENTS.putIfAbsent(alias.toString(), instrument) == null) break;
}
}
}
}
}
private NoteBlockMusic() {
}
@Nonnull
public static XSound getSoundFromInstrument(@Nonnull Instrument instrument) {
return INSTRUMENT_TO_SOUND.get(instrument);
}
/**
* Gets a note tone from a character. Can't be optimized further using an array
* since switch statement here can be optimized by JIT even more.
*
* The character paseed to this method is assumed to be uppercase,
* otherwise it needs to be {@code ch & 0x5f} manually.
*
* https://minecraft.wiki/w/Note_Block#Notes
*
* @param ch the character of the note tone.
* @return the note tone or null if not found.
* @since 3.0.0
*/
@Nullable
public static Note.Tone getNoteTone(char ch) {
switch (ch) {
case 'A':
return Note.Tone.A;
case 'B':
return Note.Tone.B;
case 'C':
return Note.Tone.C;
case 'D':
return Note.Tone.D;
case 'E':
return Note.Tone.E;
case 'F':
return Note.Tone.F;
case 'G':
return Note.Tone.G;
default:
return null;
}
}
/**
* A pre-written music script to test with {@link #playMusic(Player, Supplier, String)}
* If you made a cool script using this let me know, I'll put it here.
* You can still give me the script and I'll put it on the Spigot page.
*
* @param player the player to send the notes to.
* @return the async task handling the notes.
* @since 1.0.0
*/
public static CompletableFuture testMusic(@Nonnull Player player) {
return playMusic(player, player::getLocation, // Starting piece of Megalovania (not perfectly toned, it's screwed up)
"PIANO,D,2,100 PIANO,B#1 200 PIANO,F 250 PIANO,E 250 PIANO,B 200 PIANO,A 100 PIANO,B 100 PIANO,E");
}
/**
* Plays a music from a file.
* This file can have YAML comments (#) and empty lines.
*
* @param player the player to play the music to.
* @param location the location to play the notes to.
* @param path the path of the file to read the music notes from.
* @return the async task handling the file operations and music parsers.
* @see #playMusic(Player, Supplier, String)
* @since 1.0.0
*/
public static CompletableFuture fromFile(@Nonnull Player player, @Nonnull Supplier location, @Nonnull Path path) {
return CompletableFuture.runAsync(() -> {
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
parseInstructions(line).play(player, location, true);
}
} catch (IOException ex) {
ex.printStackTrace();
}
});
}
/**
* This is a very special and unique method.
* This method allows you to write your own Minecraft music without needing to use
* redstones and note blocks.
*
* We'll take a whole thread for the music for blocking requests.
* Format:
* Instrument, Tone, Repeat (optional), Repeating Delay (optional, required if Repeat is used) [Next Delay]
* Both delays are in milliseconds.
* Also you can use segments (segment) to repeat a segment multiple times.
*
* Example
* Shortcuts:
* {@code BD,G 20 BD,G 20 BD,G -> BD,G,3,20}
*
* (BD,G,3,20 BG,E,5,10),1000,2 1000 BA,A
*
* Translated:
* Play BASS_DRUM with tone G 3 times every 20 ticks.
* Play BASS_GUITAR with tone E 5 times every 10 ticks.
* Play those ^ again two times with 1 second delay between repeats.
* Wait 1000ms.
* Play BANJO with tone A once.
*
*
* Note Tones
* Available Note Tones: G, A, B, C, D, E, F (Idk why G is the first one) {@link org.bukkit.Note.Tone}
* You can also use sharp or flat tones by using '#' for sharp and '_' for flat e.g. B_ C#
* Octave numbers 1 and 2 can be used.
* C1, C#1, B_1, D_2
*
* Instruments
* Available Instruments: Basically the first letter of every instrument. E.g. {@code BD -> BASS_DRUM}
* You can also use their full name. {@link Instrument}
*
* CompletableFuture
* Warning: Do not use blocking methods such as join() or get()
* You may use cancel() or the then... methods.
*
* @param player in order to play the note we need a player instance. Any player.
* @param location the location to play this note to.
* @param script the music script.
* @return the async task processing the script.
* @see #fromFile(Player, Supplier, Path)
* @since 1.0.0
*/
public static CompletableFuture playMusic(@Nonnull Player player, @Nonnull Supplier location, @Nullable String script) {
// We don't want to mess around in the main thread.
// Sounds are thread-safe.
return CompletableFuture.runAsync(() -> {
if (Strings.isNullOrEmpty(script)) return;
Sequence seq = parseInstructions(script);
seq.play(player, location, true);
}).exceptionally(ex -> {
ex.printStackTrace();
return null;
});
}
public static Sequence parseInstructions(@Nonnull CharSequence script) {
return new InstructionBuilder(script).sequence;
}
/**
* Method used to handle delays of instructions.
* This method should always be called in another thread to
* avoid freezing the main Minecraft thread.
*
* @param fermata (delay) in milliseconds.
*/
private static void sleep(long fermata) {
try {
Thread.sleep(fermata);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Parses a Minecraft {@link Note} with its {@link org.bukkit.Note.Tone}
* With the format: {@literal [pitch][octave]}
*
* Available Note Tones: G, A, B, C, D, E, F (Idk why G is the first one) {@link org.bukkit.Note.Tone}
* You can also use sharp or flat tones by using '#' for sharp and '_' for flat e.g. B_ C#
* Octave numbers 1 and 2 can be used.
* C1, C#1, B_1, D_2
*
* Tones:
*
* - Sharp Notes
* - Flat Notes
* - Natural Notes
*
*
* Pitch
* Octave
*
* @return a note with a tone.
* @since 3.0.0
*/
@Nullable
public static Note parseNote(@Nonnull String note) {
Note.Tone tone = getNoteTone((char) (note.charAt(0) & 0x5f)); // Doesn't matter if it's already uppercase.
if (tone == null) return null;
int len = note.length();
char toneType = ' ';
int octave = 0;
if (len > 1) {
toneType = note.charAt(1);
if (isDigit(toneType)) octave = toneType - '0'; // parseInt for single char
else if (len > 2) {
char octaveDigit = note.charAt(2);
if (isDigit(octaveDigit)) octave = octaveDigit - '0';
}
if (octave < 0 || octave > 2) octave = 0;
}
return toneType == '#' ?
Note.sharp(octave, tone) : toneType == '_' ?
Note.flat(octave, tone) :
Note.natural(octave, tone);
}
/**
* {@link Character#isDigit(char)} won't work perfectly in this case.
*
* @param ch the character to check.
* @return if and only if this character is an English digit number.
* @since 1.2.0
*/
private static boolean isDigit(char ch) {
return ch >= '0' && ch <= '9';
}
@SuppressWarnings("deprecation")
public static float noteToPitch(@Nonnull Note note) {
return (float) Math.pow(2.0D, ((double) note.getId() - 12.0D) / 12.0D);
}
private enum InstructionParserPhase {
NEUTRAL {
@Override
protected InstructionParserPhase next() {
return INSTRUMENT;
}
@Override
protected char checkup(char ch) {
throw new AssertionError("Checkup should not be performed on NEUTRAL instruction parser phase");
}
}, INSTRUMENT {
@Override
protected InstructionParserPhase next() {
return NOTE;
}
@Override
protected char checkup(char ch) {
if (ch >= 'a' && ch <= 'z') return (char) (ch & 0x5f);
return (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '-' ? ch : '\0';
}
}, NOTE {
@Override
protected InstructionParserPhase next() {
return RESTATEMENT;
}
@Override
protected char checkup(char ch) {
if (ch >= 'a' && ch <= 'z') return (char) (ch & 0x5f);
return (ch >= 'A' && ch <= 'Z') || isDigit(ch) || ch == '.' || ch == '_' || ch == '#' ? ch : '\0';
}
}, END_SEQ {
@Override
protected InstructionParserPhase next() {
return RESTATEMENT;
}
@Override
protected char checkup(char ch) {
return 0;
}
}, RESTATEMENT {
@Override
protected InstructionParserPhase next() {
return RESTATEMENT_DELAY;
}
@Override
protected char checkup(char ch) {
return isDigit(ch) ? ch : '\0';
}
}, RESTATEMENT_DELAY {
@Override
protected InstructionParserPhase next() {
return FERMATA;
}
@Override
protected char checkup(char ch) {
return isDigit(ch) ? ch : '\0';
}
}, FERMATA {
@Override
protected InstructionParserPhase next() {
return NEUTRAL;
}
@Override
protected char checkup(char ch) {
return isDigit(ch) ? ch : '\0';
}
};
protected abstract InstructionParserPhase next();
protected abstract char checkup(char ch);
}
@SuppressWarnings("StringBufferField")
private static final class InstructionBuilder {
@Nonnull final CharSequence script;
final int len;
final StringBuilder
instrumentBuilder = new StringBuilder(10),
pitchBuiler = new StringBuilder(3), volumeBuilder = new StringBuilder(3),
restatementBuilder = new StringBuilder(10), restatementDelayBuilder = new StringBuilder(10),
fermataBuilder = new StringBuilder(10);
int i;
boolean isSequence, isBuilding;
Sequence sequence = new Sequence();
InstructionParserPhase phase = InstructionParserPhase.NEUTRAL;
StringBuilder currentBuilder;
public InstructionBuilder(@Nonnull CharSequence script) {
this.script = script;
len = script.length();
for (; i < len; i++) {
char ch = script.charAt(i);
switch (ch) {
case '(':
Sequence parent = new Sequence();
parent.parent = sequence;
sequence = parent;
break;
case ')':
if (sequence.parent == null) err("Cannot find start of the sequence for sequence at: " + i);
buildAndAddInstruction();
sequence = sequence.parent;
prepareHandlers();
phase = InstructionParserPhase.END_SEQ;
isSequence = true;
break;
case ' ':
if (!isBuilding) continue;
isBuilding = false;
switch (phase) {
case FERMATA:
buildAndAddInstruction();
prepareHandlers();
break;
case NOTE:
case RESTATEMENT_DELAY:
phase = InstructionParserPhase.FERMATA;
currentBuilder = fermataBuilder;
break;
}
break;
case ':': // Pitch/Note & Volume Separator
if (phase == InstructionParserPhase.NOTE) currentBuilder = volumeBuilder;
else err("Unexpected ':' pitch-volume separator at " + i + " with current phase: " + phase);
break;
case ',':
switch (phase) {
case INSTRUMENT:
currentBuilder = pitchBuiler;
break;
case NOTE:
case END_SEQ:
currentBuilder = restatementBuilder;
break;
case RESTATEMENT:
currentBuilder = restatementDelayBuilder;
break;
default:
err("Unexpected phase '" + phase + "' at index: " + i);
}
isBuilding = false;
phase = phase.next();
break;
default:
if (phase == InstructionParserPhase.NEUTRAL ||
(canBuildInstructionInPhase() && InstructionParserPhase.INSTRUMENT.checkup(ch) != '\0')) {
currentBuilder = instrumentBuilder;
if (phase == InstructionParserPhase.FERMATA) {
buildAndAddInstruction();
prepareHandlers();
}
phase = InstructionParserPhase.INSTRUMENT;
}
isBuilding = true;
if ((ch = phase.checkup(ch)) == '\0')
err("Unexpected char at index " + i + " with phase " + phase + ": " + script.charAt(i));
currentBuilder.append(ch);
}
}
// if (!isBuilding) buildAndAddInstruction();
buildAndAddInstruction();
sequence = getRoot();
}
private Instruction buildInstruction() {
int fermata = fermataBuilder.length() == 0 ? 0 : Integer.parseInt(fermataBuilder.toString());
int restatement = restatementBuilder.length() == 0 ? 1 : Integer.parseInt(restatementBuilder.toString());
int restatementFermata = restatementDelayBuilder.length() == 0 ? 0 : Integer.parseInt(restatementDelayBuilder.toString());
// if (restatement > 1 && restatementFermata <= 0) throw new IllegalStateException("No restatement fermata found at " + i + " with restatement: " + restatement);
Instruction instruction;
if (isSequence) {
instruction = new Sequence(restatement, restatementFermata, fermata);
} else {
String instrumentStr = instrumentBuilder.toString();
XSound sound;
Instrument instrument = INSTRUMENTS.get(instrumentStr);
if (instrument == null) sound = XSound.matchXSound(instrumentStr).orElse(null);
else sound = getSoundFromInstrument(instrument);
String pitchStr = pitchBuiler.toString();
float pitch;
Note note = parseNote(pitchStr);
if (note == null) pitch = Float.parseFloat(pitchStr);
else pitch = noteToPitch(note);
float volume = 5.0f;
if (volumeBuilder.length() != 0) volume = Float.parseFloat(volumeBuilder.toString());
instruction = new Sound(sound, pitch, volume, restatement, restatementFermata, fermata);
}
return instruction;
}
private void prepareHandlers() {
instrumentBuilder.setLength(0);
pitchBuiler.setLength(0);
volumeBuilder.setLength(0);
restatementBuilder.setLength(0);
restatementDelayBuilder.setLength(0);
fermataBuilder.setLength(0);
phase = InstructionParserPhase.NEUTRAL;
isBuilding = false;
isSequence = false;
}
private boolean canBuildInstructionInPhase() {
switch (phase) {
case RESTATEMENT:
case RESTATEMENT_DELAY:
case FERMATA:
return true;
default:
return false;
}
}
private void buildAndAddInstruction() {
// Sequence previous = sequence.parent == null ? sequence : sequence.parent;
sequence.addInstruction(buildInstruction());
}
private Sequence getRoot() {
Sequence sequence = this.sequence;
while (sequence.parent != null) sequence = sequence.parent;
return sequence;
}
private String illustrateError() {
return '\n' + script.toString() + '\n' + Strings.repeat(" ", i) + '^';
}
private void err(String str) {
throw new IllegalStateException(str + illustrateError());
}
}
/**
* An instruction that produces a sonud which consists of a {@link Instrument} and a {@link Note} with {@link org.bukkit.Note.Tone},
* but without duration or
*
* @since 3.0.0
*/
public static class Sound extends Instruction {
public XSound sound;
/**
* In Minecraft, you have no control over note
* durations.
* A note, has a tone, and a tone is a named pitch
* with a specific (constant) timbre.
*/
public float volume, pitch;
public Sound(Instrument instrument, Note note, float volume, int restatement, int restatementFermata, int fermata) {
super(restatement, restatementFermata, fermata);
this.sound = getSoundFromInstrument(instrument);
this.pitch = noteToPitch(note);
this.volume = volume;
}
public Sound(XSound sound, float pitch, float volume, int restatement, int restatementFermata, int fermata) {
super(restatement, restatementFermata, fermata);
this.sound = sound;
this.pitch = pitch;
this.volume = volume;
}
public void setSound(Instrument instrument) {
this.sound = getSoundFromInstrument(instrument);
}
public void setPitch(Note note) {
this.pitch = noteToPitch(note);
}
@Override
public void play(Player player, Supplier location, boolean playAtLocation) {
org.bukkit.Sound bukkitSound = sound.parseSound();
for (int repeat = restatement; repeat > 0; repeat--) {
Location finalLocation = location.get();
if (bukkitSound != null) {
if (playAtLocation) {
finalLocation.getWorld().playSound(finalLocation, bukkitSound, volume, pitch);
} else {
player.playSound(finalLocation, bukkitSound, volume, pitch);
}
}
if (restatementFermata > 0) sleep(restatementFermata);
}
if (fermata > 0) sleep(fermata);
}
@Override
public String toString() {
return "Sound:{sound=" + sound + ", pitch=" + pitch + ", volume=" + volume +
", restatement=" + restatement + ", restatementFermata=" + restatementFermata + ", fermata=" + fermata + '}';
}
}
/**
* Plays an instrument's notes in an ascending form.
* This method is not really relevant to this utility class, but a nice feature.
*
* @param plugin the plugin handling schedulers.
* @param player the player to play the note from.
* @param playTo the entity to play the note to.
* @param instrument the instrument.
* @param ascendLevel the ascend level of notes. Can only be positive and not higher than 7
* @param delay the delay between each play.
* @return the async task handling the operation.
* @since 2.0.0
*/
@Nonnull
public static BukkitTask playAscendingNote(@Nonnull Plugin plugin, @Nonnull Player player, @Nonnull Entity playTo, @Nonnull Instrument instrument,
int ascendLevel, int delay) {
Objects.requireNonNull(player, "Cannot play note from null player");
Objects.requireNonNull(playTo, "Cannot play note to null entity");
if (ascendLevel <= 0) throw new IllegalArgumentException("Note ascend level cannot be lower than 1");
if (ascendLevel > 7) throw new IllegalArgumentException("Note ascend level cannot be greater than 7");
if (delay <= 0) throw new IllegalArgumentException("Delay ticks must be at least 1");
return new BukkitRunnable() {
int repeating = ascendLevel;
@Override
public void run() {
player.playNote(playTo.getLocation(), instrument, Note.natural(1, Note.Tone.values()[ascendLevel - repeating]));
if (repeating-- == 0) cancel();
}
}.runTaskTimerAsynchronously(plugin, 0, delay);
}
/**
* An instruction is any musical movement or
* section that can be a restatement
* and might have a fermata.
* https://en.wikipedia.org/wiki/Repetition_(music)
*
* @since 3.0.0
*/
public abstract static class Instruction {
@Nullable
public Sequence parent;
public int restatement, restatementFermata, fermata;
public Instruction(int restatement, int restatementFermata, int fermata) {
this.restatement = restatement;
this.restatementFermata = restatementFermata;
this.fermata = fermata;
}
public abstract void play(Player player, Supplier location, boolean playAtLocation);
public long getEstimatedLength() {
return (long) restatement * restatementFermata;
}
}
/**
* A sequence is a restatement collection
* of multiple other {@link Sequence}s and {@link Sound} that itself might be a part of another {@link Sequence}
*
* A sequence in a script is shown with: {@code (instruction1, instruction2, ...),restatement,restatementFermata fermata}
*
* @since 3.0.0
*/
public static class Sequence extends Instruction {
public Collection instructions = new ArrayList<>(16);
public Sequence() {
super(1, 0, 0);
}
public Sequence(Instruction first) {
super(1, 0, 0);
instructions.add(first);
}
public Sequence(int restatement, int restatementFermata, int fermata) {
super(restatement, restatementFermata, fermata);
}
@Override
public void play(Player player, Supplier location, boolean playAtLocation) {
for (int repeat = restatement; repeat > 0; repeat--) {
for (Instruction instruction : instructions) {
instruction.play(player, location, playAtLocation);
}
if (restatementFermata > 0) sleep(restatementFermata);
}
if (fermata > 0) sleep(fermata);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(200 + (instructions.size() * 100));
builder.append("Sequence:{restatement=").append(restatement).append(", restatementFermata=")
.append(restatementFermata).append(", fermata=").append(fermata).append(", instructions[");
int i = 0, size = instructions.size();
for (Instruction instruction : instructions) {
builder.append(instruction);
if (++i < size) builder.append(", ");
}
builder.append("]}");
return builder.toString();
}
public void addInstruction(Instruction instruction) {
instruction.parent = this;
instructions.add(instruction);
}
@Override
public long getEstimatedLength() {
long result = (long) restatement * restatementFermata;
for (Instruction instruction : instructions) result += instruction.getEstimatedLength();
return result;
}
}
}