Created
January 10, 2017 23:31
-
-
Save kbridbur/b0b9c413389139e79b08b94898b2ce7d to your computer and use it in GitHub Desktop.
Portion of a class final project in which we created a parser and player for txt files containing music
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package abc.parser; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import abc.sound.Chord; | |
import abc.sound.EmptyVoice; | |
import abc.sound.Note; | |
import abc.sound.Rest; | |
import abc.sound.Single; | |
import abc.sound.Song; | |
import abc.sound.Voice; | |
import abc.sound.VoiceInterface; | |
import lib6005.parser.ParseTree; | |
public class BodyParser { | |
//This will turn into ticks per beat, used to make sure every note is a full integer length | |
public static int LCMDenominator = 6; //start at 6 to accommodate triplets and quadruplets | |
public enum bodyGrammar { | |
ROOT, SINGLENOTE, NOTE, COMMENT, TUPLET, BARLINE, ENDING, WHITESPACE, REST, | |
CHORD, SONG, ELEMENT, ABCLINE, VOICE, DURATION, ACCIDENTAL, | |
OCTAVE, BASENOTE, MIDTUNEFIELD, STARTREPEAT, ENDREPEAT, TEXT, | |
WHOLENUMBER, FRACTION, NEWLINE, DIGIT, ENDOFLINE | |
} | |
//Whether or not each voice should be recording the notes it is playing to be used in a repeat | |
public static Map<String, Boolean> recording = new HashMap<String, Boolean>(); | |
//Lists of recorded notes for repeats | |
public static Map<String, List<Note>> recordedNotes = new HashMap<String, List<Note>>(); | |
//Default accidentals for this measure, reset each measure | |
public static Map<String, String> defaultAccidentals = new HashMap<String, String>(); | |
//Record the incoming notes to the recorded notes dictionary (do when placing a repeat) | |
public static boolean recordThese = true; | |
//keeps track of measures reset to 0 every time a measure is ended | |
public static double measureCount = 0; | |
//Duration key to get the unaltered duration from notes | |
public static final String DURATION_KEY = "1/1"; | |
public static final int NOTES_PER_MEASURE = 4; | |
public static Object buildSong(ParseTree<bodyGrammar> tree, VoiceInterface currentVoice) { | |
switch(tree.getName()) { | |
/* | |
* All trees start with a root, we want the song which will always be a child of this | |
*/ | |
case ROOT: | |
resetDefaultAccidentals(); | |
return buildSong(tree.childrenByName(bodyGrammar.SONG).get(0), currentVoice); | |
/* | |
* Look at the note child and parse that | |
*/ | |
case SINGLENOTE: | |
return buildSong(tree.childrenByName(bodyGrammar.NOTE).get(0), currentVoice); | |
/* | |
* Get the specifics of the note and return a List with that single note | |
*/ | |
case NOTE: | |
//look at children for fields and then add a new note to current voice | |
double durationModifier = 1; | |
String pitch = ""; | |
for (ParseTree<bodyGrammar> child : tree.children()){ | |
if (child.getName().equals(bodyGrammar.DURATION)) { | |
ParseTree<bodyGrammar> fracOrWhole = child.children().get(0); | |
durationModifier = (double) buildSong(fracOrWhole, currentVoice); | |
} else if (child.getName().equals(bodyGrammar.ACCIDENTAL) | |
|| child.getName().equals(bodyGrammar.OCTAVE) | |
|| child.getName().equals(bodyGrammar.BASENOTE)){ | |
pitch += child.getContents(); | |
} | |
} | |
List<Note> notes = Arrays.asList(new Single(pitch, durationModifier)); | |
return notes; | |
/* | |
* Get the specifics for each note and modify their duration and starting time based on the type of tuplet | |
* return a list containing all notes in the tuplet | |
*/ | |
case TUPLET: | |
//add new notes with some duration modifier | |
List<Note> tupletNotes = new ArrayList<Note>(); | |
final int numNotes = tree.childrenByName(bodyGrammar.NOTE).size(); | |
final double singleDuration; | |
if (numNotes > 2){ | |
singleDuration = (numNotes-1)/((double) numNotes); | |
} else{ | |
singleDuration = 1.5; | |
} | |
for (ParseTree<bodyGrammar> child : tree.childrenByName(bodyGrammar.NOTE)){ | |
String singlePitch = ""; | |
for (ParseTree<bodyGrammar> singleChild : child.children()){ | |
if (singleChild.getName().equals(bodyGrammar.ACCIDENTAL) | |
|| singleChild.getName().equals(bodyGrammar.OCTAVE) | |
|| singleChild.getName().equals(bodyGrammar.BASENOTE)) { | |
singlePitch += singleChild.getContents(); | |
} | |
} | |
Single newNote = new Single(singlePitch, singleDuration); | |
tupletNotes.add(newNote); | |
} | |
return tupletNotes; | |
/* | |
* get the duration of the rest and return a list with a single rest object | |
*/ | |
case REST: | |
//make a new rest and add to current voice | |
List<Note> rest = new ArrayList<Note>(); | |
double restDurationModifier = 1; | |
for (ParseTree<bodyGrammar> child : tree.childrenByName(bodyGrammar.DURATION)){ | |
restDurationModifier = (double) buildSong(child.children().get(0), currentVoice); | |
} | |
rest.add(new Rest(restDurationModifier)); | |
return rest; | |
/* | |
* return a list of all the notes in the chord, all with the same start time and duration | |
*/ | |
case CHORD: | |
List<Single> chordNotes = new ArrayList<Single>(); | |
for (ParseTree<bodyGrammar> child : tree.childrenByName(bodyGrammar.NOTE)){ | |
double singleDurationModifier = 1; | |
String singlePitch = ""; | |
for (ParseTree<bodyGrammar> noteChild : child.children()){ | |
if (noteChild.getName().equals(bodyGrammar.DURATION)) { | |
ParseTree<bodyGrammar> fracOrWhole = noteChild.children().get(0); | |
singleDurationModifier = (double) buildSong(fracOrWhole, currentVoice); | |
} else if (noteChild.getName().equals(bodyGrammar.ACCIDENTAL) | |
|| noteChild.getName().equals(bodyGrammar.OCTAVE) | |
|| noteChild.getName().equals(bodyGrammar.BASENOTE)) { | |
singlePitch += noteChild.getContents(); | |
} | |
} | |
chordNotes.add(new Single(singlePitch, singleDurationModifier)); | |
} | |
List<Note> chord = Arrays.asList(new Chord(chordNotes)); | |
return chord; | |
//need to find some way to make this case external so variables can be saved during recursion | |
/* | |
* Assemble each voice from its elements and return a song containing all voices | |
*/ | |
case SONG: | |
//look at children, make a dictionary of voice names to abclines then run through each abcline for each voice adding notes | |
Map<Voice, List<ParseTree<bodyGrammar>>> voiceNames = buildVoiceMap(tree); | |
List<Voice> voices = new ArrayList<Voice>(); | |
voices.addAll(voiceNames.keySet()); | |
for (Voice voice : voices){ | |
for (ParseTree<bodyGrammar> abcline : voiceNames.get(voice)){ //look at all the abclines in each voice | |
for (ParseTree<bodyGrammar> element : abcline.childrenByName(bodyGrammar.ELEMENT)){ //look at all the elements in side the voice | |
Object returned = buildSong(element, voice); //build what's inside the element to get a list of notes | |
if (returned == null){ | |
continue; //returned was a newline or a barline | |
} else{ | |
for (int i = 0; i < ((List<Note>) returned).size(); i++){ | |
Note noteToAdd = BodyParser.setDefaultAccidentals(((List<Note>) returned).get(i)); | |
voice.addNote(noteToAdd); | |
if (recording.get(voice.getName()) && recordThese){ | |
recordedNotes.get(voice.getName()).add(noteToAdd); | |
} | |
} | |
recordThese = true; | |
} | |
} | |
} | |
} | |
return new Song(voices); | |
/* | |
* Re-route to appropriate case based on the type of element passed in, filter out unnecessary cases | |
*/ | |
case ELEMENT: | |
//element ::= note | tuplet | barline | ending | whitespace | startrepeat | endrepeat | rest | chord; | |
if (!tree.childrenByName(bodyGrammar.CHORD).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.CHORD).get(0), currentVoice); | |
} else if(!tree.childrenByName(bodyGrammar.TUPLET).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.TUPLET).get(0), currentVoice); | |
} else if(!tree.childrenByName(bodyGrammar.SINGLENOTE).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.SINGLENOTE).get(0), currentVoice); | |
} else if(!tree.childrenByName(bodyGrammar.REST).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.REST).get(0), currentVoice); | |
} else if(!tree.childrenByName(bodyGrammar.STARTREPEAT).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.STARTREPEAT).get(0), currentVoice); | |
} else if(!tree.childrenByName(bodyGrammar.ENDREPEAT).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.ENDREPEAT).get(0), currentVoice); | |
} else if(!tree.childrenByName(bodyGrammar.ENDING).isEmpty()){ | |
return buildSong(tree.childrenByName(bodyGrammar.ENDING).get(0), currentVoice); | |
} else{ | |
return null; | |
} | |
/* | |
* Get value from children and return double value | |
*/ | |
case FRACTION: | |
List<ParseTree<bodyGrammar>> fraction = tree.childrenByName(bodyGrammar.WHOLENUMBER); | |
double denom; | |
if (fraction.size() == 0){ | |
return 1.0; | |
} | |
else if (fraction.size() == 1){ | |
denom = (double) buildSong(fraction.get(0), currentVoice); | |
LCMDenominator = LeastCommonMultiple(LCMDenominator, (int) denom); | |
return 1.0/denom; | |
} else{ | |
denom = (double) buildSong(fraction.get(1), currentVoice); | |
LCMDenominator = LeastCommonMultiple(LCMDenominator, (int) denom); | |
return ((double) buildSong(fraction.get(0), currentVoice))/denom; | |
} | |
/* | |
* Return the value within the whole number | |
*/ | |
case WHOLENUMBER: | |
return Double.parseDouble(tree.children().get(0).getContents()); | |
/* | |
* Return the saved repeat notes to be appended to the current song, do not add these repeated notes to the recorded notes | |
*/ | |
case ENDREPEAT: | |
recordThese = false; | |
List<Note> currentRecordedNotes = recordedNotes.get(currentVoice.getName()); | |
return currentRecordedNotes; | |
/* | |
* Clear currently recorded notes | |
*/ | |
case STARTREPEAT: | |
recordedNotes.put(currentVoice.getName(), new ArrayList<Note>()); | |
return null; | |
/* | |
* Stop recording notes so the first ending is not included after the next end repeat | |
*/ | |
case ENDING: | |
//set recording to false | |
recording.put(currentVoice.getName(), false); | |
return null; | |
/* | |
* The following cases should never be gone to, if they do print an error message but do not stop the program | |
*/ | |
case COMMENT: | |
System.out.println("Error: Unexpected Comment found"); | |
//ignore | |
case ABCLINE: | |
System.out.println("Error: Unexpected ABCLine found"); | |
//should never be called | |
case VOICE: | |
System.out.println("Error: Unexpected Voice found"); | |
//should never be called | |
case DURATION: | |
System.out.println("Error: Unexpected Duration found"); | |
//should never be called | |
case ACCIDENTAL: | |
System.out.println("Error: Unexpected Accidental found"); | |
//should never be called | |
case BASENOTE: | |
System.out.println("Error: Unexpected Basenote found"); | |
//should never be called | |
case OCTAVE: | |
System.out.println("Error: Unexpected Octave found"); | |
//should never be called | |
case MIDTUNEFIELD: | |
System.out.println("Error: Unexpected Midtunefield found"); | |
//should never be called | |
case TEXT: | |
System.out.println("Error: Unexpected Text found"); | |
//should never be called | |
case BARLINE: | |
System.out.println("Error: Unexpected Barline found"); | |
//ignore | |
case WHITESPACE: | |
System.out.println("Error: Unexpected Whitespace found"); | |
//ignore | |
case NEWLINE: | |
System.out.println("Error: Unexpected Newline found"); | |
//ignore | |
case DIGIT: | |
System.out.println("Error: Unexpected digit found"); | |
//ignore | |
default: | |
break; | |
} | |
return new Song(null); //should never get here | |
} | |
/** | |
* @param note to apply default to or use as default | |
* @return modified note with correct accidental | |
*/ | |
private static Note setDefaultAccidentals(Note note){ | |
if (note.getSingles().size() == 1){ | |
if (!note.getSingles().get(0).isPlayable()){ | |
measureCount = (measureCount + note.getDuration(DURATION_KEY))%NOTES_PER_MEASURE; | |
return note.getSingles().get(0); | |
} | |
Single modifiedNote; | |
measureCount = (measureCount + note.getDuration(DURATION_KEY))%NOTES_PER_MEASURE; | |
String currentPitch = note.getSingles().get(0).getPitchField(); //may need different handling for chords | |
String finalPitch; | |
String charZero = Character.toString(currentPitch.charAt(0)); | |
if (!(currentPitch.contains("^") || currentPitch.contains("_") || currentPitch.contains("="))){ | |
finalPitch = defaultAccidentals.get(charZero) + currentPitch; | |
} else{ | |
String charOne = Character.toString(currentPitch.charAt(1)); | |
finalPitch = currentPitch; | |
//something to set it | |
if (defaultAccidentals.get(charOne).equals("")) { | |
defaultAccidentals.put(charOne, charZero); | |
} | |
} | |
modifiedNote = new Single(finalPitch, note.getSingles().get(0).getDurationField()); | |
//do this after the note, as this is the last note in the measure | |
if (measureCount == 0){ | |
resetDefaultAccidentals(); | |
} | |
return modifiedNote; | |
} else{ | |
List<Single> modifiedChordNotes = new ArrayList<Single>(); | |
for (Single single : note.getSingles()){ | |
modifiedChordNotes.add((Single) setDefaultAccidentals(single)); | |
} | |
return new Chord(modifiedChordNotes); | |
} | |
} | |
/** | |
* Resets the state of the default accidentals map, to be called between measures | |
*/ | |
private static void resetDefaultAccidentals(){ | |
defaultAccidentals.put("A", ""); | |
defaultAccidentals.put("B", ""); | |
defaultAccidentals.put("C", ""); | |
defaultAccidentals.put("D", ""); | |
defaultAccidentals.put("E", ""); | |
defaultAccidentals.put("F", ""); | |
defaultAccidentals.put("G", ""); | |
defaultAccidentals.put("a", ""); | |
defaultAccidentals.put("b", ""); | |
defaultAccidentals.put("c", ""); | |
defaultAccidentals.put("d", ""); | |
defaultAccidentals.put("e", ""); | |
defaultAccidentals.put("f", ""); | |
defaultAccidentals.put("g", ""); | |
} | |
/** | |
* @param tree takes in a parse tree with top level being a song | |
* @return map of voices in the song to lists of notes they contain | |
*/ | |
private static Map<Voice, List<ParseTree<bodyGrammar>>> buildVoiceMap(ParseTree<bodyGrammar> tree){ | |
Map<Voice, List<ParseTree<bodyGrammar>>> voiceMap = new HashMap<Voice, List<ParseTree<bodyGrammar>>>(); | |
Map<String, Voice> voiceNameMap = new HashMap<String, Voice>(); | |
String currentLineVoiceName = "defaultVoice"; | |
//Set up default in case no voices are specified in body | |
Voice defaultVoice = new Voice(currentLineVoiceName); | |
//Add default to all the required fields | |
voiceMap.put(defaultVoice, new ArrayList<ParseTree<bodyGrammar>>()); | |
voiceNameMap.put(currentLineVoiceName, defaultVoice); | |
recording.put(defaultVoice.getName(), true); | |
recordedNotes.put(defaultVoice.getName(), new ArrayList<Note>()); | |
for (ParseTree<bodyGrammar> abcline : tree.childrenByName(bodyGrammar.ABCLINE)){ //each child is an abcline | |
//Look through the rest | |
for (ParseTree<bodyGrammar> line : abcline.children()){ | |
if (line.getName().equals(bodyGrammar.MIDTUNEFIELD)) { //set current voice using mid tune field | |
currentLineVoiceName = line.childrenByName(bodyGrammar.TEXT).get(0).getContents(); //get name of voice | |
if (!voiceNameMap.containsKey(currentLineVoiceName)){ | |
Voice newVoice = new Voice(currentLineVoiceName); | |
//Add new voice to all maps | |
voiceNameMap.put(currentLineVoiceName, newVoice); | |
voiceMap.put(newVoice, new ArrayList<ParseTree<bodyGrammar>>()); //Just not ADDING | |
recording.put(newVoice.getName(), true); | |
recordedNotes.put(newVoice.getName(), new ArrayList<Note>()); | |
} | |
//add abclines of notes to current voice | |
} else if(line.getName().equals(bodyGrammar.VOICE)) { //add parse trees of each voice to the appropriate list in the hash map | |
voiceMap.get(voiceNameMap.get(currentLineVoiceName)).add(line); //OVERWRITING NOT ADDING | |
} | |
} | |
} | |
//if default voice is unused remove it from the maps | |
List<ParseTree<bodyGrammar>> defaultVoiceObj = voiceMap.get(defaultVoice); | |
if (voiceMap.get(defaultVoice).isEmpty()) { | |
voiceMap.remove(defaultVoice); | |
recording.remove("defaultVoice"); | |
recordedNotes.remove("defaultVoice"); | |
voiceNameMap.remove("defaultVoice"); | |
} | |
//all the voices have been added, check element stuff now | |
return voiceMap; | |
} | |
/** | |
* Simple version of finding common multiple of two integers | |
* @param a non zero positive integer | |
* @param b non zero positive integer | |
* @return common multiple of integers | |
*/ | |
public static int LeastCommonMultiple(int a, int b){ | |
if (a % b == 0){ | |
return a; | |
} else if (b % a == 0){ | |
return b; | |
} else { | |
return a*b; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment