package org.herac.tuxguitar.io.musicxml;
import java.io.OutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.log4j.Logger;
import org.herac.tuxguitar.io.base.TGFileFormatException;
import org.herac.tuxguitar.player.base.MidiInstrument;
import org.herac.tuxguitar.song.managers.TGSongManager;
import org.herac.tuxguitar.song.models.Clef;
import org.herac.tuxguitar.song.models.TGBeat;
import org.herac.tuxguitar.song.models.TGDivisionType;
import org.herac.tuxguitar.song.models.TGDuration;
import org.herac.tuxguitar.song.models.TGMeasure;
import org.herac.tuxguitar.song.models.TGNote;
import org.herac.tuxguitar.song.models.TGSong;
import org.herac.tuxguitar.song.models.TGString;
import org.herac.tuxguitar.song.models.TGTempo;
import org.herac.tuxguitar.song.models.TGTimeSignature;
import org.herac.tuxguitar.song.models.TGTrack;
import org.herac.tuxguitar.song.models.TGVoice;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
public class MusicXMLWriter {
/** The Logger for this class. */
public static final transient Logger LOG = Logger
.getLogger(MusicXMLWriter.class);
private static class TGVoiceJoiner {
// private TGFactory factory;
private TGMeasure measure;
public TGVoiceJoiner(TGMeasure measure) {
// this.factory = factory;
this.measure = measure.clone(measure.getHeader());
this.measure.setTrack(measure.getTrack());
}
public void joinBeats() {
TGBeat previous = null;
boolean finish = true;
long measureStart = this.measure.getStart();
long measureEnd = (measureStart + this.measure.getLength());
for (int i = 0; i < this.measure.countBeats(); i++) {
TGBeat beat = this.measure.getBeat(i);
TGVoice voice = beat.getVoice(0);
for (int v = 1; v < beat.countVoices(); v++) {
TGVoice currentVoice = beat.getVoice(v);
if (!currentVoice.isEmpty()) {
for (int n = 0; n < currentVoice.getNotes().size(); n++) {
TGNote note = currentVoice.getNote(n);
voice.addNote(note);
}
}
}
if (voice.isEmpty()) {
this.measure.removeBeat(beat);
finish = false;
break;
}
long beatStart = beat.getStart();
if (previous != null) {
long previousStart = previous.getStart();
TGDuration previousBestDuration = null;
for (int v = /* 1 */0; v < previous.countVoices(); v++) {
TGVoice previousVoice = previous.getVoice(v);
if (!previousVoice.isEmpty()) {
long length = previousVoice.getDuration().getTime();
if ((previousStart + length) <= beatStart) {
if (previousBestDuration == null
|| length > previousBestDuration.getTime()) {
previousBestDuration = previousVoice.getDuration();
}
}
}
}
if (previousBestDuration != null) {
previous.getVoice(0).setDuration(previousBestDuration.clone());
} else {
if (voice.isRestVoice()) {
this.measure.removeBeat(beat);
finish = false;
break;
}
TGDuration duration = TGDuration
.fromTime((beatStart - previousStart));
previous.getVoice(0).setDuration(duration);
}
}
TGDuration beatBestDuration = null;
for (int v = /* 1 */0; v < beat.countVoices(); v++) {
TGVoice currentVoice = beat.getVoice(v);
if (!currentVoice.isEmpty()) {
long length = currentVoice.getDuration().getTime();
if ((beatStart + length) <= measureEnd) {
if (beatBestDuration == null
|| length > beatBestDuration.getTime()) {
beatBestDuration = currentVoice.getDuration();
}
}
}
}
if (beatBestDuration == null) {
if (voice.isRestVoice()) {
this.measure.removeBeat(beat);
finish = false;
break;
}
TGDuration duration = TGDuration.fromTime((measureEnd - beatStart));
voice.setDuration(duration.clone());
}
previous = beat;
}
if (!finish) {
joinBeats();
}
}
public void orderBeats() {
for (int i = 0; i < this.measure.countBeats(); i++) {
TGBeat minBeat = null;
for (int j = i; j < this.measure.countBeats(); j++) {
TGBeat beat = this.measure.getBeat(j);
if (minBeat == null || beat.getStart() < minBeat.getStart()) {
minBeat = beat;
}
}
this.measure.moveBeat(i, minBeat);
}
}
public TGMeasure process() {
this.orderBeats();
this.joinBeats();
return this.measure;
}
}
private static final int DURATION_DIVISIONS = (int) TGDuration.QUARTER_TIME;
private static final String[] DURATION_NAMES = new String[] { "whole",
"half", "quarter", "eighth", "16th", "32nd", "64th", };
private static final int[] DURATION_VALUES = new int[] {
DURATION_DIVISIONS * 4, // WHOLE
DURATION_DIVISIONS * 2, // HALF
DURATION_DIVISIONS * 1, // QUARTER
DURATION_DIVISIONS / 2, // EIGHTH
DURATION_DIVISIONS / 4, // SIXTEENTH
DURATION_DIVISIONS / 8, // THIRTY_SECOND
DURATION_DIVISIONS / 16, // SIXTY_FOURTH
};
private static final boolean[] NOTE_ALTERATIONS = new boolean[] { false,
true, false, true, false, false, true, false, true, false, true, false };
private static final int NOTE_FLATS[] = new int[] { 0, 1, 1, 2, 2, 3, 4, 4,
5, 5, 6, 6 };
private static final String[] NOTE_NAMES = new String[] { "C", "D", "E", "F",
"G", "A", "B" };
private static final int NOTE_SHARPS[] = new int[] { 0, 0, 1, 1, 2, 3, 3, 4,
4, 5, 5, 6 };
private Document document;
private TGSongManager manager;
private OutputStream stream;
public MusicXMLWriter(OutputStream stream) {
this.stream = stream;
}
private Node addAttribute(Node node, String name, String value) {
Attr attribute = this.document.createAttribute(name);
attribute.setNodeValue(value);
node.getAttributes().setNamedItem(attribute);
return node;
}
private Node addNode(Node parent, String name) {
Node node = this.document.createElement(name);
parent.appendChild(node);
return node;
}
private Node addNode(Node parent, String name, String content) {
Node node = this.addNode(parent, name);
node.setTextContent(content);
return node;
}
private Document newDocument() {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
return document;
} catch (Throwable throwable) {
LOG.error(throwable);
}
return null;
}
private void saveDocument() {
try {
TransformerFactory xformFactory = TransformerFactory.newInstance();
Transformer idTransform = xformFactory.newTransformer();
Source input = new DOMSource(this.document);
Result output = new StreamResult(this.stream);
idTransform.setOutputProperty(OutputKeys.INDENT, "yes");
idTransform.transform(input, output);
} catch (Throwable throwable) {
LOG.error(throwable);
}
}
private void writeBeats(Node parent, TGMeasure measure) {
int ks = measure.getKeySignature();
int beatCount = measure.countBeats();
for (int b = 0; b < beatCount; b++) {
TGBeat beat = measure.getBeat(b);
TGVoice voice = beat.getVoice(0);
if (voice.isRestVoice()) {
Node noteNode = this.addNode(parent, "note");
this.addNode(noteNode, "rest");
this.addNode(noteNode, "voice", "1");
this.writeDuration(noteNode, voice.getDuration());
} else {
int noteCount = voice.getNotes().size();
for (int n = 0; n < noteCount; n++) {
TGNote note = voice.getNote(n);
Node noteNode = this.addNode(parent, "note");
int value = (beat.getMeasure().getTrack().getString(note.getString())
.getValue() + note.getValue());
Node pitchNode = this.addNode(noteNode, "pitch");
this.addNode(pitchNode, "step",
NOTE_NAMES[(ks <= 7 ? NOTE_SHARPS[value % 12]
: NOTE_FLATS[value % 12])]);
this.addNode(pitchNode, "octave", Integer.toString(value / 12));
if (NOTE_ALTERATIONS[value % 12]) {
this.addNode(pitchNode, "alter", (ks <= 7 ? "1" : "-1"));
}
Node technicalNode = this.addNode(
this.addNode(noteNode, "notations"), "technical");
this
.addNode(technicalNode, "fret", Integer.toString(note.getValue()));
this.addNode(technicalNode, "string", Integer.toString(note
.getString()));
this.addNode(noteNode, "voice", "1");
this.writeDuration(noteNode, voice.getDuration());
if (note.isTiedNote()) {
this.addAttribute(this.addNode(noteNode, "tie"), "type", "stop");
}
if (n > 0) {
this.addNode(noteNode, "chord");
}
}
}
}
}
private void writeClef(Node parent, Clef clef) {
Node node = this.addNode(parent, "clef");
switch (clef) {
case TREBLE:
this.addNode(node, "sign", "G");
this.addNode(node, "line", "2");
break;
case BASS:
this.addNode(node, "sign", "F");
this.addNode(node, "line", "4");
break;
case TENOR:
this.addNode(node, "sign", "G");
this.addNode(node, "line", "2");
break;
case ALTO:
this.addNode(node, "sign", "G");
this.addNode(node, "line", "2");
break;
}
}
private void writeDirection(Node parent, TGMeasure measure, TGMeasure previous) {
boolean tempoChanges = (previous == null || measure.getTempo().getValue() != previous
.getTempo().getValue());
if (tempoChanges) {
Node direction = this.addAttribute(this.addNode(parent, "direction"),
"placement", "above");
this.writeMeasureTempo(direction, measure.getTempo());
}
}
private void writeDuration(Node parent, TGDuration duration) {
int index = duration.getIndex();
if (index >= 0 && index <= 6) {
int value = (DURATION_VALUES[index] * duration.getDivision().getTimes() / duration
.getDivision().getEnters());
if (duration.isDotted()) {
value += (value / 2);
} else if (duration.isDoubleDotted()) {
value += ((value / 4) * 3);
}
this.addNode(parent, "duration", Integer.toString(value));
this.addNode(parent, "type", DURATION_NAMES[index]);
if (duration.isDotted()) {
this.addNode(parent, "dot");
} else if (duration.isDoubleDotted()) {
this.addNode(parent, "dot");
this.addNode(parent, "dot");
}
if (!duration.getDivision().isEqual(TGDivisionType.NORMAL)) {
Node divisionType = this.addNode(parent, "time-modification");
this.addNode(divisionType, "actual-notes", Integer.toString(duration
.getDivision().getEnters()));
this.addNode(divisionType, "normal-notes", Integer.toString(duration
.getDivision().getTimes()));
}
}
}
private void writeHeaders(Node parent) {
this.writeWork(parent);
this.writeIdentification(parent);
}
private void writeIdentification(Node parent) {
Node identification = this.addNode(parent, "identification");
this.addNode(this.addNode(identification, "encoding"), "software",
"TuxGuitar");
this.addAttribute(this.addNode(identification, "creator", this.manager
.getSong().getAuthor()), "type", "composer");
}
private void writeKeySignature(Node parent, int ks) {
int value = ks;
if (value != 0) {
value = ((((ks - 1) % 7) + 1) * (ks > 7 ? -1 : 1));
}
Node key = this.addNode(parent, "key");
this.addNode(key, "fifths", Integer.toString(value));
this.addNode(key, "mode", "major");
}
private void writeMeasureAttributes(Node parent, TGMeasure measure,
TGMeasure previous) {
boolean divisionChanges = (previous == null);
boolean keyChanges = (previous == null || measure.getKeySignature() != previous
.getKeySignature());
boolean clefChanges = (previous == null || measure.getClef() != previous
.getClef());
boolean timeSignatureChanges = (previous == null || !measure
.getTimeSignature().isEqual(previous.getTimeSignature()));
boolean tuningChanges = (measure.getNumber() == 1);
if (divisionChanges || keyChanges || clefChanges || timeSignatureChanges) {
Node measureAttributes = this.addNode(parent, "attributes");
if (divisionChanges) {
this.addNode(measureAttributes, "divisions", Integer
.toString(DURATION_DIVISIONS));
}
if (keyChanges) {
this.writeKeySignature(measureAttributes, measure.getKeySignature());
}
if (clefChanges) {
this.writeClef(measureAttributes, measure.getClef());
}
if (timeSignatureChanges) {
this.writeTimeSignature(measureAttributes, measure.getTimeSignature());
}
if (tuningChanges) {
this.writeTuning(measureAttributes, measure.getTrack());
}
}
}
private void writeMeasureTempo(Node parent, TGTempo tempo) {
this.addAttribute(this.addNode(parent, "sound"), "tempo", Integer
.toString(tempo.getValue()));
}
private void writePartList(Node parent) {
Node partList = this.addNode(parent, "part-list");
for (final TGTrack track : this.manager.getSong().getTracks()) {
Node scoreParts = this.addNode(partList, "score-part");
this.addAttribute(scoreParts, "id", "P" + track.getNumber());
this.addNode(scoreParts, "part-name", track.getName());
Node scoreInstrument = this.addAttribute(this.addNode(scoreParts,
"score-instrument"), "id", "P" + track.getNumber() + "-I1");
this.addNode(scoreInstrument, "instrument-name",
MidiInstrument.INSTRUMENT_LIST[track.getChannel().getInstrument()]
.getName());
Node midiInstrument = this.addAttribute(this.addNode(scoreParts,
"midi-instrument"), "id", "P" + track.getNumber() + "-I1");
this.addNode(midiInstrument, "midi-channel", Integer.toString(track
.getChannel().getChannel() + 1));
this.addNode(midiInstrument, "midi-program", Integer.toString(track
.getChannel().getInstrument() + 1));
}
}
private void writeParts(Node parent) {
for (final TGTrack track : this.manager.getSong().getTracks()) {
Node part = this.addAttribute(this.addNode(parent, "part"), "id", "P"
+ track.getNumber());
TGMeasure previous = null;
for (final TGMeasure srcMeasure : track.getMeasures()) {
// TODO: Add multivoice support.
TGMeasure measure = new TGVoiceJoiner(srcMeasure).process();
Node measureNode = this.addAttribute(this.addNode(part, "measure"),
"number", Integer.toString(measure.getNumber()));
this.writeMeasureAttributes(measureNode, measure, previous);
this.writeDirection(measureNode, measure, previous);
this.writeBeats(measureNode, measure);
previous = measure;
}
}
}
private void writeSong(Node parent) {
this.writePartList(parent);
this.writeParts(parent);
}
public void writeSong(TGSong song) throws TGFileFormatException {
try {
this.manager = new TGSongManager();
this.manager.setSong(song);
this.document = newDocument();
Node node = this.addNode(this.document, "score-partwise");
this.writeHeaders(node);
this.writeSong(node);
this.saveDocument();
this.stream.flush();
this.stream.close();
} catch (Throwable throwable) {
throw new TGFileFormatException("Could not write song!.", throwable);
}
}
private void writeTimeSignature(Node parent, TGTimeSignature ts) {
Node node = this.addNode(parent, "time");
this.addNode(node, "beats", Integer.toString(ts.getNumerator()));
this.addNode(node, "beat-type", Integer.toString(ts.getDenominator()
.getValue()));
}
private void writeTuning(Node parent, TGTrack track) {
Node staffDetailsNode = this.addNode(parent, "staff-details");
this.addNode(staffDetailsNode, "staff-lines", Integer.toString(track
.stringCount()));
for (int i = track.stringCount(); i > 0; i--) {
TGString string = track.getString(i);
Node stringNode = this.addNode(staffDetailsNode, "staff-tuning");
this.addAttribute(stringNode, "line", Integer.toString((track
.stringCount() - string.getNumber()) + 1));
this.addNode(stringNode, "tuning-step", NOTE_NAMES[NOTE_SHARPS[(string
.getValue() % 12)]]);
this.addNode(stringNode, "tuning-octave", Integer.toString(string
.getValue() / 12));
}
}
private void writeWork(Node parent) {
this.addNode(this.addNode(parent, "work"), "work-title", this.manager
.getSong().getName());
}
}