/* -*- 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.music;
/* It is never the case that musical objects in the future will affect
* objects in the past. This property can be exploited by sorting all
* the objects by their temporal order into a one-dimensional list.
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.SortedMap;
import java.util.TreeMap;
import freedots.math.Fraction;
import freedots.musicxml.Chord; // FIXME
import freedots.musicxml.Direction;
public class MusicList extends java.util.ArrayList<Event> {
public MusicList() {
super();
}
/** Add a new element, inserting at the last possible position.
*/
@Override public boolean add(Event newElement) {
final Fraction moment = newElement.getMoment();
ListIterator<Event> iterator = listIterator();
while (iterator.hasNext())
if (iterator.next().getMoment().compareTo(moment) > 0) {
iterator.previous();
break;
}
iterator.add(newElement);
return true;
}
/** Returns a list of events which appear at a given time offset.
* @return an empty list if there is no event at the given offset.
*/
public MusicList eventsAt(Fraction moment) {
if (moment.compareTo(0) < 0)
throw new IllegalArgumentException("Negative offset");
final MusicList events = new MusicList();
final Iterator<Event> iterator = iterator();
while (iterator.hasNext()) {
final Event event = iterator.next();
if (event.getMoment().compareTo(moment) < 0) continue;
if (event.getMoment().equals(moment))
events.add(event);
else
break;
}
return events;
}
public int getStaffCount() {
for (Event event:this) {
if (event instanceof StartBar) {
StartBar startBar = (StartBar)event;
return startBar.getStaffCount();
}
}
return 0;
}
public Staff getStaff(int index) {
List<Staff> staves = new ArrayList<Staff>();
for (int i = 0; i < getStaffCount(); i++) staves.add(new Staff());
for (Event event:this) {
if (event instanceof VerticalEvent) {
for (Staff staff:staves) staff.add(event);
} else if (event instanceof StaffElement) {
staves.get(((StaffElement)event).getStaffNumber()).add(event);
} else if (event instanceof Chord) {
for (StaffElement staffChord:((Chord)event).getStaffChords()) {
int staffNumber = staffChord.getStaffNumber();
staves.get(staffNumber).add(staffChord);
staffChord.setStaff(staves.get(staffNumber));
}
} else if (event instanceof GlobalKeyChange) {
GlobalKeyChange globalKeyChange = (GlobalKeyChange)event;
for (int i = 0; i < staves.size(); i++) {
KeyChange keyChange = new KeyChange(globalKeyChange.getMoment(),
globalKeyChange.getKeySignature(), i);
staves.get(i).add(keyChange);
}
}
}
return staves.get(index);
}
public List<Voice> getVoices() {
SortedMap<String, Voice> voices = new TreeMap<String, Voice>();
Voice defaultVoice = null;
for (Event event:this) {
if (event instanceof VoiceElement) {
String voiceName = ((VoiceElement)event).getVoiceName();
if (voiceName == null) {
if (defaultVoice == null) defaultVoice = new Voice(null);
defaultVoice.add(event);
} else {
if (!voices.containsKey(voiceName))
voices.put(voiceName, new Voice(voiceName));
voices.get(voiceName).add(event);
}
} else if (event instanceof StaffChord) {
for (VoiceElement voiceElement:((StaffChord)event).getVoiceChords()) {
String voiceName = voiceElement.getVoiceName();
if (voiceName == null) {
if (defaultVoice == null) defaultVoice = new Voice(null);
defaultVoice.add(voiceElement);
} else {
if (!voices.containsKey(voiceName))
voices.put(voiceName, new Voice(voiceName));
voices.get(voiceName).add(voiceElement);
}
}
}
}
Iterator<Voice> iter = voices.values().iterator();
while (iter.hasNext()) if (iter.next().restsOnly()) iter.remove();
List<Voice> voiceList = new ArrayList<Voice>(voices.values());
if (defaultVoice != null) voiceList.add(defaultVoice);
// Now that we have distributed all notes to their voices we can
// insert directions at appropriate positions in appropriate voices
insertDirections(voiceList);
return voiceList;
}
public List<Voice> getVoices(int ordering) {
List<Voice> voices = getVoices();
Collections.sort(voices, new VoiceComparator(ordering));
return voices;
}
/** Insert directions into appropriate voices.
* <p>
* For every {@link freedots.musicxml.Direction} contained in this list,
* find a voice which has a note/rest at the position of the direction
* and insert it there.
* This is necessary because braille music needs directions attached
* to specific notes or rests, not just a vertical staff position as with
* print music.
*/
// TODO: take direction position (above, below) into account.
private void insertDirections(List<Voice> voices) {
for (Event event: this) {
if (event instanceof Direction) {
Direction direction = (Direction)event;
final Fraction moment = direction.getMoment();
SEARCH: for (Voice voice: voices) {
ListIterator<Event> iterator = voice.listIterator();
while (iterator.hasNext()) {
Event next = iterator.next();
if (next.getMoment().equals(moment)
&& !(next instanceof Direction)) {
iterator.previous(); iterator.add(direction);
break SEARCH;
}
}
iterator.add(direction);
}
}
}
}
private class VoiceComparator implements java.util.Comparator<Voice> {
private int direction;
VoiceComparator(final int direction) {
this.direction = direction;
}
public int compare(Voice v1, Voice v2) {
int v1Pitch = v1.averagePitch();
int v2Pitch = v2.averagePitch();
int result = (v1Pitch < v2Pitch ? -1 : (v1Pitch == v2Pitch ? 0 : 1));
if (direction > 0) return result;
return -result;
}
}
public boolean equalsIgnoreOffset(MusicList other) {
if (this.size() == other.size()) {
for (int i = 0; i < size(); i++) {
if (!(this.get(i).getClass() == other.get(i).getClass())) return false;
if (!this.get(i).equalsIgnoreOffset(other.get(i))) return false;
}
return true;
}
return false;
}
public String getLyricText() {
StringBuilder stringBuilder = new StringBuilder();
for (Event event:this) {
if (event instanceof freedots.musicxml.Note) {
freedots.musicxml.Note note = (freedots.musicxml.Note)event;
Lyric lyric = note.getLyric();
if (lyric != null) {
stringBuilder.append(lyric.getText());
if (lyric.getSyllabic() == Syllabic.SINGLE ||
lyric.getSyllabic() == Syllabic.END)
stringBuilder.append(" ");
}
}
}
return stringBuilder.toString();
}
public boolean noteGroupingIsLegal() {
if (size() > 1 && get(0) instanceof RhythmicElement) {
RhythmicElement start = (RhythmicElement)get(0);
AugmentedPowerOfTwo firstAugmentedFraction = start.getAugmentedFraction();
for (int index = 1; index < size(); index++) {
if (get(index) instanceof RhythmicElement) {
RhythmicElement element = (RhythmicElement)get(index);
if (element.getPitch() != null)
if (!firstAugmentedFraction.equals(element.getAugmentedFraction()))
return false;
else return false;
} else return false;
}
}
return false;
}
}