Skip to content

Instantly share code, notes, and snippets.

@kbridbur
Created January 10, 2017 23:31
Show Gist options
  • Save kbridbur/b0b9c413389139e79b08b94898b2ce7d to your computer and use it in GitHub Desktop.
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
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