package com.xenoage.zong.webserver.io;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.xenoage.utils.math.Fraction;
import com.xenoage.utils.math.geom.Point2f;
import com.xenoage.utils.math.geom.Size2f;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.desktop.io.midi.out.JseMidiSequenceWriter;
import com.xenoage.zong.documents.ScoreDoc;
import com.xenoage.zong.io.midi.out.MidiConverter;
import com.xenoage.zong.layout.Layout;
import com.xenoage.zong.layout.Page;
import com.xenoage.zong.layout.frames.Frame;
import com.xenoage.zong.layout.frames.ScoreFrame;
import com.xenoage.zong.musiclayout.ScoreFrameLayout;
import com.xenoage.zong.musiclayout.spacing.BeatOffset;
import com.xenoage.zong.musiclayout.spacing.SystemSpacing;
import lombok.val;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.zong.io.midi.out.MidiConverter.Options.optionsForFileExport;
/**
* This class creates a JSON object which contains a mapping from time
* (in sampled audio files) to cursor positions.
*
* This is the format in detail:
*
* <pre>{"mps":[ // map time to measure/beat
* {"measure":0, "beat":"0/1", "ms":0}, // ms: milliseconds in MP3/OGG file
* {"measure":0, "beat":"1/8", "ms":112},
* ... ],
* "systems":[ // for each system its number, page, and y coordinates (ratio to page height)
* {"page":0, "number":0, "top":0.1, "bottom":0.4},
* {"page":0, "number":1, "top":0.5, "bottom":0.8"},
* ... ],
* "measures":[ //the list of measures
* //for each measure: system index, left and right boundaries
* {"number":0, "system":0, "left":0.1, "right":0.22, "beats":[
* //beat and horizontal coordinate (ratio to page width; and relative to system start)
* {"at":"0/1", "x":0.21},
* {"at":"1/8", "x":0.214},
* ... ]},
* ... ],
* "timecursors":[ //simple mapping from time to cursor positions
* //for each beat (given in milliseconds), the page index, x-coordinate and y-coordinate
* //and height (ratio to page size, relative to the top-left corner of the page) of the cursor
* { time : 0.5, page : 0, top : 0.05, left : 0.08, bottom : 0.10 },
* ... ]}</pre>
*
* @author Andreas Wenger
*/
public class CursorOutput {
private class System {
int page = 0;
float top = Integer.MAX_VALUE;
float bottom = Integer.MIN_VALUE;
}
private class Measure {
int system = 0;
float left = 0;
float right = 0;
HashMap<Fraction, Float> beats = new HashMap<>();
}
public JsonObject write(ScoreDoc doc) {
JsonObject ret = new JsonObject();
//create midi sequence and mp mappings
Score score = doc.getScore();
val seq = MidiConverter.convertToSequence(score, optionsForFileExport,
new JseMidiSequenceWriter());
//save time map
JsonArray jsonMPs = new JsonArray();
val timeMap = seq.getTimeMap();
for (int iRep : range(timeMap.getRepetitionsCount())) {
for (val time : timeMap.getTimesSorted(iRep)) {
val midiTime = timeMap.getByRepTime(iRep, time);
JsonObject jsonMP = new JsonObject();
jsonMP.addProperty("measure", time.measure);
jsonMP.addProperty("beat", "" + time.beat);
jsonMP.addProperty("ms", midiTime.ms);
jsonMPs.add(jsonMP);
}
}
ret.add("mps", jsonMPs);
//collect data
int measuresCount = score.getMeasuresCount();
ArrayList<System> systems = new ArrayList<>();
ArrayList<Measure> measures = new ArrayList<>();
for (int i = 0; i < measuresCount; i++) {
measures.add(new Measure());
}
int systemCount = 0;
Layout layout = doc.getLayout();
for (int iPage : range(layout.getPages())) {
Page page = layout.getPages().get(iPage);
Size2f pageSize = page.getFormat().getSize();
for (Frame frame : page.getFrames()) {
if (frame instanceof ScoreFrame) {
Point2f absPos = frame.getAbsolutePosition();
float offsetX = absPos.x - frame.getSize().width / 2;
float offsetY = absPos.y - frame.getSize().height / 2;
ScoreFrameLayout sfl = ((ScoreFrame) frame).getScoreFrameLayout();
for (SystemSpacing systemSpacing : sfl.getFrameSpacing().getSystems()) {
//read system data
int systemIndex = systemCount + systemSpacing.getSystemIndexInFrame();
while (systems.size() - 1 < systemIndex)
systems.add(new System());
System system = systems.get(systemIndex);
system.page = iPage;
system.top = (offsetY + systemSpacing.offsetYMm) / pageSize.height;
system.bottom = (offsetY + systemSpacing.offsetYMm + systemSpacing.getHeightMm()) / pageSize.height;
//read measure beats
float systemOffsetX = systemSpacing.marginLeftMm;
for (int iMeasure : systemSpacing.getMeasures()) {
Measure measure = measures.get(iMeasure);
measure.system = systemIndex;
measure.left = (offsetX + systemOffsetX + systemSpacing.getMeasureStartMm(iMeasure)) / pageSize.width;
measure.right = (offsetX + systemOffsetX + systemSpacing.getMeasureEndMm(iMeasure)) / pageSize.width;
for (BeatOffset bo : systemSpacing.getColumn(iMeasure).getBeatOffsets()) {
measure.beats.put(bo.getBeat(), (offsetX + systemOffsetX + bo.getOffsetMm()) /
pageSize.width);
}
}
}
systemCount += sfl.getFrameSpacing().getSystems().size();
}
}
}
//save systems
JsonArray jsonSystems = new JsonArray();
for (int i = 0; i < systems.size(); i++) {
System system = systems.get(i);
JsonObject jsonSystem = new JsonObject();
jsonSystem.addProperty("number", i);
jsonSystem.addProperty("page", system.page);
jsonSystem.addProperty("top", system.top);
jsonSystem.addProperty("bottom", system.bottom);
jsonSystems.add(jsonSystem);
}
ret.add("systems", jsonSystems);
//save measures
JsonArray jsonMeasures = new JsonArray();
for (int i = 0; i < measuresCount; i++) {
Measure measure = measures.get(i);
JsonObject jsonMeasure = new JsonObject();
jsonMeasure.addProperty("number", i);
jsonMeasure.addProperty("system", measure.system);
jsonMeasure.addProperty("left", measure.left);
jsonMeasure.addProperty("right", measure.right);
//beats
JsonArray jsonBeats = new JsonArray();
ArrayList<Fraction> sortedBeats = new ArrayList<>(measure.beats.keySet());
Collections.sort(sortedBeats);
for (Fraction beat : sortedBeats) {
JsonObject jsonBeat = new JsonObject();
jsonBeat.addProperty("at", "" + beat);
jsonBeat.addProperty("x", measure.beats.get(beat));
jsonBeats.add(jsonBeat);
}
jsonMeasure.add("beats", jsonBeats);
jsonMeasures.add(jsonMeasure);
}
ret.add("measures", jsonMeasures);
//save time cursors
JsonArray jsonTCs = new JsonArray();
for (int iRep : range(timeMap.getRepetitionsCount())) {
for (val time : timeMap.getTimesSorted(iRep)) {
val midiTime = timeMap.getByRepTime(iRep, time);
JsonObject jsonTC = new JsonObject();
jsonTC.addProperty("time", midiTime.ms);
Measure measure = measures.get(time.measure);
System system = systems.get(measure.system);
jsonTC.addProperty("page", system.page);
jsonTC.addProperty("top", system.top);
jsonTC.addProperty("left", measure.beats.get(time.beat));
jsonTC.addProperty("bottom", system.bottom);
jsonTCs.add(jsonTC);
}
}
ret.add("timecursors", jsonTCs);
return ret;
}
}