/* -*- c-basic-offset: 2; indent-tabs-mode: nil; -*- */
/*
* FreeDots -- MusicXML to braille music transcription
*
* Copyright 2008-2010 Mario Lang All Rights Reserved.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details (a copy is included in the LICENSE.txt file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License
* along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This file is maintained by Mario Lang <mlang@delysid.org>.
*/
package freedots.transcription;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Logger;
import java.util.Set;
import freedots.Braille;
import freedots.braille.BrailleChord;
import freedots.braille.BrailleDynamics;
import freedots.braille.BrailleList;
import freedots.braille.BrailleNote;
import freedots.braille.BrailleSequence;
import freedots.braille.BrailleWords;
import freedots.braille.FullMeasureInAccordSign;
import freedots.braille.PartMeasureInAccordSign;
import freedots.braille.PartMeasureInAccordDivisionSign;
import freedots.braille.RestSign;
import freedots.braille.SimileSign;
import freedots.braille.Text;
import freedots.music.AbstractPitch;
import freedots.music.AugmentedPowerOfTwo;
import freedots.music.Event;
import freedots.music.MusicList;
import freedots.music.RhythmicElement;
import freedots.music.TimeSignature;
import freedots.music.Voice;
import freedots.music.VoiceChord;
import freedots.musicxml.Direction;
import freedots.musicxml.Note;
class BrailleMeasure {
private static final Logger LOG = Logger.getLogger(BrailleMeasure.class.getName());
private BrailleMeasure previous = null;
private MusicList events = new MusicList();
public MusicList getEvents() { return events; }
private AbstractPitch finalPitch = null;
private TimeSignature timeSignature = null;
private int voiceDirection = -1; /* By default, top to bottom */
private Comparator<RhythmicElement> chordComparator =
new VoiceChord.DescendingComparator();
private final Transcriber transcriber;
BrailleMeasure(final Transcriber transcriber) {
this.transcriber = transcriber;
}
BrailleMeasure(final Transcriber transcriber, final BrailleMeasure previous) {
this(transcriber);
this.previous = previous;
}
public void add(Event event) { events.add(event); }
public void setTimeSignature(TimeSignature timeSignature) {
this.timeSignature = timeSignature;
}
public void setChordDirection(final int direction) {
if (direction < 0) chordComparator = new VoiceChord.DescendingComparator();
else chordComparator = new VoiceChord.AscendingComparator();
}
public void setVoiceDirection(int direction) {
voiceDirection = direction;
}
public void unlinkPrevious() { previous = null; }
private List<Object> brailleVoices = new ArrayList<Object>();
private boolean fullSimile = false;
/**
* Detect simile and find voice overlaps for part measure in-accord.
* <p>
* This code is very hairy and definitely needs an overhaul.
* It is also incomplete as it does not handle individual voices repeating.
*/
// TODO: Find a data structure to represent part/full measure in-accord
public void process() {
if (previous != null) {
if (previous.getEvents().equalsIgnoreOffset(this.events)) {
fullSimile = true;
}
}
if (!fullSimile) {
brailleVoices = new ArrayList<Object>();
List<Voice> voices = events.getVoices(voiceDirection);
FullMeasureInAccord fmia = new FullMeasureInAccord();
PartMeasureInAccord pmia = new PartMeasureInAccord();
while (voices.size() > 0) {
Voice voice = voices.get(0);
boolean foundOverlap = false;
int headLength = 0;
for (int j = 1; j < voices.size(); j++) {
int equalsAtBeginning = voice.countEqualsAtBeginning(voices.get(j));
if (equalsAtBeginning > 0) {
headLength = equalsAtBeginning;
MusicList head = new MusicList();
for (int k = 0; k < equalsAtBeginning; k++) {
head.add(voice.get(k));
voices.get(j).remove(0);
}
pmia.setHead(head);
pmia.addPart(voice);
pmia.addPart(voices.get(j));
voices.remove(voices.get(j));
foundOverlap = true;
} else if (foundOverlap && equalsAtBeginning == headLength) {
for (int k = 0; k < equalsAtBeginning; k++) {
voices.get(j).remove(k);
}
pmia.addPart(voices.get(j));
voices.remove(voices.get(j));
}
}
if (foundOverlap) {
for (int k = 0; k < headLength; k++) {
voice.remove(0);
}
} else {
fmia.addPart(voice);
}
voices.remove(voice);
}
if (fmia.getParts().size() > 0) brailleVoices.add(fmia);
if (pmia.getParts().size() > 0) brailleVoices.add(pmia);
}
}
public AbstractPitch getFinalPitch() { return finalPitch; }
private BrailleList tail;
public BrailleList tail() { return tail; }
class State {
private int width;
private AbstractPitch lastPitch;
private BrailleList head = new BrailleList();
private BrailleList tail = new BrailleList();
private boolean hyphenated = false;
State(final int width, final AbstractPitch lastPitch) {
this.width = width;
this.lastPitch = lastPitch;
}
void append(String braille) {
append(new Text(braille));
}
void append(BrailleSequence braille) {
if (head.length() + braille.length() < width && !hyphenated) {
head.add(braille);
} else {
hyphenated = true;
tail.add(braille);
}
}
AbstractPitch getLastPitch() { return lastPitch; }
void setLastPitch(AbstractPitch lastPitch) { this.lastPitch = lastPitch; }
BrailleList getHead() { return head; }
BrailleList getTail() { return tail; }
}
/** Actually performs the task of transcribing events given the amount
* of remaining characters on this line.
* @see #tail
*/
public BrailleList head(int width, boolean lastLine) {
State state = new State(width,
previous != null? previous.getFinalPitch(): null);
if (fullSimile) {
state.append(new SimileSign());
} else {
for (int i = 0; i < brailleVoices.size(); i++) {
if (brailleVoices.get(i) instanceof PartMeasureInAccord) {
final PartMeasureInAccord pmia = (PartMeasureInAccord)brailleVoices.get(i);
if (i > 0) {
state.append(new FullMeasureInAccordSign());
/* The octave mark must be shown for
* the first note after an in-accord.
************************************/
state.setLastPitch(null);
}
MusicList pmiaHead = pmia.getHead();
if (pmiaHead.size() > 0) {
printNoteList(pmiaHead, state, null);
state.append(new PartMeasureInAccordSign());
}
for (int p = 0; p < pmia.getParts().size(); p++) {
printNoteList(pmia.getParts().get(p), state, null);
if (p < pmia.getParts().size() - 1) {
state.append(new PartMeasureInAccordDivisionSign());
/* The octave mark must be shown for
* the first note after an in-accord.
************************************/
state.setLastPitch(null);
}
}
MusicList pmiaTail = pmia.getTail();
if (pmiaTail.size() > 0) {
state.append(new PartMeasureInAccordSign());
printNoteList(pmiaTail, state, null);
}
} else if (brailleVoices.get(i) instanceof FullMeasureInAccord) {
FullMeasureInAccord fmia = (FullMeasureInAccord)brailleVoices.get(i);
for (int p = 0; p < fmia.getParts().size(); p++) {
Object splitPoint = null;
ValueInterpreter valueInterpreter =
new ValueInterpreter(fmia.getParts().get(p), timeSignature);
Set<ValueInterpreter.Interpretation>
interpretations = valueInterpreter.getInterpretations();
if (interpretations.size() > 1) {
splitPoint = valueInterpreter.getSplitPoint();
if (splitPoint == null) {
StringBuilder sb = new StringBuilder();
sb.append("Unimplemented: "
+ interpretations.size()
+ " possible interpretations:\n");
for (ValueInterpreter.Interpretation
interpretation:interpretations) {
sb.append((interpretation.isCorrect()?" * ":" ")
+ interpretation.toString() + "\n");
}
LOG.warning(sb.toString());
}
}
printNoteList(fmia.getParts().get(p), state, splitPoint);
if (p < fmia.getParts().size() - 1) {
state.append(new FullMeasureInAccordSign());
/* The octave mark must be shown for
* the first note after an in-accord.
************************************/
state.setLastPitch(null);
}
}
}
}
if (brailleVoices.size() == 0) {
state.append(new RestSign(new AugmentedPowerOfTwo(AugmentedPowerOfTwo.BREVE,
0) {
public String getDescription() {
return "A whole measure rest";
}
}));
}
/* 5-12. The octave mark must be shown for the first note after an
* in-accord and _at the beginning of the next measure_, whether or not
* that measure contains an in-accord.
***********************************************************************/
if (brailleVoices.size() == 1
&& !((FullMeasureInAccord)brailleVoices.get(0)).isInAccord())
finalPitch = state.getLastPitch();
}
tail = state.getTail();
return state.getHead();
}
void printNoteList(MusicList musicList, State state, Object splitPoint) {
for (Event element:musicList) {
if (element instanceof Note) {
Note note = (Note)element;
if (splitPoint != null && splitPoint == note) {
state.append(Braille.valueDistinction.toString());
}
BrailleNote brailleNote = new BrailleNote(note, state.getLastPitch());
AbstractPitch pitch = (AbstractPitch)note.getPitch();
if (pitch != null) {
state.setLastPitch(pitch);
}
state.append(brailleNote);
} else if (element instanceof VoiceChord) {
BrailleChord brailleChord = new BrailleChord((VoiceChord)element,
chordComparator,
state.getLastPitch());
AbstractPitch pitch = (AbstractPitch)brailleChord.getNotePitch();
if (pitch != null) {
state.setLastPitch(pitch);
}
state.append(brailleChord);
} else if (element instanceof Direction) {
Direction direction = (Direction)element;
if (!transcriber.getAlreadyPrintedDirections().contains(direction)) {
List<String> dynamics = direction.getDynamics();
if (dynamics != null) {
for (String dyn: dynamics) {
state.append(new BrailleDynamics(dyn, direction));
state.setLastPitch(null);
}
}
String words = direction.getWords();
if (words != null && !words.isEmpty()) {
state.append(new BrailleWords(words, direction));
}
if (direction.isPedalPress())
state.append(Braille.pedalPress.toString());
else if (direction.isPedalRelease())
state.append(Braille.pedalRelease.toString());
}
}
}
}
}