/*
* JFugue, an Application Programming Interface (API) for Music Programming
* http://www.jfugue.org
*
* Copyright (C) 2003-2014 David Koelle
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jfugue.tools;
import org.jfugue.midi.MidiFileManager;
import org.jfugue.parser.ParserListener;
import org.jfugue.pattern.Pattern;
import org.jfugue.theory.Key;
import org.jfugue.theory.Note;
import org.staccato.StaccatoParser;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import jp.kshoji.javax.sound.midi.InvalidMidiDataException;
/** Provides <code>Pattern</code> and MIDI analysis of the following elements:
* General descriptors, Pitch descriptors, Duration descriptors, Silence descriptors (rests),
* Interval descriptors (half-steps), Inter Onset Interval (IOI), Harmonic Descriptors (Non-diatonics),
* Rhythm Descriptors (Syncopations), Normality Descriptors.
*
* @Author Grant Mehrer (gtmehrer@gmail.com)
* @Date September 28, 2013
*/
public class GetPatternStats {
private List<Number> pitches = new ArrayList<Number>();
private List<Number> intervals = new ArrayList<Number>();
private List<Number> degreeNonDiatonic = new ArrayList<Number>();
private List<Number> interOI = new ArrayList<Number>();
private List<Number> durations = new ArrayList<Number>();
private List<Number> restDurations = new ArrayList<Number>();
private List<Byte> attacks = new ArrayList<Byte>();
private List<Byte> decays = new ArrayList<Byte>();
private List<TimeEvent> musicEvents= new ArrayList<TimeEvent>();
private int rhythm, measures = 0;
private double tickPos = 0;
private Key key = new Key("Cmaj");
/** Parses JFugue <code>Pattern<code> to calculate statistics
* for all descriptors.
*
* @param pattern The JFuge Pattern to be parsed.
* @param clear True to clear previous data, false to add to previous data
*/
public void parsePattern(Pattern pattern, Boolean clear){
tickPos = 0;
if (clear){
this.clearLists();
}
StaccatoParser sp = new StaccatoParser();
Listener l = new Listener();
sp.addParserListener(l);
sp.parse(pattern.toString());
processEvents();
}
/** Parses MIDI file to calculate statistics for all descriptors
*
* @param midiFile The MIDI file to be parsed.
* @param clear True to clear previous data, false to add to previous data
* @throws java.io.IOException
* @throws InvalidMidiDataException
*/
public void parsePattern(File midiFile, Boolean clear) throws IOException, InvalidMidiDataException {
Pattern midiPattern = new Pattern();
String ext = midiFile.getName().substring(midiFile.getName().lastIndexOf(".")+1, midiFile.getName().length());
if (ext.equalsIgnoreCase("mid")){
Pattern fromMidi = MidiFileManager.loadPatternFromMidi(midiFile);
midiPattern.add(fromMidi.toString());
}
this.parsePattern(midiPattern, clear);
}
private void clearLists(){
pitches.clear();
intervals.clear();
degreeNonDiatonic.clear();
interOI.clear();
durations.clear();
restDurations.clear();
attacks.clear();
decays.clear();
musicEvents.clear();
measures = 0;
rhythm = 0;
}
/**Parses two patterns to find average difference of all stats
*
* @param Pattern Pattern 1 to be parsed
* @param Pattern Pattern 2 to be parsed
* @return The average difference between patterns
*/
public double comparePatterns(Pattern p1, Pattern p2){
List<Number> difference = new ArrayList<Number>();
GetPatternStats pa1 = new GetPatternStats();
GetPatternStats pa2 = new GetPatternStats();
pa1.parsePattern(p1, true);
pa2.parsePattern(p2, true);
difference.add(pa1.getGeneralStats()[0] - pa2.getGeneralStats()[0]);
difference.add(pa1.getGeneralStats()[1] - pa2.getGeneralStats()[1]);
difference.add(pa1.getGeneralStats()[2] - pa2.getGeneralStats()[2]);
findDifference(pa1.pitches, pa2.pitches, this.pitches);
difference.add(pa1.getPitchStats().average - pa2.getPitchStats().average);
//difference.add(pa1.getPitchStats().range - pa2.getPitchStats().range);
difference.add(pa1.getPitchStats().sd - pa2.getPitchStats().sd);
findDifference(pa1.durations, pa2.durations, this.durations);
difference.add(pa1.getDurationStats().average - pa2.getDurationStats().average);
//difference.add(pa1.getDurationStats().range - pa2.getDurationStats().range);
difference.add(pa1.getDurationStats().sd - pa2.getDurationStats().sd);
findDifference(pa1.restDurations, pa2.restDurations, this.restDurations);
difference.add(pa1.getRestStats().average - pa2.getRestStats().average);
//difference.add(pa1.getRestStats().range - pa2.getRestStats().range);
difference.add(pa1.getRestStats().sd - pa2.getRestStats().sd);
findDifference(pa1.intervals, pa2.intervals, this.intervals);
difference.add(pa1.getIntervalStats().average - pa2.getIntervalStats().average);
//difference.add(pa1.getIntervalStats().range - pa2.getIntervalStats().range);
difference.add(pa1.getIntervalStats().sd - pa2.getIntervalStats().sd);
findDifference(pa1.interOI, pa2.interOI, this.interOI);
difference.add(pa1.getIOIStats().average - pa2.getIOIStats().average);
//difference.add(pa1.getIOIStats().range - pa2.getIOIStats().range);
difference.add(pa1.getIOIStats().sd - pa2.getIOIStats().sd);
findDifference(pa1.degreeNonDiatonic, pa2.degreeNonDiatonic, this.degreeNonDiatonic);
difference.add(pa1.getHarmonicStats().average - pa2.getHarmonicStats().average);
//difference.add(pa1.getHarmonicStats().range - pa2.getHarmonicStats().range);
difference.add(pa1.getHarmonicStats().sd - pa2.getHarmonicStats().sd);
rhythm = Math.abs(pa1.getRhythmStats() - pa2.getRhythmStats());
difference.add(pa1.getRhythmStats() - pa2.getRhythmStats());
return Math.abs(computeAverage(difference));
}
private void findDifference(List<Number> p1, List<Number> p2, List<Number> thisList){
if (p1.isEmpty()){
if (!p2.isEmpty()){
for(Number num: p2){
thisList.add(num.doubleValue());
}}}
else if (p2.isEmpty()){
if (!p1.isEmpty()){
for(Number num: p1){
thisList.add(num.doubleValue());
}}}
else {
int count = 0;
for(Number num: p2){
thisList.add(num.doubleValue() - p1.get(count).doubleValue());
++count;
if (count > (p1.size() -1)){break;}
}}
}
/** Gets general statistics (Number of notes, number of rests, number of measures)
*
* @return Array index 0: N of Notes; index 1: N of rests; index 2: N of measures
*/
public int[] getGeneralStats(){
return new int[]{durations.size(),restDurations.size(), measures};
}
/** Gets <code>Stats</code> object containing pitch N, Average (mean - min), SD, and Range.
*
* @return <code>Stats</code> object for pitch
*/
public Stats getPitchStats(){
return new Stats(pitches);
}
/** Gets <code>Stats</code> object containing note duration N, Average (mean - min), SD, and Range.
*
* @return <code>Stats</code> object for note duration
*/
public Stats getDurationStats(){
return new Stats(durations);
}
/** Gets <code>Stats</code> object containing rest duration N, Average (mean - min), SD, and Range.
* Only silences greater than a 16th note are evaluated.
*
* @return <code>Stats</code> object for rest duration
*/
public Stats getRestStats(){
return new Stats(restDurations);
}
/** Gets <code>Stats</code> object containing pitch interval N, Average (mean - min), SD, and Range.
* Intervals are in half-steps.
*
* @return <code>Stats</code> object for pitch interval
*/
public Stats getIntervalStats(){
return new Stats(intervals);
}
/** Gets <code>Stats</code> object containing inter-onset-interval(IOI) N, Average (mean - min), SD, and Range.
* Inter-onset-intervals are the number of MIDI pulses between the onset of non-rest notes.
*
* @return <code>Stats</code> object for IOI
*/
public Stats getIOIStats(){
return new Stats(interOI);
}
/** Gets number of syncopations.
* Syncopations are calculated as any note that begins between beats and extends beyond the next beat.
* ie. C5H C5W C5H - the middle note is syncopated.
*
* @return <code>Stats</code> object for rhythm
*/
public int getRhythmStats(){
return rhythm;
}
/** Gets <code>Stats</code> object containing harmonics N, Average (mean - min), SD, and Range.
* Non-diatonic notes based off of the parsed key signature or default of Cmaj.
* Average is average of 0-4, as a degree of non-diatonic notes. ie. 0: ♭II, 1: ♭III (♮III for minor key), 2: ♭V, 3: ♭VI, 4: ♭VII.
* SD is also computed using degree of non-diatonic notes.
*
* @return <code>Stats</code> object for harmonics
*/
public Stats getHarmonicStats(){
return new Stats(degreeNonDiatonic);
}
/** Sorts time events for chronological processing
*
*/
private void sortTimeEvents(){
Collections.sort(musicEvents, new Comparator<TimeEvent>() {
@Override
public int compare(TimeEvent eg1, TimeEvent eg2) {
if (eg1.time < eg2.time){
return -1;
}
if (eg1.time > eg2.time) {
return 1;
}
else {
return 0;
}}});
}
/** Checks pitch value of note against Key Signature to determine whether value is "non-diatonic"
*
* @param note The note to be checked
* @return True if note is non-diatonic
*/
private boolean checkHarmonics(Note note){
List keyScale;
List<List> majors = new ArrayList<List>();
List<List> minors = new ArrayList<List>();
majors.add(Arrays.asList(0,2,4,5,7,9,11)); //C D E F G A B no flats or sharps
majors.add(Arrays.asList(1,3,5,5,8,10,0)); //Db Eb F Gb Ab Bb C 5 flats //C# D# E# F# G# A# B# 7 sharps
majors.add(Arrays.asList(2,4,6,7,9,11,1 )); //D E F# G A B C# 2 sharps
majors.add(Arrays.asList(3,4,7,8,10,0,2)); //Eb F G Ab Bb C D 3 flats
majors.add(Arrays.asList(4,6,8,9,11,1,3)); //E F# G# A B C# D# 4 sharps
majors.add(Arrays.asList(5,7,9,10,0,1,4)); //F G A Bb C D E 1 flat
majors.add(Arrays.asList(6,8,10,11,1,3,5)); //Gb Ab Bb Cb Db Eb F 6 flats //F# G# A# B C# D# E# 6 sharps
majors.add(Arrays.asList(7,9,11,0,2,4,6)); //G A B C D E F# 1 sharp
majors.add(Arrays.asList(8,10,0,1,3,5,7)); //Ab Bb C Db Eb F G 4 flats
majors.add(Arrays.asList(9,11,1,2,4,6,8)); //A B C# D E F# G# 3 sharps
majors.add(Arrays.asList(10,0,2,3,5,7,9)); //Bb C D Eb F G A 2 flats
majors.add(Arrays.asList(11,1,3,4,6,8,10)); //Cb Db Eb Fb Gb Ab Bb 7 flats //B C# D# E F# G# A# 5 sharps
minors.add(Arrays.asList(3,4,7,8,10,0,2)); //C D Eb F G Ab Bb 3 flats
minors.add(Arrays.asList(4,6,8,9,11,1,3)); // C# D# E F# G# A B 4 sharps
minors.add(Arrays.asList(5,7,9,10,0,1,4)); // D E F G A Bb C 1 flat
minors.add(Arrays.asList(6,8,10,11,1,3,5)); //Eb F Gb Ab Bb Cb Db 6 flats /D# E# F# G# A# B C# 6 sharps
minors.add(Arrays.asList(7,9,11,0,2,4,6)); // E F# G A B C D 1 sharp
minors.add(Arrays.asList(8,10,0,1,3,5,7)); //F G Ab Bb C Db Eb 4 flats
minors.add(Arrays.asList(9,11,1,2,4,6,8)); //F# G# A B C# D E 3 sharps
minors.add(Arrays.asList(10,0,2,3,5,7,9)); // G A Bb C D Eb F 2 flats
minors.add(Arrays.asList(11,1,3,4,6,8,10)); //G# A# B C# D# E F# 5 sharps /Ab Bb Cb Db Eb Fb Gb 7 flats
minors.add(Arrays.asList(0,2,4,5,7,9,11)); // A B C D E F G no flats or sharps
minors.add(Arrays.asList(1,3,5,5,8,10,0)); //Bb C Db Eb F Gb Ab 5 flats /A# B# C# D# E# F# G# 7 sharps
minors.add(Arrays.asList(2,4,6,7,9,11,1 )); //B C# D E F# G A 2 sharps
if (key.getScale().getMajorOrMinorIndicator()==0){ //0 for major
keyScale = majors.get(reduceValue(key.getRoot().getValue()));
}
else{
keyScale = minors.get(reduceValue(key.getRoot().getValue()));
}
if (!keyScale.contains(reduceValue(note.getValue()))){
return true;
}
return false;
}
/** Reduces a note value (0-128) to its equivalent in the lowest octave.
*
* @param v The note value to be reduced
* @return The reduced value
*/
private int reduceValue(int v) {
return v % 12;
}
/** Checks the degree of non-diatonics and adds the degree to list
*
* @param n The note to have it's value checked
*/
private void checkDegree(Number n){
Integer rootValue = reduceValue(key.getRoot().getValue());
int noteValue = n.intValue();
//degrees are: 0: ♭II, 1: ♭III (♮III for minor key), 2: ♭V, 3: ♭VI, 4: ♭VII.
if (noteValue < rootValue){
noteValue += 12;
}
int difference = noteValue - rootValue;
switch (difference){
case 0: case 1:
degreeNonDiatonic.add(0);
break;
case 2: case 3: case 4:
degreeNonDiatonic.add(1);
break;
case 5: case 6:
degreeNonDiatonic.add(2);
break;
case 7: case 8:
degreeNonDiatonic.add(3);
break;
case 9: case 10: case 11:
degreeNonDiatonic.add(4);
break;
default:
break;
}
}
/** Process all events in chronological order.
* - add pitch, duration, etc. to lists for further statistical processing
*/
private void processEvents(){
sortTimeEvents();
double lastTime = 0;
double ioi = 0.0;
int ticks = 0;
byte interval = 60;
sortTimeEvents();
//Find first note event and get first stats for relative calculations
for (TimeEvent t : musicEvents){
if (t.getEvent() instanceof Note || t.getEvent() instanceof org.jfugue.theory.Chord){
Note note = (Note)t.getEvent();
interval = note.getValue(); //set interval to fist note value
lastTime = t.time; //get first time
durations.add(note.getDuration()); //add first duration
ioi = (int)convertDecimalToTicks(note.getDuration());
break;
}
}
for (TimeEvent t : musicEvents){
Note note;
//If event is note, collect stats
if (t.getEvent() instanceof Note || t.getEvent() instanceof org.jfugue.theory.Chord){
if (t.getEvent() instanceof org.jfugue.theory.Chord){
org.jfugue.theory.Chord c = (org.jfugue.theory.Chord)t.getEvent();
note = c.getRoot();
}
else{
note = (Note)t.getEvent();
}
int noteTicks = (int)convertDecimalToTicks(note.getDuration());
if (!note.isRest()){
pitches.add(note.getValue());
attacks.add(note.getOnVelocity());
decays.add(note.getOffVelocity());
//check for non-diatonics
if (checkHarmonics(note)){
checkDegree(reduceValue(note.getValue()));
}
//check for syncopations
if (ticks%128 > 15 && ticks%128 < 112
&& noteTicks > (142 - ticks%128)){ //16 TICKS IS 32ND NOTE 16+128 = 142
rhythm++;
}
ticks = ticks + noteTicks;
interval = (byte)(Math.abs(note.getValue() - interval));
intervals.add(interval);
interval = note.getValue();
if (lastTime != t.time) {
durations.add(note.getDuration()); //ADD NOTE IF IT IS NOT SYNCRONOUS
interOI.add(ioi);
ioi = noteTicks;
}
lastTime = t.time;
}
else {
ioi = ioi + noteTicks;
ticks = ticks + noteTicks;
lastTime = t.time;
if (note.getDuration() > .0615){
restDurations.add(note.getDuration());
}
}
}
else if (t.getEvent() instanceof Key){
key = (Key)t.getEvent();
}
}
if (intervals.size() > 0){
intervals.remove(0); //remove first interval (first note value)
}
}
private long convertBeatsToTicks(double time) {
long durationInTicks = (long)(time * 512);
return durationInTicks;
}
private double convertDecimalToTicks(double decimalDuration){
double wholeNoteinTicks = 512; //128*4
return decimalDuration * wholeNoteinTicks;
}
private double calcAverage(List<Number> list){
//AVERAGES: computed as (mean-min)
if (calcN(list)==0){
return 0;
}
Double average;
Double total = 0.0;
Double min = list.get(0).doubleValue();
for (Number n : list){
Double d = n.doubleValue();
total = total + d;
if(min > d){
min = d;
}}
average = (total/list.size()) - min;
return average;
}
private double calcSD(List<Number> list){
if (calcN(list)==0){
return 0;
}
double average;
List<Number> squares = new ArrayList<Number>();
average = computeAverage(list);
for (Number n : list){
squares.add(Math.pow(n.doubleValue()-average, 2));
}
average = Math.sqrt(computeAverage(squares));
return average;
}
private double calcRange(List<Number> list){
if (calcN(list)==0){
return 0;
}
Double max = list.get(0).doubleValue();
Double min = list.get(0).doubleValue();
Double range;
for (Number n : list){
double d = n.doubleValue();
if(min > d){
min = d;
}
if(max < d){
max = d;
}}
range = max - min;
return range;
}
private int calcN(List<Number> list){
int n = list.size();
return n;
}
private double computeAverage(List<Number> list){
Double total = 0.0;
for (Number n : list){
total = total + n.doubleValue();
}
double average = (total/list.size());
return average;
}
@Override
public String toString(){
int[] general = this.getGeneralStats();
Stats duration = this.getDurationStats();
Stats pitch = this.getPitchStats();
Stats ioi = this.getIOIStats();
Stats interval = this.getIntervalStats();
Stats silence = this.getRestStats();
Stats harmonics = this.getHarmonicStats();
return ("General Stats: \n Notes = " + general[0] + "\n Silences = " + general[1]
+ "\nInterval Stats: n = " + interval.getN() + " \n Average = " + interval.getAverage()
+ "\n Range = " + interval.getRange() + "\n SD = " + interval.getSD()
+ "\nIOI Stats: n = " + ioi.getN() + "\n Average = " + ioi.getAverage()
+ "\n Range = " + ioi.getRange() + "\n SD = " + ioi.getSD()
+ "\nDuration Stats: n = " + duration.getN() + "\n Average = " + duration.getAverage()
+ "\n Range = " + duration.getRange() + "\n SD = " + duration.getSD()
+ "\nPitch Stats: n = " + pitch.getN() + "\n Average = " + pitch.getAverage()
+ "\n Range = " + pitch.getRange() + "\n SD = " + pitch.getSD()
+ "\nSilence Stats: n = " + silence.getN() + "\n Average = " + silence.getAverage()
+ "\n Range = " + silence.getRange() + "\n SD = " + silence.getSD()
+ "\nHarmonic Stats: n = " + harmonics.getN() + "\n Average = " + harmonics.getAverage()
+ "\n Range = " + harmonics.getRange() + "\n SD = " + silence.getSD()
+ "\nRythm Stats: n = " + this.getRhythmStats());
}
private class TimeEvent<T> {
double time; //in ticks
T eventValue;
private TimeEvent(double ticks, T event){
time = ticks;
eventValue = event;
}
private T getEvent(){
return eventValue;
}
}
public final class Stats{
private int n;
private double range;
private double sd;
private double average;
private Stats(List<Number> list){
n = calcN(list);
range = calcRange(list);
sd = calcSD(list);
average = calcAverage(list);
}
private Stats(List<Number> list1, List<Number> list2){
n = calcN(list1);
range = calcRange(list1);
sd = calcSD(list1);
average = calcAverage(list2);
}
public int getN(){
return n;
}
/**Get the population range
*
* @return Population range
*/
public double getRange(){
return range;
}
/**Get the Population Standard Deviation
*
* @return Population SD
*/
public double getSD(){
return sd;
}
/**Get the average (mean - min)
*
* @return (mean - min)
*/
public double getAverage(){
return average;
}
}
private class Listener implements ParserListener {
@Override
public void beforeParsingStarts() {
// System.out.println("Begin Parse");
}
@Override
public void afterParsingFinished() {
}
@Override
public void onTrackChanged(byte t) {
musicEvents.add(new TimeEvent<Byte>(tickPos, t));
}
@Override
public void onLayerChanged(byte layerNum) {
musicEvents.add(new TimeEvent<Byte>(tickPos, layerNum));
tickPos = 0;
}
@Override
public void onInstrumentParsed(byte i) {
musicEvents.add(new TimeEvent<Byte>(tickPos, i));
}
@Override
public void onTempoChanged(int tBPM) {
musicEvents.add(new TimeEvent<Integer>(tickPos, tBPM));
}
@Override
public void onKeySignatureParsed(byte keyB, byte scale) {
Key k;
String maj = ("maj"), min = ("min");
if (scale == 0){
k = new Key(new Note(keyB).toString() + maj);
}
else{
k = new Key(new Note(keyB).toString() + min);
}
musicEvents.add(new TimeEvent<Key>(tickPos, k));
}
@Override
public void onTimeSignatureParsed(byte bDuration, byte bNumber) {
musicEvents.add(new TimeEvent<Byte[]>(tickPos, new Byte[]{bDuration,bNumber}));
}
@Override
public void onBarLineParsed(long m) {
musicEvents.add(new TimeEvent<Long>(tickPos, m));
++measures;
}
@Override
public void onTrackBeatTimeBookmarked(String string) {
// System.out.println("Time bookmarked");
}
@Override
public void onTrackBeatTimeBookmarkRequested(String string) {
// System.out.println("Time bookmark requested");
}
@Override
public void onTrackBeatTimeRequested(double beats) {
tickPos = convertBeatsToTicks(beats);
}
@Override
public void onPitchWheelParsed(byte b, byte b1) {
musicEvents.add(new TimeEvent<Byte[]>(tickPos, new Byte[]{b,b1}));
}
@Override
public void onChannelPressureParsed(byte b) {
musicEvents.add(new TimeEvent<Byte>(tickPos, b));
}
@Override
public void onPolyphonicPressureParsed(byte b, byte b1) {
musicEvents.add(new TimeEvent<Byte[]>(tickPos, new Byte[]{b,b1}));
}
@Override
public void onSystemExclusiveParsed(byte... bytes) {
//Byte[] sysEx = new Byte[bytes.length];
//System.arraycopy(bytes, 0, sysEx, 0, bytes.length);
// musicEvents.add(new TimeEvent<Byte[]>(tickPos, sysEx));
/*
* Exception in thread "AWT-EventQueue-0" java.lang.ArrayStoreException
at java.lang.System.arraycopy(Native Method)
at music.generator.PatternAnalyzer$Listener.onSystemExclusiveParsed(PatternAnalyzer.java:763)
at org.jfugue.parser.ParserContext.fireSystemExclusiveParsed(ParserContext.java:253)
at org.jfugue.functions.SysexFunction.apply(SysexFunction.java:50)
at org.staccato.FunctionSubparser.parse(FunctionSubparser.java:53)
at org.staccato.StaccatoParser.parse(StaccatoParser.java:98)
at music.generator.PatternAnalyzer.parsePattern(PatternAnalyzer.java:77)
at music.generator.PatternAnalyzer.parsePattern(PatternAnalyzer.java:106)
*/
}
@Override
public void onControllerEventParsed(byte b, byte b1) {
musicEvents.add(new TimeEvent<Byte[]>(tickPos, new Byte[]{b,b1}));
}
@Override
public void onLyricParsed(String lyric) {
musicEvents.add(new TimeEvent<String>(tickPos, lyric));
}
@Override
public void onMarkerParsed(String string) {
musicEvents.add(new TimeEvent<String>(tickPos, string));
}
@Override
public void onFunctionParsed(String string, Object o) {
musicEvents.add(new TimeEvent<Object>(tickPos, o));
}
@Override
public void onNoteParsed(Note note) {
musicEvents.add(new TimeEvent<Note>(tickPos, note));
tickPos = tickPos + convertDecimalToTicks(note.getDuration()); //advance tick position
}
@Override
public void onChordParsed(org.jfugue.theory.Chord chord) {
musicEvents.add(new TimeEvent<org.jfugue.theory.Chord>(tickPos, chord));
tickPos = tickPos + convertDecimalToTicks(chord.getRoot().getDuration());
}
}
}