Stroke.java
package com.github.dakusui.symfonion.song;
import com.github.dakusui.json.*;
import com.github.dakusui.symfonion.core.MidiCompiler;
import com.github.dakusui.symfonion.core.MidiCompilerContext;
import com.github.dakusui.symfonion.exceptions.ExceptionThrower;
import com.github.dakusui.symfonion.exceptions.SymfonionException;
import com.github.dakusui.symfonion.exceptions.SymfonionIllegalFormatException;
import com.github.dakusui.symfonion.song.Pattern.Parameters;
import com.github.dakusui.symfonion.utils.Fraction;
import com.github.dakusui.symfonion.utils.Utils;
import com.google.gson.*;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.Track;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import static com.github.dakusui.symfonion.exceptions.ExceptionThrower.*;
import static com.github.dakusui.symfonion.exceptions.ExceptionThrower.ContextKey.JSON_ELEMENT_ROOT;
import static com.github.dakusui.symfonion.exceptions.SymfonionIllegalFormatException.NOTE_LENGTH_EXAMPLE;
import static com.github.dakusui.symfonion.exceptions.SymfonionTypeMismatchException.PRIMITIVE;
public class Stroke {
private static final int UNDEFINED_NUM = -1;
private final Fraction length;
static java.util.regex.Pattern notesPattern = java.util.regex.Pattern.compile("([A-Zac-z])([#b]*)([><]*)([\\+\\-]*)?");
List<NoteSet> notes = new LinkedList<>();
private final double gate;
private final NoteMap noteMap;
private final int[] volume;
private final int[] pan;
private final int[] reverb;
private final int[] chorus;
private final int[] pitch;
private final int[] modulation;
private final int pgno;
private String bkno = null;
private final int tempo;
private final JsonArray sysex;
private final int[] aftertouch;
private final JsonElement strokeJson;
public Stroke(JsonElement strokeJson, Parameters params, NoteMap noteMap) throws SymfonionException, JsonException {
String notes;
Fraction len = params.length();
double gate = params.gate();
this.strokeJson = strokeJson;
JsonObject obj = JsonUtils.asJsonObjectWithPromotion(strokeJson, new String[]{
Keyword.$notes.name(),
Keyword.$length.name()
});
notes = JsonUtils.asStringWithDefault(obj, null, Keyword.$notes);
if (JsonUtils.hasPath(obj, Keyword.$length)) {
JsonElement lenJSON = JsonUtils.asJsonElement(obj, Keyword.$length);
if (lenJSON.isJsonPrimitive()) {
len = Utils.parseNoteLength(lenJSON.getAsString());
if (len == null) {
throw illegalFormatException(lenJSON, NOTE_LENGTH_EXAMPLE);
}
} else {
throw typeMismatchException(lenJSON, PRIMITIVE);
}
}
if (JsonUtils.hasPath(obj, Keyword.$gate)) {
gate = JsonUtils.asDouble(obj, Keyword.$gate);
}
this.tempo = JsonUtils.hasPath(obj, Keyword.$tempo) ? JsonUtils.asInt(obj, Keyword.$tempo) : UNDEFINED_NUM;
this.pgno = JsonUtils.hasPath(obj, Keyword.$program) ? JsonUtils.asInt(obj, Keyword.$program) : UNDEFINED_NUM;
if (JsonUtils.hasPath(obj, Keyword.$bank)) {
this.bkno = JsonUtils.asString(obj, Keyword.$bank);
// Checks if this.bkno can be parsed as a double value.
assert this.bkno != null;
//noinspection ResultOfMethodCallIgnored
Double.parseDouble(this.bkno);
}
this.volume = getIntArray(obj, Keyword.$volume);
this.pan = getIntArray(obj, Keyword.$pan);
this.reverb = getIntArray(obj, Keyword.$reverb);
this.chorus = getIntArray(obj, Keyword.$chorus);
this.pitch = getIntArray(obj, Keyword.$pitch);
this.modulation = getIntArray(obj, Keyword.$modulation);
this.aftertouch = getIntArray(obj, Keyword.$aftertouch);
this.sysex = JsonUtils.asJsonArrayWithDefault(obj, null, Keyword.$sysex);
/*
* } else {
* // unsupported
* }
*/
this.noteMap = noteMap;
this.gate = gate;
Fraction strokeLen = Fraction.zero;
if (notes != null) {
for (String nn : notes.split(";")) {
NoteSet ns = new NoteSet();
Fraction nsLen;
String l;
if ((l = parseNotes(nn, ns)) != null) {
nsLen = Utils.parseNoteLength(l);
} else {
nsLen = len;
}
ns.setLength(nsLen);
this.notes.add(ns);
strokeLen = Fraction.add(strokeLen, nsLen);
}
}
if (Fraction.zero.equals(strokeLen)) {
strokeLen = len;
}
this.length = strokeLen;
}
private int[] getIntArray(JsonObject cur, Keyword kw) throws JsonInvalidPathException, JsonTypeMismatchException, JsonFormatException {
int[] ret;
if (!JsonUtils.hasPath(cur, kw)) {
return null;
}
JsonElement json = JsonUtils.asJsonElement(cur, kw);
if (json.isJsonArray()) {
JsonArray arr = json.getAsJsonArray();
ret = interpolate(expandDots(arr));
} else {
ret = new int[1];
ret[0] = JsonUtils.asInt(cur, kw);
}
return ret;
}
private static JsonArray expandDots(JsonArray arr) throws SymfonionIllegalFormatException {
JsonArray ret = new JsonArray();
for (int i = 0; i < arr.size(); i++) {
JsonElement cur = arr.get(i);
if (cur.isJsonPrimitive()) {
JsonPrimitive p = cur.getAsJsonPrimitive();
if (p.isBoolean() || p.isNumber())
ret.add(p);
else if (p.isString()) {
String s = p.getAsString();
for (int j = 0; j < s.length(); j++) {
if (s.charAt(j) == '.')
ret.add(JsonNull.INSTANCE);
else
throw syntaxErrorWhenExpandingDotsIn(arr);
}
}
} else if (cur.isJsonNull())
ret.add(cur);
else
throw ExceptionThrower.typeMismatchWhenExpandingDotsIn(arr);
}
return ret;
}
private static int[] interpolate(JsonArray arr) {
int[] ret;
Integer[] tmp = new Integer[arr.size()];
for (int i = 0; i < tmp.length; i++) {
if (arr.get(i) == null || arr.get(i).isJsonNull()) {
tmp[i] = null;
} else {
tmp[i] = arr.get(i).getAsInt();
}
}
ret = new int[arr.size()];
int start = 0;
int end = 0;
for (int i = 0; i < tmp.length; i++) {
if (tmp[i] != null) {
start = ret[i] = tmp[i];
} else {
int j = i + 1;
while (j < tmp.length) {
if (tmp[j] != null) {
end = tmp[j];
ret[j] = end;
break;
}
j++;
}
int step = (end - start) / (j - i);
int curval = start;
for (int k = i; k < j; k++) {
curval += step;
ret[k] = curval;
}
i = j;
}
}
return ret;
}
public Fraction length() {
return length;
}
public double gate() {
return this.gate;
}
public List<NoteSet> noteSets() {
return this.notes;
}
/*
* Returns the 'length' portion of the string <code>s</code>.
*/
private String parseNotes(String s, List<Note> notes) throws SymfonionException {
Matcher m = notesPattern.matcher(s);
int i;
for (i = 0; m.find(i); i = m.end()) {
if (i != m.start()) {
throw syntaxErrorInNotePattern(s, i, m);
}
int n_ = this.noteMap.note(m.group(1), this.strokeJson);
if (n_ >= 0) {
int n =
n_ +
Utils.count('#', m.group(2)) - Utils.count('b', m.group(2)) +
Utils.count('>', m.group(3)) * 12 - Utils.count('<', m.group(3)) * 12;
int a = Utils.count('+', m.group(4)) - Utils.count('-', m.group(4));
Note nn = new Note(n, a);
notes.add(nn);
}
}
Matcher n = Utils.lengthPattern.matcher(s);
String ret = null;
if (n.find(i)) {
ret = s.substring(n.start(), n.end());
i = n.end();
}
if (i != s.length()) {
String msg = s.substring(0, i) + "`" + s.substring(i) + "' isn't a valid note expression. Notes must be like 'C', 'CEG8.', and so on.";
throw illegalFormatException(this.strokeJson, msg);
}
return ret;
}
interface EventCreator {
void createEvent(int v, long pos) throws InvalidMidiDataException;
}
private void renderValues(int[] values, long pos, long strokeLen, MidiCompiler compiler, EventCreator creator) throws
InvalidMidiDataException {
if (values == null) {
return;
}
long step = strokeLen / values.length;
for (int i = 0; i < values.length; i++) {
creator.createEvent(values[i], pos + step * i);
compiler.controlEventProcessed();
}
}
public void compile(final MidiCompiler compiler, MidiCompilerContext context) throws InvalidMidiDataException {
final Track track = context.track();
final int ch = context.channel();
long absolutePosition = context.convertRelativePositionInStrokeToAbsolutePosition(Fraction.zero);
long strokeLen = context.getStrokeLengthInTicks(this);
if (tempo != UNDEFINED_NUM) {
track.add(compiler.createTempoEvent(this.tempo, absolutePosition));
compiler.controlEventProcessed();
}
if (bkno != null) {
int msb = Integer.parseInt(bkno.substring(0, this.bkno.indexOf('.')));
track.add(compiler.createBankSelectMSBEvent(ch, msb, absolutePosition));
if (this.bkno.indexOf('.') != -1) {
int lsb = Integer.parseInt(bkno.substring(this.bkno.indexOf('.') + 1));
track.add(compiler.createBankSelectLSBEvent(ch, lsb, absolutePosition));
}
compiler.controlEventProcessed();
}
if (pgno != UNDEFINED_NUM) {
track.add(compiler.createProgramChangeEvent(ch, this.pgno, absolutePosition));
compiler.controlEventProcessed();
}
if (sysex != null) {
MidiEvent ev = compiler.createSysexEvent(ch, sysex, absolutePosition);
if (ev != null) {
track.add(ev);
compiler.sysexEventProcessed();
}
}
renderValues(volume, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createVolumeChangeEvent(ch, v, pos)));
renderValues(pan, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createPanChangeEvent(ch, v, pos)));
renderValues(reverb, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createReverbEvent(ch, v, pos)));
renderValues(chorus, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createChorusEvent(ch, v, pos)));
renderValues(pitch, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createPitchBendEvent(ch, v, pos)));
renderValues(modulation, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createModulationEvent(ch, v, pos)));
renderValues(aftertouch, absolutePosition, strokeLen, compiler, (v, pos) -> track.add(compiler.createAfterTouchChangeEvent(ch, v, pos)));
int transpose = context.params().transpose();
int arpegiodelay = context.params().arpegio();
int delta = 0;
Fraction relPosInStroke = Fraction.zero;
for (NoteSet noteSet : this.noteSets()) {
absolutePosition = context.convertRelativePositionInStrokeToAbsolutePosition(relPosInStroke);
long absolutePositionWhereNoteFinishes = context.convertRelativePositionInStrokeToAbsolutePosition(
Fraction.add(
relPosInStroke,
noteSet.getLength()
)
);
long noteLengthInTicks = absolutePositionWhereNoteFinishes - absolutePosition;
for (Note note : noteSet) {
int key = note.key() + transpose;
int velocity = Math.max(
0,
Math.min(
127,
context.params().velocitybase() +
note.accent() * context.params().velocitydelta() +
context.getGrooveAccent(relPosInStroke)
)
);
track.add(compiler.createNoteOnEvent(ch, key, velocity, absolutePosition + delta));
track.add(compiler.createNoteOffEvent(ch, key, (long) (absolutePosition + delta + noteLengthInTicks * this.gate())));
compiler.noteProcessed();
delta += arpegiodelay;
}
compiler.noteSetProcessed();
relPosInStroke = Fraction.add(relPosInStroke, noteSet.getLength());
}
}
}