From f96000ca339244bded0bc031fa3c1265d63149ff Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Thu, 5 Nov 2020 18:29:50 +0100 Subject: [PATCH 01/13] Distance calculation much faster --- src/mv2h/tools/Aligner.java | 180 ++++++++++++++---------- src/mv2h/tools/Converter.java | 70 +++++----- src/mv2h/tools/MusicXmlConverter.java | 188 +++++++++++++------------- 3 files changed, 241 insertions(+), 197 deletions(-) diff --git a/src/mv2h/tools/Aligner.java b/src/mv2h/tools/Aligner.java index dc95cd2..cbe86c7 100644 --- a/src/mv2h/tools/Aligner.java +++ b/src/mv2h/tools/Aligner.java @@ -2,9 +2,11 @@ import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.Map.Entry; import mv2h.Main; import mv2h.objects.Music; @@ -15,18 +17,18 @@ * All of its methods are static, and it uses a heuristic-based dynamic time warp to get a number of * candidate alignments {@link #getPossibleAlignments(Music, Music)}, and can be used to convert * the times of a transcription based on one of those alignments - * {@link #convertTime(int, Music, Music, List)}. - * + * {@link #convertTime(int, Music, Music, List)}. + * * @author Andrew McLeod */ public class Aligner { - + /** * Get all possible alignments of the given ground truth and transcription. - * + * * @param gt The ground truth. * @param m The transcription. - * + * * @return A Set of all possible alignments of the transcription to the ground truth. * An alignment is a list containing, for each ground truth note list, the index of the transcription * note list to which it is aligned, or -1 if it was not aligned with any transcription note. @@ -35,36 +37,36 @@ public static Set> getPossibleAlignments(Music gt, Music m) { double[][] distances = getAlignmentMatrix(gt.getNoteLists(), m.getNoteLists()); return getPossibleAlignmentsFromMatrix(distances.length - 1, distances[0].length - 1, distances); } - + /** * A recursive function to get all of the possible alignments which lead to * the optimal distance from the distance matrix returned by the heuristic-based DTW in * {@link #getAlignmentMatrix(List, List)}, up to matrix indices i, j. - * + * * @param i The first index, representing the transcribed note index. * @param j The second index, representing the ground truth note index. * @param distances The full distances matrix from {@link #getAlignmentMatrix(List, List)}. - * + * * @return A Set of all possible alignments given the distance matrix, up to notes i, j. * An alignment is a list containing, for each ground truth note list, the index of the transcription * note list to which it is aligned, or -1 if it was not aligned with any transcription note. */ private static Set> getPossibleAlignmentsFromMatrix(int i, int j, double[][] distances) { Set> alignments = new HashSet>(); - + // Base case. we are at the beginning and nothing else needs to be aligned. if (i == 0 && j == 0) { alignments.add(new ArrayList()); return alignments; } - + double min = Math.min(Math.min( i > 0 ? distances[i - 1][j] : Double.POSITIVE_INFINITY, j > 0 ? distances[i][j - 1] : Double.POSITIVE_INFINITY), i > 0 && j > 0 ? distances[i - 1][j - 1] : Double.POSITIVE_INFINITY); - + // Note that we could perform multiple of these if blocks. - + // This transcription note was aligned with nothing in the ground truth. Add -1. if (distances[i - 1][j] == min) { for (List list : getPossibleAlignmentsFromMatrix(i - 1, j, distances)) { @@ -72,14 +74,14 @@ private static Set> getPossibleAlignmentsFromMatrix(int i, int j, alignments.add(list); } } - + // This ground truth note was aligned with nothing in the transcription. Skip it. if (distances[i][j - 1] == min) { for (List list : getPossibleAlignmentsFromMatrix(i, j - 1, distances)) { alignments.add(list); } } - + // The current transcription and ground truth notes were aligned. Add the current ground // truth index to the alignment list. if (distances[i - 1][j - 1] == min) { @@ -88,37 +90,42 @@ private static Set> getPossibleAlignmentsFromMatrix(int i, int j, alignments.add(list); } } - + return alignments; } - + /** * Get the Dynamic Time Warping distance matrix from the note lists. *
* During calculation, we add an additional 0.01 penalty to any aligned note whose previous * notes (in both ground truth and transcription) were not aligned. This is used to prefer * alignments which align many consecutive notes. - * + * * @param gtNotes The ground truth note lists, split by onset time. * @param mNotes The transcribed note lists, split by onset time. - * + * * @return The DTW distance matrix. */ private static double[][] getAlignmentMatrix(List> gtNotes, List> mNotes) { + List> gtNoteMaps = getNotePitchMaps(gtNotes); + List> mNoteMaps = getNotePitchMaps(mNotes); + double[][] distances = new double[gtNotes.size() + 1][mNotes.size() + 1]; - + for (int i = 1; i < distances.length; i++) { distances[i][0] = Double.POSITIVE_INFINITY; } - + for (int j = 1; j < distances[0].length; j++) { distances[0][j] = Double.POSITIVE_INFINITY; } - + + boolean useProgressBar = distances[0].length >= 100; + for (int j = 1; j < distances[0].length; j++) { for (int i = 1; i < distances.length; i++) { - double distance = getDistance(gtNotes.get(i - 1), mNotes.get(j - 1)); - + double distance = getDistance(gtNoteMaps.get(i - 1), mNoteMaps.get(j - 1)); + double min = Math.min(Math.min( distances[i - 1][j] + 0.6, distances[i][j - 1] + 0.6), @@ -126,100 +133,137 @@ private static double[][] getAlignmentMatrix(List> gtNotes, List> getNotePitchMaps(List> noteLists) { + List> notePitchMaps = new ArrayList>(noteLists.size()); + + for (List notesList : noteLists) { + Map pitchMap = new HashMap(notesList.size()); + notePitchMaps.add(pitchMap); + + for (Note note : notesList) { + if (pitchMap.containsKey(note.pitch)) { + pitchMap.put(note.pitch, pitchMap.get(note.pitch) + 1); + } else { + pitchMap.put(note.pitch, 1); + } + } + } + + return notePitchMaps; + } + /** * Get the distance between a given ground truth note set and a possible transcription note set. - * - * @param gtNotes The ground truth notes. - * @param mNotes The possible transcription notes. + * + * @param gtNoteMap The pitch map of a ground truth note set. + * @param mNoteMap The pitch map of a possible transcription note set. * @return The alignment score. 1 - its F-measure. */ - private static double getDistance(List gtNotes, List mNotes) { + private static double getDistance(Map gtNoteMap, Map mNoteMap) { int truePositives = 0; - List gtNotesCopy = new ArrayList(gtNotes); - - for (Note mNote : mNotes) { - Iterator gtIterator = gtNotesCopy.iterator(); - - while (gtIterator.hasNext()) { - Note gtNote = gtIterator.next(); - - if (mNote.pitch == gtNote.pitch) { - truePositives++; - gtIterator.remove(); - break; + int falsePositives = 0; + + for (Entry entry : mNoteMap.entrySet()) { + Integer pitch = entry.getKey(); + int count = entry.getValue(); + + if (gtNoteMap.containsKey(pitch)) { + int gtCount = gtNoteMap.get(pitch); + + truePositives += Math.min(count, gtCount); + if (count > gtCount) { + falsePositives += count - gtCount; } + + } else { + falsePositives += count; } } - - int falsePositives = mNotes.size() - truePositives; - int falseNegatives = gtNotesCopy.size(); - + + if (truePositives == 0) { + return 1.0; + } + + int gtNoteCount = 0; + for (int count : gtNoteMap.values()) { + gtNoteCount += count; + } + + int falseNegatives = gtNoteCount - truePositives; + return 1.0 - Main.getF1(truePositives, falsePositives, falseNegatives); } /** * Convert a time to a new time given some alignment. - * + * * @param time The time we want to convert. * @param gt The ground truth music, to help with alignment. * @param transcription The transcribed music, where the time comes from. * @param alignment The alignment. * An alignment is a list containing, for each ground truth note list, the index of the transcription * note list to which it is aligned, or -1 if it was not aligned with any transcription note. - * + * * @return A time converted from transcription scale to ground truth scale. */ public static int convertTime(int time, Music gt, Music transcription, List alignment) { double transcriptionIndex = -1; List> transcriptionNotes = transcription.getNoteLists(); - + // Find the correct transcription anchor index to start with for (int i = 0; i < transcriptionNotes.size(); i++) { - + // Time matches an anchor exactly if (transcriptionNotes.get(i).get(0).valueOnsetTime == time) { transcriptionIndex = i; break; } - + // This anchor is past the time if (transcriptionNotes.get(i).get(0).valueOnsetTime > time) { transcriptionIndex = i - 0.5; break; } } - + List> gtNotes = gt.getNoteLists(); int gtPreviousAnchor = -1; int gtPreviousPreviousAnchor = -1; int gtNextAnchor = gtNotes.size(); int gtNextNextAnchor = gtNotes.size(); - + // Go through the alignments for (int i = 0; i < alignment.size(); i++) { if (alignment.get(i) != -1) { // There was an alignment here - + if (alignment.get(i) == transcriptionIndex) { // This is the correct time, exactly on the index return gtNotes.get(i).get(0).valueOnsetTime; } - + if (alignment.get(i) < transcriptionIndex) { // The time is past this anchor gtPreviousPreviousAnchor = gtPreviousAnchor; gtPreviousAnchor = i; - + } else { // We are past the time if (gtNextAnchor == gtNotes.size()) { // This is the first anchor for which we are past the time gtNextAnchor = i; - + } else { // This is the 2nd anchor for which we are past the time gtNextNextAnchor = i; @@ -228,40 +272,40 @@ public static int convertTime(int time, Music gt, Music transcription, List> gtNotes, List> mNotes, List alignment) { @@ -278,9 +322,9 @@ private static int convertTime(int time, int gtPreviousAnchor, int gtNextAnchor, int gtNextTime = gtNotes.get(gtNextAnchor).get(0).valueOnsetTime; int mPreviousTime = mNotes.get(alignment.get(gtPreviousAnchor)).get(0).valueOnsetTime; int mNextTime = mNotes.get(alignment.get(gtNextAnchor)).get(0).valueOnsetTime; - + double rate = ((double) (gtNextTime - gtPreviousTime)) / (mNextTime - mPreviousTime); - + return (int) Math.round(rate * (time - mPreviousTime) + gtPreviousTime); } } \ No newline at end of file diff --git a/src/mv2h/tools/Converter.java b/src/mv2h/tools/Converter.java index 498cb4e..9fda60e 100644 --- a/src/mv2h/tools/Converter.java +++ b/src/mv2h/tools/Converter.java @@ -11,7 +11,7 @@ /** * The Converter class is used to convert another file format into * a format that can be read by the MV2H package (standard out). - * + * * @author Andrew McLeod */ public class Converter { @@ -19,24 +19,24 @@ public class Converter { * The number of milliseconds per beat by default. */ public static final int MS_PER_BEAT = 500; - + /** * Options for MusicXML voice calculation. */ public static boolean PART = false; public static boolean STAFF = false; public static boolean VOICE = false; - + /** * Options for MIDI voice calculation. */ public static boolean CHANNEL = false; public static boolean TRACK = false; - + /** * Run the program, reading the MusicXMLParser output from standard in and printing to * standard out. - * + * * @param args Unused command line arguments. */ public static void main(String[] args) { @@ -44,15 +44,15 @@ public static void main(String[] args) { boolean useMidi = false; int numToUse = 0; int anacrusis = 0; - + File inFile = null; File outFile = null; - + // No args given if (args.length == 0) { argumentError("No arguments given"); } - + for (int i = 0; i < args.length; i++) { switch (args[i].charAt(0)) { // ARGS @@ -60,7 +60,7 @@ public static void main(String[] args) { if (args[i].length() == 1) { argumentError("Unrecognized option: " + args[i]); } - + switch (args[i].charAt(1)) { // midi case 'm': @@ -69,7 +69,7 @@ public static void main(String[] args) { useMidi = true; } break; - + // musicxml case 'x': if (!useXml) { @@ -77,7 +77,7 @@ public static void main(String[] args) { useXml = true; } break; - + // anacrusis case 'a': i++; @@ -90,7 +90,7 @@ public static void main(String[] args) { argumentError("Anacrusis must be an integer."); } break; - + // input file case 'i': i++; @@ -105,7 +105,7 @@ public static void main(String[] args) { argumentError("Input file " + inFile + " does not exist."); } break; - + // output file case 'o': i++; @@ -117,51 +117,51 @@ public static void main(String[] args) { } outFile = new File(args[i]); break; - + // Voice options case '-': switch (args[i].substring(2)) { case "part": PART = true; break; - + case "staff": STAFF = true; break; - + case "voice": VOICE = true; break; - + case "channel": CHANNEL = true; break; - + case "track": TRACK = true; break; - + default: argumentError("Unrecognized option: " + args[i]); } break; - + // Error default: argumentError("Unrecognized option: " + args[i]); } break; - + // Error default: argumentError("Unrecognized option: " + args[i]); } } - + if (numToUse != 1) { argumentError("Exactly 1 format is required"); } - + // Convert Converter converter = null; if (useMidi) { @@ -178,7 +178,7 @@ public static void main(String[] args) { System.err.println("Error reading from " + inFile + ":\n" + e.getMessage()); System.exit(1); } - + } else if (useXml) { if (!PART && !STAFF && !VOICE) { PART = true; @@ -204,7 +204,7 @@ public static void main(String[] args) { } } } - + // Print result if (outFile != null) { try { @@ -212,37 +212,37 @@ public static void main(String[] args) { fw.write(converter.toString()); fw.close(); } catch (IOException e) { - System.err.println("Error reading from " + inFile + ":\n" + e.getMessage()); + System.err.println("Error writing to " + outFile + ":\n" + e.getMessage()); System.err.println(); System.err.println("Printing to std out instead:"); System.out.println(converter.toString()); } - + } else { System.out.println(converter.toString()); } } - + /** * Some argument error occurred. Print the given message and the usage instructions to std err * and exit. - * + * * @param message The message to print to std err. */ private static void argumentError(String message) { StringBuilder sb = new StringBuilder(message).append('\n'); - + sb.append("Usage: Converter [-x | -m] [-i FILE] [-o FILE] [-a INT] [--VOICE_ARGS]\n\n"); - + sb.append("Exactly one format of -x or -m is required:\n"); sb.append("-x = Convert from parsed MusicXML.\n"); sb.append("-m = Convert from MIDI.\n\n"); - + sb.append("-i FILE = Read input from the given FILE. Required for MIDI.\n"); sb.append(" If not given for MusicXML, read from std input.\n"); sb.append("-o FILE = Print out to the given FILE.\n"); sb.append(" If not given, print to std out.\n\n"); - + sb.append("Voice specific args (can include multiple; defaults to all):\n"); sb.append("MusicXML:\n"); sb.append(" --part = Use part (instrument) to separate parsed voices.\n"); @@ -251,10 +251,10 @@ private static void argumentError(String message) { sb.append("MIDI:\n"); sb.append(" --channel = Use channel to separate parsed voices.\n"); sb.append(" --track = Use track to separate parsed voices.\n\n"); - + sb.append("MIDI-specific args:\n"); sb.append("-a INT = Set the length of the anacrusis (pick-up bar), in sub-beats.\n"); - + System.err.println(sb); System.exit(1); } diff --git a/src/mv2h/tools/MusicXmlConverter.java b/src/mv2h/tools/MusicXmlConverter.java index 81d4136..965f8be 100644 --- a/src/mv2h/tools/MusicXmlConverter.java +++ b/src/mv2h/tools/MusicXmlConverter.java @@ -17,7 +17,7 @@ /** * The MusicXmlConverter class is used to convert a given output from the MusicXMLParser * into a format that can be read by the MV2H package (using the toString() method). - * + * * @author Andrew McLeod */ public class MusicXmlConverter extends Converter { @@ -25,49 +25,49 @@ public class MusicXmlConverter extends Converter { * The notes present in the XML piece. */ private List notes = new ArrayList(); - + /** * The hierarchies of this piece. */ private List hierarchies = new ArrayList(); - + /** * The tick of the starting time of each hierarchy. */ private List hierarchyTicks = new ArrayList(); - + /** * A list of the key signatures of this piece. */ private List keys = new ArrayList(); - + /** * A list of the notes for which there hasn't yet been an offset. */ private List unfinishedNotes = new ArrayList(); - + /** * The last tick of the piece. */ private int lastTick = 0; - + /** * The first tick of the piece. */ private int firstTick = Integer.MAX_VALUE; - + /** * The bar of the previous line. This is kept updated until the anacrusis is handled. *
* @see #handleAnacrusis(int, int, int, int) */ private int previousBar = -1; - + /** * The number of ticks per quarter note. 1 tick is 1 tatum. Defaults to 4. */ private int ticksPerQuarterNote = 4; - + /** * A mapping for XML voices ("part_staff_voice" strings) to 0-indexed MV2H voices. */ @@ -78,34 +78,34 @@ public class MusicXmlConverter extends Converter { *
* This method contains the main program logic, and printing is handled by * {@link #toString()}. - * + * * @param stream The MusicXMLParser output to convert. */ public MusicXmlConverter(InputStream stream) { Scanner in = new Scanner(stream); int lineNum = 0; boolean anacrusisHandled = false; - + voiceMap = new HashMap(); - + while (in.hasNextLine()) { lineNum++; String line = in.nextLine(); - + // Skip comment lines if (line.startsWith("//")) { continue; } - + String[] attributes = line.split("\t"); if (attributes.length < 5) { // Error if fewer than 5 columns System.err.println("WARNING: Line type not found. Skipping line " + lineNum + ": " + line); continue; } - + int tick = Integer.parseInt(attributes[0]); - + // Zero out unused voice markers if (!Converter.PART) { attributes[2] = "0"; @@ -117,40 +117,40 @@ public MusicXmlConverter(InputStream stream) { attributes[4] = "0"; } String xmlVoice = attributes[2] + "_" + attributes[3] + "_" + attributes[4]; - + if (!voiceMap.containsKey(xmlVoice)) { voiceMap.put(xmlVoice, voiceMap.size()); } int voice = voiceMap.get(xmlVoice); - + lastTick = Math.max(tick, lastTick); firstTick = Math.min(tick, firstTick); - + // Switch for different types of lines switch (attributes[5]) { // Attributes is the base line type describing time signature, tempo, etc. case "attributes": ticksPerQuarterNote = Integer.parseInt(attributes[6]); - + // Time signature int tsNumerator = Integer.parseInt(attributes[9]); int tsDenominator = Integer.parseInt(attributes[10]); - + int beatsPerBar = tsNumerator; int subBeatsPerBeat = 2; - + int subBeatsPerQuarterNote = tsDenominator / 2; - + // Check for compound meter if (beatsPerBar % 3 == 0 && beatsPerBar > 3) { beatsPerBar /= 3; subBeatsPerBeat = 3; - + subBeatsPerQuarterNote = tsDenominator / 4; } - + int tatumsPerSubBeat = ticksPerQuarterNote / subBeatsPerQuarterNote; - + // Add the new time signature (if it is new) Hierarchy mostRecent = hierarchies.isEmpty() ? null : hierarchies.get(hierarchies.size() - 1); if (mostRecent == null || mostRecent.beatsPerBar != beatsPerBar || @@ -158,43 +158,43 @@ public MusicXmlConverter(InputStream stream) { hierarchyTicks.add(tick); hierarchies.add(new Hierarchy(beatsPerBar, subBeatsPerBeat, tatumsPerSubBeat, 0, getTimeFromTick(tick))); } - + // Key signature int keyFifths = Integer.parseInt(attributes[7]); String keyMode = attributes[8]; - + int tonic = ((7 * keyFifths) + 144) % 12; boolean mode = keyMode.equalsIgnoreCase("Major"); - + // Add the new key (if it is new) Key mostRecentKey = keys.isEmpty() ? null : keys.get(keys.size() - 1); if (mostRecentKey == null || mostRecentKey.tonic != tonic || mostRecentKey.isMajor != mode) { keys.add(new Key(tonic, mode, getTimeFromTick(tick))); } - + break; - + case "rest": // Handle anacrusis if (!anacrusisHandled) { anacrusisHandled = handleAnacrusis(Integer.parseInt(attributes[1]), tick); } break; - + case "chord": // There are notes here - + // Handle anacrusis if (!anacrusisHandled) { anacrusisHandled = handleAnacrusis(Integer.parseInt(attributes[1]), tick); } - + int duration = Integer.parseInt(attributes[6]); lastTick = Math.max(tick + duration, lastTick); - + int tieInfo = Integer.parseInt(attributes[7]); int numNotes = Integer.parseInt(attributes[8]); - + // Get all of the pitches int[] pitches = new int[numNotes]; for (int i = 0; i < numNotes; i++) { @@ -205,7 +205,7 @@ public MusicXmlConverter(InputStream stream) { continue; } } - + // Handle each pitch for (int pitch : pitches) { switch (tieInfo) { @@ -213,12 +213,12 @@ public MusicXmlConverter(InputStream stream) { case 0: notes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); break; - + // Tie out case 1: unfinishedNotes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); break; - + // Tie in case 2: try { @@ -229,7 +229,7 @@ public MusicXmlConverter(InputStream stream) { notes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); } break; - + // Tie in and out case 3: try { @@ -240,7 +240,7 @@ public MusicXmlConverter(InputStream stream) { unfinishedNotes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); } break; - + // ??? default: System.err.println("WARNING: Unknown tie type " + tieInfo + ". Skipping line " + lineNum + ": " + line); @@ -248,13 +248,13 @@ public MusicXmlConverter(InputStream stream) { } } break; - + case "tremolo-m": duration = Integer.parseInt(attributes[6]); lastTick = Math.max(tick + duration, lastTick); - + numNotes = Integer.parseInt(attributes[8]); - + pitches = new int[numNotes]; for (int i = 0; i < numNotes; i++) { try { @@ -264,38 +264,38 @@ public MusicXmlConverter(InputStream stream) { continue; } } - + for (int pitch : pitches) { // TODO: Decide how many notes to add here. i.e., default to eighth notes for now? int ticksPerTremolo = ticksPerQuarterNote / 2; int numTremolos = duration / ticksPerTremolo; - - + + for (int i = 0; i < numTremolos; i++) { notes.add(new Note(pitch, getTimeFromTick(tick + ticksPerTremolo * i), getTimeFromTick(tick + ticksPerTremolo * i), getTimeFromTick(tick + ticksPerTremolo * (i + 1)), voice)); } } break; - + default: System.err.println("WARNING: Unrecognized line type. Skipping line " + lineNum + ": " + line); continue; } } - + in.close(); - + // Check for any unfinished notes (because of ties out). for (Note note : unfinishedNotes) { System.err.println("WARNING: Tie never ended for note " + note + ". Adding note as untied."); notes.add(note); } } - + /** * Handle any anacrusis, if possible. First, detect if at least 1 bar has finished. If it has, * check how long the previous bar was, and set the first anacrusis according to that. - * + * * @param bar The bar number of the current line. This will be compared to {@link #previousBar} * to check if a bar has just finished. * @param tick The tick of the current line. @@ -305,39 +305,39 @@ private boolean handleAnacrusis(int bar, int tick) { if (previousBar == -1) { // This is the first bar we've seen previousBar = bar; - + } else if (previousBar != bar) { // Ready to handle the anacrusis - + // Add a default 4/4 at time 0 if no hierarchy has been seen yet if (hierarchies.isEmpty()) { hierarchies.add(new Hierarchy(4, 2, ticksPerQuarterNote / 2, 0, 0)); } - + if (hierarchies.size() != 1) { System.err.println("Warning: More than 1 time signature seen in the first bar."); } - + // Duplicate mostRecent, but with correct anacrusis (tick % tatumsPerBar) Hierarchy mostRecent = hierarchies.get(hierarchies.size() - 1); int tatumsPerBar = mostRecent.beatsPerBar * mostRecent.subBeatsPerBeat * mostRecent.tatumsPerSubBeat; hierarchies.set(hierarchies.size() - 1, new Hierarchy(mostRecent.beatsPerBar, mostRecent.subBeatsPerBeat, mostRecent.tatumsPerSubBeat, tick % tatumsPerBar, mostRecent.time)); - + return true; } - + return false; } - + /** * Find a tied out note that matches a new tied in note, return it, and remove it from * {@link #unfinishedNotes}. - * + * * @param pitch The pitch of the tie. * @param valueOnsetTime The onset time of the tied in note. * @param voice The voice of the tied in note. - * + * * @return The note from {@link #unfinishedNotes} that matches the pitch onset time and voice. * @throws IOException If no matching note is found. */ @@ -345,19 +345,19 @@ private Note findAndRemoveUnfinishedNote(int pitch, int valueOnsetTime, int voic Iterator noteIterator = unfinishedNotes.iterator(); while (noteIterator.hasNext()) { Note note = noteIterator.next(); - + if (note.pitch == pitch && note.valueOffsetTime == valueOnsetTime && note.voice == voice) { noteIterator.remove(); return note; } } - + throw new IOException("Tied note not found at pitch=" + pitch + " offset=" + valueOnsetTime + " voice=" + voice + "."); } - + /** * Convert from XML tick to time, using {@link #MS_PER_BEAT}. - * + * * @param tick The XML tick. * @return The time, in milliseconds. */ @@ -365,70 +365,70 @@ private int getTimeFromTick(int tick) { if (hierarchies.isEmpty()) { return tick; } - + int i; for (i = 0; i < hierarchies.size() - 1; i++) { if (hierarchyTicks.get(i + 1) > tick) { break; } } - + Hierarchy hierarchy = hierarchies.get(i); int hierarchyTick = hierarchyTicks.get(i); - + return hierarchy.time + (int) Math.round(((double) tick - hierarchyTick) / hierarchy.tatumsPerSubBeat / hierarchy.subBeatsPerBeat * MS_PER_BEAT); } - + /** * Create and return a list of tatums based on the parsed {@link #hierarchy}, {@link #firstTick}, and * {@link #lastTick}. - * + * * @return A list of the parsed tatums. */ private List getTatums() { List tatums = new ArrayList(lastTick - firstTick); - + for (int tick = firstTick; tick < lastTick; tick++) { tatums.add(new Tatum(getTimeFromTick(tick))); } - + return tatums; } - + /** * Return a String version of the parsed musical score into our mv2h format. - * + * * @return The parsed musical score. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); - + for (Note note : notes) { sb.append(note).append('\n'); } - + for (Tatum tatum : getTatums()) { sb.append(tatum).append('\n'); } - + for (Key key : keys) { sb.append(key).append('\n'); } - + for (Hierarchy hierarchy : hierarchies) { sb.append(hierarchy).append('\n'); } - + return sb.toString(); } - + /** * Get the pitch number of a note given its String. - * + * * @param pitchString A pitch String, like C##4, or Ab1, or G7. * @return The number of the given pitch, with A4 = 440Hz = 69. - * + * * @throws IOException If a parse error occurs. */ private static int getPitchFromString(String pitchString) throws IOException { @@ -438,40 +438,40 @@ private static int getPitchFromString(String pitchString) throws IOException { case 'C': pitch = 0; break; - + case 'D': pitch = 2; break; - + case 'E': pitch = 4; break; - + case 'F': pitch = 5; break; - + case 'G': pitch = 7; break; - + case 'A': pitch = 9; break; - + case 'B': pitch = 11; break; - + default: throw new IOException("Pith " + pitchChar + " not recognized."); } - + int accidental = pitchString.length() - pitchString.replace("#", "").length(); accidental -= pitchString.length() - pitchString.replace("b", "").length(); - - int octave = Integer.parseInt(pitchString.substring(1).replace("#", "").replace("b", "")); - + + int octave = Integer.parseInt(pitchString.substring(1).replace("#", "").replace("b", "")); + return (octave + 1) * 12 + pitch + accidental; } } From 207ce707326bb5bdd39f37a38390a4daa41526c0 Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Fri, 6 Nov 2020 11:51:12 +0100 Subject: [PATCH 02/13] Alignment MUCH faster --- src/mv2h/Main.java | 145 ++++++++++++++-------------- src/mv2h/objects/Music.java | 186 +++++++++++++++++++----------------- src/mv2h/tools/Aligner.java | 137 +++++++++++++++----------- 3 files changed, 256 insertions(+), 212 deletions(-) diff --git a/src/mv2h/Main.java b/src/mv2h/Main.java index 09d2abc..e7b58d5 100644 --- a/src/mv2h/Main.java +++ b/src/mv2h/Main.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Scanner; +import java.util.Set; import mv2h.objects.MV2H; import mv2h.objects.Music; @@ -13,11 +14,11 @@ /** * The Main class is the class called to evaluate anything with the MV2H package. - * + * * @author Andrew McLeod */ public class Main { - + /** * The difference in duration between two {@link mv2h.objects.Note}s for their value * to be counted as a match. @@ -25,7 +26,7 @@ public class Main { * Measured in milliseconds. */ public static int DURATION_DELTA = 100; - + /** * The difference in onset time between two {@link mv2h.objects.Note}s for them to be * counted as a match. @@ -33,7 +34,7 @@ public class Main { * Measured in milliseconds. */ public static int ONSET_DELTA = 50; - + /** * The difference in time between beginning and end times of a {@link mv2h.objects.meter.Grouping} * for it to be counted as a match. @@ -41,7 +42,7 @@ public class Main { * Measured in milliseconds. */ public static int GROUPING_EPSILON = 50; - + /** * A flag representing if alignment should be performed. Defaults to false. * Can be set to true with the -a or -A flags. @@ -49,7 +50,7 @@ public class Main { * @see #PRINT_ALIGNMENT */ private static boolean PERFORM_ALIGNMENT = false; - + /** * A flag representing if the alignment should be printed or not. Defaults to false. * Can be set to true with the -A flag (which also sets @@ -72,21 +73,21 @@ public class Main { *
* 2. Get the means and standard deviations of many outputs of this program * (read from standard in): -F - * + * * @param args The command line arguments, as described. - * + * * @throws IOException If a File given with -g or -t cannot be * read. */ public static void main(String[] args) throws IOException { File groundTruth = null; File transcription = null; - + // No args given if (args.length == 0) { argumentError("No arguments given"); } - + for (int i = 0; i < args.length; i++) { switch (args[i].charAt(0)) { // ARGS @@ -94,23 +95,23 @@ public static void main(String[] args) throws IOException { if (args[i].length() == 1) { argumentError("Unrecognized option: " + args[i]); } - + switch (args[i].charAt(1)) { // Check Full case 'F': checkFull(); return; - + case 'A': PRINT_ALIGNMENT = true; - + case 'a': DURATION_DELTA = 20; ONSET_DELTA = 0; GROUPING_EPSILON = 20; PERFORM_ALIGNMENT = true; break; - + // Evaluate! case 'g': i++; @@ -125,7 +126,7 @@ public static void main(String[] args) throws IOException { argumentError("Ground truth file " + groundTruth + " does not exist."); } break; - + case 't': i++; if (args.length <= i) { @@ -139,65 +140,69 @@ public static void main(String[] args) throws IOException { argumentError("Transcription file " + transcription + " does not exist."); } break; - + // Error default: argumentError("Unrecognized option: " + args[i]); } break; - + // Error default: argumentError("Unrecognized option: " + args[i]); } } - + if (groundTruth != null && transcription != null) { evaluateGroundTruth(groundTruth, transcription); } else { argumentError("Must give either -F, or both -g FILE and -t FILE."); } } - + /** * Evaluate the given transcription against the given ground truth file. * Prints the result to std out. - * + * * @param groundTruthFile The ground truth. * @param transcriptionFile The transcription. - * - * @throws IOException If one of the Files could not be read. + * + * @throws IOException If one of the Files could not be read. */ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionFile) throws IOException { Music groundTruth = Music.parseMusic(new Scanner(groundTruthFile)); Music transcription = Music.parseMusic(new Scanner(transcriptionFile)); - + // Get scores if (PERFORM_ALIGNMENT) { - + // Choose the best possible alignment out of all potential alignments. MV2H best = new MV2H(0, 0, 0, 0, 0); List bestAlignment = new ArrayList(); - - for (List alignment : Aligner.getPossibleAlignments(groundTruth, transcription)) { + + Set> alignments = Aligner.getPossibleAlignments(groundTruth, transcription); + int i = 0; + for (List alignment : alignments) { + System.out.print("Evaluating alignment " + (i++) + " / " + alignments.size() + "\r"); + MV2H candidate = groundTruth.evaluateTranscription(transcription.align(groundTruth, alignment)); - - if (candidate.compareTo(best) > 0) { + + if (candidate.compareTo(best) > 0) { best = candidate; bestAlignment = alignment; } } - + if (PRINT_ALIGNMENT) { System.out.println("ALIGNMENT"); System.out.println("========="); - + List> nonAlignedNotes = new ArrayList>(); - + System.out.println("Aligned notes (transcribed -> ground truth):"); for (int noteIndex = 0; noteIndex < transcription.getNoteLists().size(); noteIndex++) { int alignment = bestAlignment.indexOf(noteIndex); - + if (alignment == -1) { nonAlignedNotes.add(transcription.getNoteLists().get(noteIndex)); } else { @@ -206,13 +211,13 @@ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionF } } System.out.println(); - + System.out.println("Non-aligned transcription notes:"); for (List notes : nonAlignedNotes) { System.out.println(notes); } System.out.println(); - + System.out.println("Non-aligned ground truth notes:"); for (int noteIndex = 0; noteIndex < groundTruth.getNoteLists().size(); noteIndex++) { if (bestAlignment.get(noteIndex) == -1) { @@ -220,19 +225,19 @@ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionF } } System.out.println(); - + System.out.println("MV2H"); System.out.println("===="); } - + System.out.println(best); - + } else { // No alignment System.out.println(groundTruth.evaluateTranscription(transcription)); } } - + /** * Calculate and print mean and standard deviation of Multi-pitch, Voice, Meter, Value, Harmony, and MV2H * scores as produced by this program, read from std in. @@ -242,70 +247,70 @@ private static void checkFull() { int multiPitchCount = 0; double multiPitchSum = 0.0; double multiPitchSumSquared = 0.0; - + int voiceCount = 0; double voiceSum = 0.0; double voiceSumSquared = 0.0; - + int meterCount = 0; double meterSum = 0.0; double meterSumSquared = 0.0; - + int valueCount = 0; double valueSum = 0.0; double valueSumSquared = 0.0; - + int harmonyCount = 0; double harmonySum = 0.0; double harmonySumSquared = 0.0; - + int mv2hCount = 0; double mv2hSum = 0.0; double mv2hSumSquared = 0.0; - + // Parse std in Scanner input = new Scanner(System.in); while (input.hasNextLine()) { String line = input.nextLine(); - + int breakPoint = line.indexOf(": "); if (breakPoint == -1) { continue; } - + String prefix = line.substring(0, breakPoint); - + // Check for matching prefixes if (prefix.equalsIgnoreCase("Multi-pitch")) { double score = Double.parseDouble(line.substring(breakPoint + 2)); multiPitchSum += score; multiPitchSumSquared += score * score; multiPitchCount++; - + } else if (prefix.equalsIgnoreCase("Voice")) { double score = Double.parseDouble(line.substring(breakPoint + 2)); voiceSum += score; voiceSumSquared += score * score; voiceCount++; - + } else if (prefix.equalsIgnoreCase("Meter")) { double score = Double.parseDouble(line.substring(breakPoint + 2)); meterSum += score; meterSumSquared += score * score; meterCount++; - + } else if (prefix.equalsIgnoreCase("Value")) { double score = Double.parseDouble(line.substring(breakPoint + 2)); valueSum += score; valueSumSquared += score * score; valueCount++; - + } else if (prefix.equalsIgnoreCase("Harmony")) { double score = Double.parseDouble(line.substring(breakPoint + 2)); harmonySum += score; harmonySumSquared += score * score; harmonyCount++; - + } else if (prefix.equalsIgnoreCase("MV2H")) { double score = Double.parseDouble(line.substring(breakPoint + 2)); mv2hSum += score; @@ -314,26 +319,26 @@ private static void checkFull() { } } input.close(); - + // Calculate means and standard deviations double multiPitchMean = multiPitchSum / multiPitchCount; double multiPitchVariance = multiPitchSumSquared / multiPitchCount - multiPitchMean * multiPitchMean; - + double voiceMean = voiceSum / voiceCount; double voiceVariance = voiceSumSquared / voiceCount - voiceMean * voiceMean; - + double meterMean = meterSum / meterCount; double meterVariance = meterSumSquared / meterCount - meterMean * meterMean; - + double valueMean = valueSum / valueCount; double valueVariance = valueSumSquared / valueCount - valueMean * valueMean; - + double harmonyMean = harmonySum / harmonyCount; double harmonyVariance = harmonySumSquared / harmonyCount - harmonyMean * harmonyMean; - + double mv2hMean = mv2hSum / mv2hCount; double mv2hVariance = mv2hSumSquared / mv2hCount - mv2hMean * mv2hMean; - + // Print System.out.println("Multi-pitch: mean=" + multiPitchMean + " stdev=" + Math.sqrt(multiPitchVariance)); System.out.println("Voice: mean=" + voiceMean + " stdev=" + Math.sqrt(voiceVariance)); @@ -342,46 +347,46 @@ private static void checkFull() { System.out.println("Harmony: mean=" + harmonyMean + " stdev=" + Math.sqrt(harmonyVariance)); System.out.println("MV2H: mean=" + mv2hMean + " stdev=" + Math.sqrt(mv2hVariance)); } - + /** * Calculate the F-measure given counts of TP, FP, and FN. - * + * * @param truePositives The number of true positives. * @param falsePositives The number of false positives. * @param falseNegatives The number of false negatives. - * + * * @return The F-measure of the given counts, or 0 if the result is otherwise NaN. */ public static double getF1(double truePositives, double falsePositives, double falseNegatives) { double precision = truePositives / (truePositives + falsePositives); double recall = truePositives / (truePositives + falseNegatives); - + double f1 = 2.0 * recall * precision / (recall + precision); return Double.isNaN(f1) ? 0.0 : f1; } - + /** * Some argument error occurred. Print the given message and the usage instructions to std err * and exit. - * + * * @param message The message to print to std err. */ private static void argumentError(String message) { StringBuilder sb = new StringBuilder(message).append('\n'); - + sb.append("Usage: Main ARGS\n"); sb.append("ARGS:\n"); - + sb.append("-a = Perform DTW alignment to evaluate non-aligned transcriptions.\n"); sb.append("-A = Perform and print the DTW alignment.\n"); - + sb.append("-g FILE = Use the given FILE as the ground truth (defaults to std in).\n"); sb.append("-t FILE = Use the given FILE as the transcription (defaults to std in).\n"); sb.append("Either -g or -t (or both) must be given to evaluate, since both cannot be read from std in.\n"); - + sb.append("-F = Combine the scores from std in (from this program's output) into final"); sb.append(" global mean and standard deviation distributions for each score.\n"); - + System.err.println(sb); System.exit(1); } diff --git a/src/mv2h/objects/Music.java b/src/mv2h/objects/Music.java index 2decc87..41a967c 100644 --- a/src/mv2h/objects/Music.java +++ b/src/mv2h/objects/Music.java @@ -27,36 +27,41 @@ * ({@link #align(Music, List)}). *
* New Music objects should be created with the {@link #parseMusic(Scanner)} method. - * + * * @author Andrew McLeod */ public class Music { - + /** * The notes present in this score. */ private final List notes; - + + /** + * The notes lists, to avoid calculating them every time. + */ + private List> notesLists = null; + /** * The voices of this score. */ private final List voices; - + /** * The metrical structure of this score. */ private final Meter meter; - + /** * The key signature and changes of this score. */ private final KeyProgression keyProgression; - + /** * The chord progression of this score. */ private final ChordProgression chordProgression; - + /** * The last time of this score. */ @@ -66,7 +71,7 @@ public class Music { * Create a new Music object with the given fields. *
* This should not usually be called directly. Rather, use {@link #parseMusic(Scanner)}. - * + * * @param notes {@link #notes} * @param voices {@link #voices} * @param meter {@link #meter} @@ -78,25 +83,29 @@ public Music(List notes, List voices, Meter meter, KeyProgression k int lastTime) { this.notes = notes; Collections.sort(notes); - + this.voices = voices; this.meter = meter; this.keyProgression = keyProgression; this.chordProgression = chordProgression; this.lastTime = lastTime; - + for (Voice voice : voices) { voice.createConnections(); } } - + /** * Get a list of lists of notes, sorted by onset time. Each 2nd level list * contains all notes which share an identical onset time. - * + * * @return A list of lists of notes. */ public List> getNoteLists() { + if (notesLists != null) { + return notesLists; + } + List> lists = new ArrayList>(); if (notes.isEmpty()) { return lists; @@ -106,14 +115,14 @@ public List> getNoteLists() { int mostRecentValueOnsetTime = notes.get(0).valueOnsetTime; lists.add(mostRecentList); mostRecentList.add(notes.get(0)); - + for (int i = 1; i < notes.size(); i++) { Note note = notes.get(i); - + // Still at the same onset time if (mostRecentValueOnsetTime == note.valueOnsetTime) { mostRecentList.add(note); - + // New onset time } else { mostRecentList = new ArrayList(); @@ -122,59 +131,60 @@ public List> getNoteLists() { mostRecentList.add(note); } } - + + notesLists = lists; return lists; } - + /** * Evaluate a given transcription, treating this object as the ground truth. - * + * * @param transcription The transcription to evaluate. - * + * * @return The MV2H evaluation scores object. */ public MV2H evaluateTranscription(Music transcription) { // Tracking objects for notes List transcriptionNotes = new ArrayList(transcription.notes); List groundTruthNotes = new ArrayList(notes); - + // Tracking lists for voices, which will include only matched notes List transcriptionVoices = new ArrayList(transcription.voices.size()); for (int i = 0; i < transcription.voices.size(); i++) { transcriptionVoices.add(new Voice()); } - + List groundTruthVoices = new ArrayList(voices.size()); for (int i = 0; i < voices.size(); i++) { groundTruthVoices.add(new Voice()); } - + // Notes which we can check for value accuracy List valueCheckNotes = new ArrayList(); - + // A mapping for the ground truth. Map groundTruthNoteMapping = new HashMap(); - - + + // Multi-pitch accuracy int multiPitchTruePositives = 0; Iterator transcriptionIterator = transcriptionNotes.iterator(); while (transcriptionIterator.hasNext()) { Note transcriptionNote = transcriptionIterator.next(); - + Iterator groundTruthIterator = groundTruthNotes.iterator(); while (groundTruthIterator.hasNext()) { Note groundTruthNote = groundTruthIterator.next(); - + // Match found if (transcriptionNote.matches(groundTruthNote)) { multiPitchTruePositives++; - + groundTruthNoteMapping.put(transcriptionNote, groundTruthNote); - + transcriptionVoices.get(transcriptionNote.voice).addNote(transcriptionNote); groundTruthVoices.get(groundTruthNote.voice).addNote(groundTruthNote); - + groundTruthIterator.remove(); transcriptionIterator.remove(); break; @@ -183,9 +193,9 @@ public MV2H evaluateTranscription(Music transcription) { } int multiPitchFalsePositives = transcriptionNotes.size(); int multiPitchFalseNegatives = groundTruthNotes.size(); - + double multiPitchF1 = Main.getF1(multiPitchTruePositives, multiPitchFalsePositives, multiPitchFalseNegatives); - + // Make voice connections for (Voice voice : transcriptionVoices) { voice.createConnections(); @@ -193,17 +203,17 @@ public MV2H evaluateTranscription(Music transcription) { for (Voice voice : groundTruthVoices) { voice.createConnections(); } - + // Voice separation double voiceTruePositives = 0; double voiceFalsePositives = 0; double voiceFalseNegatives = 0; // Go through each voice in the transcription (this is only matched notes) for (Voice transcriptionVoice : transcriptionVoices) { - + // Go through each note cluster in the transcription voice for (NoteCluster transcriptionCluster : transcriptionVoice.noteClusters.values()) { - + // Create list of notes which are linked to in the transcription List nextTranscriptionNotesFinal = new ArrayList(); for (NoteCluster nextTranscriptionCluster : transcriptionCluster.nextClusters) { @@ -211,15 +221,15 @@ public MV2H evaluateTranscription(Music transcription) { nextTranscriptionNotesFinal.add(nextTranscriptionNote); } } - + // Go through each note in the note cluster for (Note transcriptionNote : transcriptionCluster.notes) { Note groundTruthNote = groundTruthNoteMapping.get(transcriptionNote); - + // Find the matching ground truth note and its place in its voice Voice groundTruthVoice = groundTruthVoices.get(groundTruthNote.voice); NoteCluster groundTruthCluster = groundTruthVoice.getNoteCluster(groundTruthNote); - + // Create list of notes which are linked to in the ground truth List nextGroundTruthNotesFinal = new ArrayList(); for (NoteCluster nextGroundTruthCluster : groundTruthCluster.nextClusters) { @@ -228,24 +238,24 @@ public MV2H evaluateTranscription(Music transcription) { } } List nextGroundTruthNotes = new ArrayList(nextGroundTruthNotesFinal); - + // Save a copy of the linked transcription notes list List nextTranscriptionNotes = new ArrayList(nextTranscriptionNotesFinal); - + // Count how many tp, fp, and fn for these connection sets int connectionTruePositives = 0; Iterator transcriptionConnectionIterator = nextTranscriptionNotes.iterator(); while (transcriptionConnectionIterator.hasNext()) { Note nextTranscriptionNote = transcriptionConnectionIterator.next(); - + Iterator groundTruthConnectionIterator = nextGroundTruthNotes.iterator(); while (groundTruthConnectionIterator.hasNext()) { Note nextGroundTruthNote = groundTruthConnectionIterator.next(); - + // Match found if (nextTranscriptionNote.matches(nextGroundTruthNote)) { connectionTruePositives++; - + groundTruthConnectionIterator.remove(); transcriptionConnectionIterator.remove(); break; @@ -254,7 +264,7 @@ public MV2H evaluateTranscription(Music transcription) { } int connectionFalsePositives = nextTranscriptionNotes.size(); int connectionFalseNegatives = nextGroundTruthNotes.size(); - + // Normalize counts before adding to totals, so that each connection is weighted equally double outWeight = (nextGroundTruthNotesFinal.size() + nextTranscriptionNotesFinal.size()) / 2.0; if (outWeight > 0) { @@ -262,9 +272,9 @@ public MV2H evaluateTranscription(Music transcription) { voiceFalsePositives += ((double) connectionFalsePositives) / (outWeight * transcriptionCluster.notes.size()); voiceFalseNegatives += ((double) connectionFalseNegatives) / (outWeight * transcriptionCluster.notes.size()); } - + // Add note to list to noteValue check - + // List of notes which are linked to in the original ground truth (including multi-pitch non-TPs) List nextOriginalGroundTruthNotes = new ArrayList(); for (NoteCluster nextGroundTruthCluster : voices.get(groundTruthNote.voice).getNoteCluster(groundTruthNote).nextClusters) { @@ -272,11 +282,11 @@ public MV2H evaluateTranscription(Music transcription) { nextOriginalGroundTruthNotes.add(nextGroundTruthNote); } } - + // Both are the end of a voice if (nextOriginalGroundTruthNotes.isEmpty() && nextTranscriptionNotesFinal.isEmpty()) { valueCheckNotes.add(transcriptionNote); - + } else { // Check if at least one original ground truth connection was correct boolean match = false; @@ -288,7 +298,7 @@ public MV2H evaluateTranscription(Music transcription) { break; } } - + if (match) { break; } @@ -298,12 +308,12 @@ public MV2H evaluateTranscription(Music transcription) { } } double voiceF1 = Main.getF1(voiceTruePositives, voiceFalsePositives, voiceFalseNegatives); - - + + // Meter double meterF1 = transcription.meter.getF1(meter); - - + + // Note value (check only GT matches and GT voice matches) double valueScoreSum = 0.0; for (Note transcriptionNote : valueCheckNotes) { @@ -313,25 +323,25 @@ public MV2H evaluateTranscription(Music transcription) { if (Double.isNaN(valueScore)) { valueScore = 0.0; } - - + + // Harmony double keyScore = transcription.keyProgression.getScore(keyProgression, lastTime); double progressionScore = transcription.chordProgression.getScore(chordProgression, lastTime); - + double harmonyScore = (keyScore + progressionScore) / 2; if (Double.isNaN(progressionScore)) { harmonyScore = keyScore; } - + if (Double.isNaN(keyScore)) { harmonyScore = progressionScore; } - + if (Double.isNaN(harmonyScore)) { harmonyScore = 0.0; } - + // MV2H return new MV2H(multiPitchF1, voiceF1, meterF1, valueScore, harmonyScore); } @@ -339,64 +349,66 @@ public MV2H evaluateTranscription(Music transcription) { /** * Get a new Music object whose times are mapped to the corresponding ground truth's * times given the alignment. - * + * * @param gt The ground truth Music object. * @param alignment The alignment to re-map with. * An alignment is a list containing, for each ground truth note list, the index of the transcription * note list to which it is aligned, or -1 if it was not aligned with any transcription note. - * + * * @return A new Music object with the given alignment. */ public Music align(Music gt, List alignment) { List newNotes = new ArrayList(notes.size()); List newVoices = new ArrayList(voices.size()); - + + Map alignedTimes = new HashMap(); + // Convert each note into a new note for (Note note : notes) { newNotes.add(new Note( note.pitch, - Aligner.convertTime(note.onsetTime, gt, this, alignment), - Aligner.convertTime(note.valueOnsetTime, gt, this, alignment), - Aligner.convertTime(note.valueOffsetTime, gt, this, alignment), + Aligner.convertTime(note.onsetTime, gt, this, alignment, alignedTimes), + Aligner.convertTime(note.valueOnsetTime, gt, this, alignment, alignedTimes), + Aligner.convertTime(note.valueOffsetTime, gt, this, alignment, alignedTimes), note.voice)); - + while (note.voice >= newVoices.size()) { newVoices.add(new Voice()); } newVoices.get(note.voice).addNote(newNotes.get(newNotes.size() - 1)); } - + // Convert the metrical structure times - Meter newMeter = new Meter(Aligner.convertTime(0, gt, this, alignment)); + Meter newMeter = new Meter(Aligner.convertTime(0, gt, this, alignment, alignedTimes)); for (Hierarchy h : meter.getHierarchies()) { newMeter.addHierarchy(new Hierarchy(h.beatsPerBar, h.subBeatsPerBeat, h.tatumsPerSubBeat, h.anacrusisLengthTatums, - Aligner.convertTime(h.time, gt, this, alignment))); + Aligner.convertTime(h.time, gt, this, alignment, alignedTimes))); } for (Tatum tatum : meter.getTatums()) { - newMeter.addTatum(new Tatum(Aligner.convertTime(tatum.time, gt, this, alignment))); + newMeter.addTatum(new Tatum(Aligner.convertTime(tatum.time, gt, this, alignment, alignedTimes))); } - + // Convert the key change times KeyProgression newKeyProgression = new KeyProgression(); for (Key key : keyProgression.getKeys()) { - newKeyProgression.addKey(new Key(key.tonic, key.isMajor, Aligner.convertTime(key.time, gt, this, alignment))); + newKeyProgression.addKey(new Key(key.tonic, key.isMajor, Aligner.convertTime(key.time, gt, this, alignment, alignedTimes))); } - + // Convert the chord change times ChordProgression newChordProgression = new ChordProgression(); for (Chord chord : chordProgression.getChords()) { - newChordProgression.addChord(new Chord(chord.chord, Aligner.convertTime(chord.time, gt, this, alignment))); + newChordProgression.addChord(new Chord(chord.chord, Aligner.convertTime(chord.time, gt, this, alignment, alignedTimes))); } - + // Create and return the new Music object return new Music(newNotes, newVoices, newMeter, newKeyProgression, newChordProgression, - Aligner.convertTime(lastTime, gt, this, alignment)); + Aligner.convertTime(lastTime, gt, this, alignment, alignedTimes)); } /** * Parse a musical score from the given scanner in mv2h format and return a corresponding * Music object. - * + * * @param input The input stream to read from. * @return The parsed Music object. * @throws IOException If there was an error in reading or parsing the stream. @@ -409,34 +421,34 @@ public static Music parseMusic(Scanner input) throws IOException { ChordProgression chordProgression = new ChordProgression(); KeyProgression keyProgression = new KeyProgression(); int lastTime = Integer.MIN_VALUE; - + // Read through input while (input.hasNextLine()) { String line = input.nextLine(); - + // Check for matching prefixes, and pass each to its corresponding parser. if (line.startsWith("Note")) { Note note = Note.parseNote(line); notes.add(note); - + // Add note to voice while (note.voice >= voices.size()) { voices.add(new Voice()); } voices.get(note.voice).addNote(note); - + lastTime = Math.max(lastTime, note.valueOffsetTime); - + } else if (line.startsWith("Tatum")) { Tatum tatum = Tatum.parseTatum(line); meter.addTatum(tatum); - + lastTime = Math.max(lastTime, tatum.time); - + } else if (line.startsWith("Chord")) { Chord chord = Chord.parseChord(line); chordProgression.addChord(chord); - + lastTime = Math.max(lastTime, chord.time); } else if (line.startsWith("Hierarchy")) { @@ -446,12 +458,12 @@ public static Music parseMusic(Scanner input) throws IOException { } else if (line.startsWith("Key")) { Key key = Key.parseKey(line); keyProgression.addKey(key); - + lastTime = Math.max(lastTime, key.time); } } input.close(); - + return new Music(notes, voices, meter, keyProgression, chordProgression, lastTime); } diff --git a/src/mv2h/tools/Aligner.java b/src/mv2h/tools/Aligner.java index cbe86c7..dfd9290 100644 --- a/src/mv2h/tools/Aligner.java +++ b/src/mv2h/tools/Aligner.java @@ -34,24 +34,24 @@ public class Aligner { * note list to which it is aligned, or -1 if it was not aligned with any transcription note. */ public static Set> getPossibleAlignments(Music gt, Music m) { - double[][] distances = getAlignmentMatrix(gt.getNoteLists(), m.getNoteLists()); - return getPossibleAlignmentsFromMatrix(distances.length - 1, distances[0].length - 1, distances); + List>> previousCells = getAlignmentMatrix(gt.getNoteLists(), m.getNoteLists()); + return getPossibleAlignmentsFromMatrix(previousCells.size() - 1, previousCells.get(0).size() - 1, previousCells); } /** - * A recursive function to get all of the possible alignments which lead to - * the optimal distance from the distance matrix returned by the heuristic-based DTW in + * A recursive function to get all of the possible alignments from the previousCells + * pointers returned by the heuristic-based DTW in * {@link #getAlignmentMatrix(List, List)}, up to matrix indices i, j. * * @param i The first index, representing the transcribed note index. * @param j The second index, representing the ground truth note index. - * @param distances The full distances matrix from {@link #getAlignmentMatrix(List, List)}. + * @param previousCells The previous cells matrix from {@link #getAlignmentMatrix(List, List)}. * - * @return A Set of all possible alignments given the distance matrix, up to notes i, j. + * @return A Set of all possible alignments given the previous cells matrix, up to notes i, j. * An alignment is a list containing, for each ground truth note list, the index of the transcription * note list to which it is aligned, or -1 if it was not aligned with any transcription note. */ - private static Set> getPossibleAlignmentsFromMatrix(int i, int j, double[][] distances) { + private static Set> getPossibleAlignmentsFromMatrix(int i, int j, List>> previousCells) { Set> alignments = new HashSet>(); // Base case. we are at the beginning and nothing else needs to be aligned. @@ -60,34 +60,27 @@ private static Set> getPossibleAlignmentsFromMatrix(int i, int j, return alignments; } - double min = Math.min(Math.min( - i > 0 ? distances[i - 1][j] : Double.POSITIVE_INFINITY, - j > 0 ? distances[i][j - 1] : Double.POSITIVE_INFINITY), - i > 0 && j > 0 ? distances[i - 1][j - 1] : Double.POSITIVE_INFINITY); - - // Note that we could perform multiple of these if blocks. - - // This transcription note was aligned with nothing in the ground truth. Add -1. - if (distances[i - 1][j] == min) { - for (List list : getPossibleAlignmentsFromMatrix(i - 1, j, distances)) { - list.add(-1); - alignments.add(list); - } - } + for (int previousCell : previousCells.get(i).get(j)) { + if (previousCell == -1) { + // This transcription note was aligned with nothing in the ground truth. Add -1. + for (List list : getPossibleAlignmentsFromMatrix(i - 1, j, previousCells)) { + list.add(-1); + alignments.add(list); + } - // This ground truth note was aligned with nothing in the transcription. Skip it. - if (distances[i][j - 1] == min) { - for (List list : getPossibleAlignmentsFromMatrix(i, j - 1, distances)) { - alignments.add(list); - } - } + } else if (previousCell == 1) { + // This ground truth note was aligned with nothing in the transcription. Skip it. + for (List list : getPossibleAlignmentsFromMatrix(i, j - 1, previousCells)) { + alignments.add(list); + } - // The current transcription and ground truth notes were aligned. Add the current ground - // truth index to the alignment list. - if (distances[i - 1][j - 1] == min) { - for (List list : getPossibleAlignmentsFromMatrix(i - 1, j - 1, distances)) { - list.add(j - 1); // j - 2, because (j-1) is aligned, and the distance matrix starts from 1. - alignments.add(list); + } else { + // The current transcription and ground truth notes were aligned. Add the current ground + // truth index to the alignment list. + for (List list : getPossibleAlignmentsFromMatrix(i - 1, j - 1, previousCells)) { + list.add(j - 1); + alignments.add(list); + } } } @@ -95,23 +88,33 @@ private static Set> getPossibleAlignmentsFromMatrix(int i, int j, } /** - * Get the Dynamic Time Warping distance matrix from the note lists. + * Get the Dynamic Time Warping alignment paths from the note lists. *
- * During calculation, we add an additional 0.01 penalty to any aligned note whose previous + * During calculation, we add an additional 0.6 penalty to any aligned note whose previous * notes (in both ground truth and transcription) were not aligned. This is used to prefer * alignments which align many consecutive notes. * * @param gtNotes The ground truth note lists, split by onset time. * @param mNotes The transcribed note lists, split by onset time. * - * @return The DTW distance matrix. + * @return A List of the previous step's aligned cells for each cell in the alignment matrix. */ - private static double[][] getAlignmentMatrix(List> gtNotes, List> mNotes) { + private static List>> getAlignmentMatrix(List> gtNotes, List> mNotes) { List> gtNoteMaps = getNotePitchMaps(gtNotes); List> mNoteMaps = getNotePitchMaps(mNotes); double[][] distances = new double[gtNotes.size() + 1][mNotes.size() + 1]; + List>> previousCells = new ArrayList>>(gtNotes.size() + 1); + for (int i = 0; i < gtNotes.size() + 1; i++) { + List> list = new ArrayList>(mNotes.size() + 1); + for (int j = 0; j < mNotes.size() + 1; j++) { + list.add(new ArrayList(3)); + } + + previousCells.add(list); + } + for (int i = 1; i < distances.length; i++) { distances[i][0] = Double.POSITIVE_INFINITY; } @@ -120,21 +123,34 @@ private static double[][] getAlignmentMatrix(List> gtNotes, List= 100; - for (int j = 1; j < distances[0].length; j++) { for (int i = 1; i < distances.length; i++) { double distance = getDistance(gtNoteMaps.get(i - 1), mNoteMaps.get(j - 1)); - double min = Math.min(Math.min( - distances[i - 1][j] + 0.6, - distances[i][j - 1] + 0.6), - distances[i - 1][j - 1] + distance); - distances[i][j] = min; + double distance_i_1 = distances[i - 1][j] + 0.6; + double distance_j_1 = distances[i][j - 1] + 0.6; + double distance_i_j_1 = distances[i - 1][j - 1] + distance; + + double min_distance = Math.min(Math.min(distance_i_1, distance_j_1), distance_i_j_1); + + List previousCell = previousCells.get(i).get(j); + if (distance_i_1 == min_distance) { + previousCell.add(-1); + } + + if (distance_j_1 == min_distance) { + previousCell.add(1); + } + + if (distance_i_j_1 == min_distance) { + previousCell.add(0); + } + + distances[i][j] = min_distance; } } - return distances; + return previousCells; } /** @@ -217,7 +233,12 @@ private static double getDistance(Map gtNoteMap, Map alignment) { + public static int convertTime(int time, Music gt, Music transcription, List alignment, Map alignedTimes) { + Integer alignedTime = alignedTimes.get(time); + if (alignedTime != null) { + return alignedTime; + } + double transcriptionIndex = -1; List> transcriptionNotes = transcription.getNoteLists(); @@ -275,31 +296,37 @@ public static int convertTime(int time, Music gt, Music transcription, List Date: Tue, 8 Dec 2020 15:29:24 +0100 Subject: [PATCH 03/13] Removed MusicXML parser --- Makefile | 1 - MusicXMLParser/BasicCalculation_v170122.hpp | 381 ------------ MusicXMLParser/Fmt1x_v170108_2.hpp | 636 -------------------- MusicXMLParser/MusicXMLToFmt1x_v170104.cpp | 26 - MusicXMLParser/compile.sh | 3 - src/mv2h/tools/Converter.java | 115 +--- src/mv2h/tools/MusicXmlConverter.java | 477 --------------- 7 files changed, 14 insertions(+), 1625 deletions(-) delete mode 100755 MusicXMLParser/BasicCalculation_v170122.hpp delete mode 100755 MusicXMLParser/Fmt1x_v170108_2.hpp delete mode 100755 MusicXMLParser/MusicXMLToFmt1x_v170104.cpp delete mode 100755 MusicXMLParser/compile.sh delete mode 100644 src/mv2h/tools/MusicXmlConverter.java diff --git a/Makefile b/Makefile index 5af4ebf..c1982b8 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,3 @@ all: mkdir -p bin javac -d bin -cp src src/mv2h/*.java javac -d bin -cp src src/mv2h/*/*.java - cd MusicXMLParser; ./compile.sh diff --git a/MusicXMLParser/BasicCalculation_v170122.hpp b/MusicXMLParser/BasicCalculation_v170122.hpp deleted file mode 100755 index 3cdec8c..0000000 --- a/MusicXMLParser/BasicCalculation_v170122.hpp +++ /dev/null @@ -1,381 +0,0 @@ -#ifndef BasicCalculation_HPP -#define BasicCalculation_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -using namespace std; - -inline int gcd(int m, int n){ - if(0==m||0==n){return 0;} - while(m!=n){if(m>n){m=m-n;}else{n=n-m;}}//endwhile - return m; -}//end gcd -inline int lcm(int m,int n){ - if (0==m||0==n){return 0;} - return ((m/gcd(m,n))*n);//lcm=m*n/gcd(m,n) -}//end lcm - -inline double LogAdd(double d1,double d2){ - //log(exp(d1)+exp(d2))=log(exp(d1)(1+exp(d2-d1))) - if(d1>d2){ -// if(d1-d2>20){return d1;} - return d1+log(1+exp(d2-d1)); - }else{ -// if(d2-d1>20){return d2;} - return d2+log(1+exp(d1-d2)); - }//endif -}//end LogAdd -inline void Norm(vector& vd){ - double sum=0; - for(int i=0;i& vd){ - double tmpd=vd[0]; - for(int i=0;itmpd){tmpd=vd[i];}}//endfor i - for(int i=0;i& vd){ - assert(vd.size()>0); - double sum=0; - for(int i=0;i& vd){ - assert(vd.size()>1); - double ave=Average(vd); - double sum=0; - for(int i=0;i p,vector q,double regularizer=0){//p given q - assert(p.size()==q.size()); - double sum=0; - for(int i=0;i p,vector q,double scale=1){//p given q - assert(p.size()==q.size()); - double sum=0; - for(int i=0;i &p){ - double val=(1.0*rand())/(1.0*RAND_MAX); - for(int i=0;i b.value){ - return true; - }else{//if a.value <= b.value - return false; - }//endif - }//end operator() -};//end class MorePair -//sort(pairs.begin(), pairs.end(), MorePair()); - -inline vector Intervals(double valmin,double valmax,int nPoint){ - vector values; - double eps=(valmax-valmin)/double(nPoint-1); - for(int i=0;i LogIntervals(double valmin,double valmax,int nPoint){ - vector values; - double eps=(log(valmax)-log(valmin))/double(nPoint-1); - for(int i=0;i class Prob{ -public: - vector P; - vector LP; - vector samples; - - Prob(){ - }//end Prob - Prob(Prob const & prob_){ - P=prob_.P; - LP=prob_.LP; - samples=prob_.samples; - }//end Prob - - ~Prob(){ - }//end ~Prob - - Prob& operator=(const Prob & prob_){ - P=prob_.P; - LP=prob_.LP; - samples=prob_.samples; - return *this; - }//end = - - void Print(){ - for(int i=0;imax){max=P[i];} - }//endfor i - return max; - }//end MaxValue - - int ModeID(){ - double max=P[0]; - int modeID=0; - for(int i=1;imax){modeID=i;} - }//endfor i - return modeID; - }//end ModeID - - void Randomize(){ - for(int i=0;i pairs; - Pair pair; - for(int i=0;i tmpProb; - tmpProb=*this; - for(int i=0;i values; -};//endclass TemporalSample - -class TemporalData{ -public: - vector refTimes;//E.g. 1900,2000 => intervals are (-inf,1900) [1900,2000) [2000,inf) - vector data; - vector > > statistics;//(refYears.size+1)xdimValuex3; #samples,mean,stdev - int dimValue; - - void PrintTimeIntervals(){ - cout<<"(-inf,"< > > values; - values.resize(refTimes.size()+1); - int timeID; - for(int n=0;n=refTimes[i]){timeID=i+1; - }else{break; - }//endif - }//endfor i - values[timeID].push_back(data[n].values); - }//endfor n - dimValue=data[0].dimValue; - - statistics.clear(); - statistics.resize(refTimes.size()+1); - for(int i=0;i vd; - for(int n=0;n -#include -#include -#include -#include -#include -#include -#include"BasicCalculation_v170122.hpp" -using namespace std; - -inline void DeleteHeadSpace(string &buf){ - size_t pos; - while((pos = buf.find_first_of("  \t")) == 0){ - buf.erase(buf.begin()); - if(buf.empty()) break; - }//endwhile -}//end DeleteHeadSpace - -inline vector UnspaceString(string str){ - vector vs; - while(str.size()>0){ - DeleteHeadSpace(str); - if(str=="" || isspace(str[0])!=0){break;} - vs.push_back(str.substr(0,str.find_first_of("  \t"))); - for(int i=0;i0){ - for(int i=1;i<=curKeyFifth;i+=1){ - if(dc==intToDitchclass((i*4-1+70)%7+1)){return "#";} - }//endfor i - return ""; - }else if(curKeyFifth<0){ - for(int i=1;i<=-1*curKeyFifth;i+=1){ - if(dc==intToDitchclass((i*3+4-1+70)%7+1)){return "b";} - }//endfor i - return ""; - }else{ - return ""; - }//endif -}; - -inline string ditchUp(string principal,char acc_rel,int curKeyFifth){ - char dc=intToDitchclass( (ditchclassToInt(principal[0])+7-1+1)%7+1 ); - int oct=principal[principal.size()-1]-'0'; - if(dc=='C'){oct+=1;} - string acc=""; - if(acc_rel!='*'){ - if(acc_rel=='n'){acc=""; - }else if(acc_rel=='s'){acc="#"; - }else if(acc_rel=='S'){acc="##"; - }else if(acc_rel=='f'){acc="b"; - }else if(acc_rel=='F'){acc="bb"; - }//endif - }else{ - acc=acc_norm(dc,curKeyFifth); - }//endif - stringstream ss; - ss.str(""); ss< sitches;//size = numNotes - vector notetypes;//size = numNotes -// vector notenums;//size = numNotes - vector fmt1IDs;//size = numNotes - vector ties;// = 0(def)/1 if the note is NOT/is tied with a previous note. (Used only if tieinfo > 0) (size = numNotes) - string info;//used for type "attributes" -};//end class Fmt1xEvent - -class Fmt1x{ -public: - int TPQN; - vector evts; - vector comments; - - void ReadFile(string filename){ - vector v(100); - vector d(100); - vector s(100); - stringstream ss; - - evts.clear(); - comments.clear(); - - ifstream ifs(filename.c_str()); - if(!ifs.is_open()){cout<<"File not found: "<>s[0]){ - if(s[0][0]=='/'||s[0][0]=='#'){ - if(s[0]=="//TPQN:"){ - ifs>>TPQN; - getline(ifs,s[99]); - }else if(s[0]=="//Fmt1xVersion:"){ - ifs>>s[1]; - if(s[1]!="170104"){ - cout<<"Warning: The fmt1x version is not 170104!"<>evt.barnum>>evt.part>>evt.staff>>evt.voice>>evt.eventtype; - evt.sitches.clear(); evt.notetypes.clear(); evt.fmt1IDs.clear(); evt.ties.clear(); - evt.info=""; - if(evt.eventtype=="attributes"){ - getline(ifs,evt.info); - evt.info+="\n"; - }else if(evt.eventtype=="rest"||evt.eventtype=="chord"||evt.eventtype=="tremolo-s"||evt.eventtype=="tremolo-e"){ - ifs>>evt.dur>>evt.tieinfo>>evt.numNotes; - for(int j=0;j>s[8]; evt.sitches.push_back(s[8]);}//endfor j - for(int j=0;j>s[8]; evt.notetypes.push_back(s[8]);}//endfor j - for(int j=0;j>s[8]; evt.fmt1IDs.push_back(s[8]);}//endfor j - for(int j=0;j>v[8]; evt.ties.push_back(v[8]);}//endfor j - getline(ifs,s[99]); - }else{ - getline(ifs,s[99]); - continue; - }//endif - evts.push_back(evt); - }//endwhile - ifs.close(); - - //cout< v(100); - vector d(100); - vector s(100); - stringstream ss; - - evts.clear(); - comments.clear(); - - ifstream ifs(filename.c_str()); - if(!ifs.is_open()){cout<<"File not found: "< events; - vector depths; - vector inout; -{ - bool isInBracket=false; - int depth=0; - for(int i=0;i'){ - isInBracket=false; - if(all[i-1]=='/'){depth-=1;} - events.push_back(s[0]); - depths.push_back(depth); - inout.push_back(true); - s[0]=""; - continue; - }//endif - s[0]+=all[i]; - }//endfor i -}// - -{ - vector prev_events(events); - vector prev_depths(depths); - vector prev_inout(inout); - events.clear(); - depths.clear(); - inout.clear(); - for(int i=0;i divs; - for(int i=1;i curClefLab; - vector curClefOctChange; - curClefLab.push_back("G2"); - curClefOctChange.push_back(0); - v[0]=0; - int notenum; - string trem; - string curScorePart="P0"; - string curInstrumentName; - - for(int i=1;i1)? 1:0) ); - evt.tieinfo=tie; - evts.push_back(evt); - }else{ - ss.str(""); - ss<1)? 1:0) ); - if(tie%2==1 && evts[evts.size()-1].tieinfo%2==0){ - evts[evts.size()-1].tieinfo+=1; - }//enduf - if(tie/2==1 && evts[evts.size()-1].tieinfo/2==0){ - evts[evts.size()-1].tieinfo+=2; - }//enduf - }//endif - notein=false; - }//endif - }//endfor i - - }//end ReadMusicXML - -};//endclass Fmt1x - - -#endif // FMT1X_HPP diff --git a/MusicXMLParser/MusicXMLToFmt1x_v170104.cpp b/MusicXMLParser/MusicXMLToFmt1x_v170104.cpp deleted file mode 100755 index a95681f..0000000 --- a/MusicXMLParser/MusicXMLToFmt1x_v170104.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include"Fmt1x_v170108_2.hpp" -using namespace std; - -int main(int argc, char** argv) { - - vector v(100); - vector d(100); - vector s(100); - stringstream ss; - - if(argc!=3){cout<<"Error in usage! : $./this in.xml out_fmt1x.txt"<MusicXmlConverter class is used to convert a given output from the MusicXMLParser - * into a format that can be read by the MV2H package (using the toString() method). - * - * @author Andrew McLeod - */ -public class MusicXmlConverter extends Converter { - /** - * The notes present in the XML piece. - */ - private List notes = new ArrayList(); - - /** - * The hierarchies of this piece. - */ - private List hierarchies = new ArrayList(); - - /** - * The tick of the starting time of each hierarchy. - */ - private List hierarchyTicks = new ArrayList(); - - /** - * A list of the key signatures of this piece. - */ - private List keys = new ArrayList(); - - /** - * A list of the notes for which there hasn't yet been an offset. - */ - private List unfinishedNotes = new ArrayList(); - - /** - * The last tick of the piece. - */ - private int lastTick = 0; - - /** - * The first tick of the piece. - */ - private int firstTick = Integer.MAX_VALUE; - - /** - * The bar of the previous line. This is kept updated until the anacrusis is handled. - *
- * @see #handleAnacrusis(int, int, int, int) - */ - private int previousBar = -1; - - /** - * The number of ticks per quarter note. 1 tick is 1 tatum. Defaults to 4. - */ - private int ticksPerQuarterNote = 4; - - /** - * A mapping for XML voices ("part_staff_voice" strings) to 0-indexed MV2H voices. - */ - private final Map voiceMap; - - /** - * Create a new MusicXmlConverter object by parsing the input from the MusicXMLParser. - *
- * This method contains the main program logic, and printing is handled by - * {@link #toString()}. - * - * @param stream The MusicXMLParser output to convert. - */ - public MusicXmlConverter(InputStream stream) { - Scanner in = new Scanner(stream); - int lineNum = 0; - boolean anacrusisHandled = false; - - voiceMap = new HashMap(); - - while (in.hasNextLine()) { - lineNum++; - String line = in.nextLine(); - - // Skip comment lines - if (line.startsWith("//")) { - continue; - } - - String[] attributes = line.split("\t"); - if (attributes.length < 5) { - // Error if fewer than 5 columns - System.err.println("WARNING: Line type not found. Skipping line " + lineNum + ": " + line); - continue; - } - - int tick = Integer.parseInt(attributes[0]); - - // Zero out unused voice markers - if (!Converter.PART) { - attributes[2] = "0"; - } - if (!Converter.STAFF) { - attributes[3] = "0"; - } - if (!Converter.VOICE) { - attributes[4] = "0"; - } - String xmlVoice = attributes[2] + "_" + attributes[3] + "_" + attributes[4]; - - if (!voiceMap.containsKey(xmlVoice)) { - voiceMap.put(xmlVoice, voiceMap.size()); - } - int voice = voiceMap.get(xmlVoice); - - lastTick = Math.max(tick, lastTick); - firstTick = Math.min(tick, firstTick); - - // Switch for different types of lines - switch (attributes[5]) { - // Attributes is the base line type describing time signature, tempo, etc. - case "attributes": - ticksPerQuarterNote = Integer.parseInt(attributes[6]); - - // Time signature - int tsNumerator = Integer.parseInt(attributes[9]); - int tsDenominator = Integer.parseInt(attributes[10]); - - int beatsPerBar = tsNumerator; - int subBeatsPerBeat = 2; - - int subBeatsPerQuarterNote = tsDenominator / 2; - - // Check for compound meter - if (beatsPerBar % 3 == 0 && beatsPerBar > 3) { - beatsPerBar /= 3; - subBeatsPerBeat = 3; - - subBeatsPerQuarterNote = tsDenominator / 4; - } - - int tatumsPerSubBeat = ticksPerQuarterNote / subBeatsPerQuarterNote; - - // Add the new time signature (if it is new) - Hierarchy mostRecent = hierarchies.isEmpty() ? null : hierarchies.get(hierarchies.size() - 1); - if (mostRecent == null || mostRecent.beatsPerBar != beatsPerBar || - mostRecent.subBeatsPerBeat != subBeatsPerBeat || mostRecent.tatumsPerSubBeat != tatumsPerSubBeat) { - hierarchyTicks.add(tick); - hierarchies.add(new Hierarchy(beatsPerBar, subBeatsPerBeat, tatumsPerSubBeat, 0, getTimeFromTick(tick))); - } - - // Key signature - int keyFifths = Integer.parseInt(attributes[7]); - String keyMode = attributes[8]; - - int tonic = ((7 * keyFifths) + 144) % 12; - boolean mode = keyMode.equalsIgnoreCase("Major"); - - // Add the new key (if it is new) - Key mostRecentKey = keys.isEmpty() ? null : keys.get(keys.size() - 1); - if (mostRecentKey == null || mostRecentKey.tonic != tonic || mostRecentKey.isMajor != mode) { - keys.add(new Key(tonic, mode, getTimeFromTick(tick))); - } - - break; - - case "rest": - // Handle anacrusis - if (!anacrusisHandled) { - anacrusisHandled = handleAnacrusis(Integer.parseInt(attributes[1]), tick); - } - break; - - case "chord": - // There are notes here - - // Handle anacrusis - if (!anacrusisHandled) { - anacrusisHandled = handleAnacrusis(Integer.parseInt(attributes[1]), tick); - } - - int duration = Integer.parseInt(attributes[6]); - lastTick = Math.max(tick + duration, lastTick); - - int tieInfo = Integer.parseInt(attributes[7]); - int numNotes = Integer.parseInt(attributes[8]); - - // Get all of the pitches - int[] pitches = new int[numNotes]; - for (int i = 0; i < numNotes; i++) { - try { - pitches[i] = getPitchFromString(attributes[9 + i]); - } catch (IOException e) { - System.err.println("WARNING: " + e.getMessage() + " Skipping line " + lineNum + ": " + line); - continue; - } - } - - // Handle each pitch - for (int pitch : pitches) { - switch (tieInfo) { - // No tie - case 0: - notes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); - break; - - // Tie out - case 1: - unfinishedNotes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); - break; - - // Tie in - case 2: - try { - Note matchedNote = findAndRemoveUnfinishedNote(pitch, getTimeFromTick(tick), voice); - notes.add(new Note(pitch, matchedNote.onsetTime, matchedNote.valueOnsetTime, getTimeFromTick(tick), voice)); - } catch (IOException e) { - System.err.println("WARNING: " + e.getMessage() + " Adding tied note as new note " + lineNum + ": " + line); - notes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); - } - break; - - // Tie in and out - case 3: - try { - Note matchedNote = findAndRemoveUnfinishedNote(pitch, getTimeFromTick(tick), voice); - unfinishedNotes.add(new Note(pitch, matchedNote.onsetTime, matchedNote.valueOnsetTime, getTimeFromTick(tick + duration), voice)); - } catch (IOException e) { - System.err.println("WARNING: " + e.getMessage() + " Skipping note on line " + lineNum + ": " + line); - unfinishedNotes.add(new Note(pitch, getTimeFromTick(tick), getTimeFromTick(tick), getTimeFromTick(tick + duration), voice)); - } - break; - - // ??? - default: - System.err.println("WARNING: Unknown tie type " + tieInfo + ". Skipping line " + lineNum + ": " + line); - break; - } - } - break; - - case "tremolo-m": - duration = Integer.parseInt(attributes[6]); - lastTick = Math.max(tick + duration, lastTick); - - numNotes = Integer.parseInt(attributes[8]); - - pitches = new int[numNotes]; - for (int i = 0; i < numNotes; i++) { - try { - pitches[i] = getPitchFromString(attributes[9 + i]); - } catch (IOException e) { - System.err.println("WARNING: " + e.getMessage() + " Skipping line " + lineNum + ": " + line); - continue; - } - } - - for (int pitch : pitches) { - // TODO: Decide how many notes to add here. i.e., default to eighth notes for now? - int ticksPerTremolo = ticksPerQuarterNote / 2; - int numTremolos = duration / ticksPerTremolo; - - - for (int i = 0; i < numTremolos; i++) { - notes.add(new Note(pitch, getTimeFromTick(tick + ticksPerTremolo * i), getTimeFromTick(tick + ticksPerTremolo * i), getTimeFromTick(tick + ticksPerTremolo * (i + 1)), voice)); - } - } - break; - - default: - System.err.println("WARNING: Unrecognized line type. Skipping line " + lineNum + ": " + line); - continue; - } - } - - in.close(); - - // Check for any unfinished notes (because of ties out). - for (Note note : unfinishedNotes) { - System.err.println("WARNING: Tie never ended for note " + note + ". Adding note as untied."); - notes.add(note); - } - } - - /** - * Handle any anacrusis, if possible. First, detect if at least 1 bar has finished. If it has, - * check how long the previous bar was, and set the first anacrusis according to that. - * - * @param bar The bar number of the current line. This will be compared to {@link #previousBar} - * to check if a bar has just finished. - * @param tick The tick of the current line. - * @return True if the anacrusis has now been handled. False otherwise. - */ - private boolean handleAnacrusis(int bar, int tick) { - if (previousBar == -1) { - // This is the first bar we've seen - previousBar = bar; - - } else if (previousBar != bar) { - // Ready to handle the anacrusis - - // Add a default 4/4 at time 0 if no hierarchy has been seen yet - if (hierarchies.isEmpty()) { - hierarchies.add(new Hierarchy(4, 2, ticksPerQuarterNote / 2, 0, 0)); - } - - if (hierarchies.size() != 1) { - System.err.println("Warning: More than 1 time signature seen in the first bar."); - } - - // Duplicate mostRecent, but with correct anacrusis (tick % tatumsPerBar) - Hierarchy mostRecent = hierarchies.get(hierarchies.size() - 1); - int tatumsPerBar = mostRecent.beatsPerBar * mostRecent.subBeatsPerBeat * mostRecent.tatumsPerSubBeat; - hierarchies.set(hierarchies.size() - 1, new Hierarchy(mostRecent.beatsPerBar, mostRecent.subBeatsPerBeat, - mostRecent.tatumsPerSubBeat, tick % tatumsPerBar, mostRecent.time)); - - return true; - } - - return false; - } - - /** - * Find a tied out note that matches a new tied in note, return it, and remove it from - * {@link #unfinishedNotes}. - * - * @param pitch The pitch of the tie. - * @param valueOnsetTime The onset time of the tied in note. - * @param voice The voice of the tied in note. - * - * @return The note from {@link #unfinishedNotes} that matches the pitch onset time and voice. - * @throws IOException If no matching note is found. - */ - private Note findAndRemoveUnfinishedNote(int pitch, int valueOnsetTime, int voice) throws IOException { - Iterator noteIterator = unfinishedNotes.iterator(); - while (noteIterator.hasNext()) { - Note note = noteIterator.next(); - - if (note.pitch == pitch && note.valueOffsetTime == valueOnsetTime && note.voice == voice) { - noteIterator.remove(); - return note; - } - } - - throw new IOException("Tied note not found at pitch=" + pitch + " offset=" + valueOnsetTime + " voice=" + voice + "."); - } - - /** - * Convert from XML tick to time, using {@link #MS_PER_BEAT}. - * - * @param tick The XML tick. - * @return The time, in milliseconds. - */ - private int getTimeFromTick(int tick) { - if (hierarchies.isEmpty()) { - return tick; - } - - int i; - for (i = 0; i < hierarchies.size() - 1; i++) { - if (hierarchyTicks.get(i + 1) > tick) { - break; - } - } - - Hierarchy hierarchy = hierarchies.get(i); - int hierarchyTick = hierarchyTicks.get(i); - - return hierarchy.time + (int) Math.round(((double) tick - hierarchyTick) / hierarchy.tatumsPerSubBeat / hierarchy.subBeatsPerBeat * MS_PER_BEAT); - } - - /** - * Create and return a list of tatums based on the parsed {@link #hierarchy}, {@link #firstTick}, and - * {@link #lastTick}. - * - * @return A list of the parsed tatums. - */ - private List getTatums() { - List tatums = new ArrayList(lastTick - firstTick); - - for (int tick = firstTick; tick < lastTick; tick++) { - tatums.add(new Tatum(getTimeFromTick(tick))); - } - - return tatums; - } - - /** - * Return a String version of the parsed musical score into our mv2h format. - * - * @return The parsed musical score. - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - for (Note note : notes) { - sb.append(note).append('\n'); - } - - for (Tatum tatum : getTatums()) { - sb.append(tatum).append('\n'); - } - - for (Key key : keys) { - sb.append(key).append('\n'); - } - - for (Hierarchy hierarchy : hierarchies) { - sb.append(hierarchy).append('\n'); - } - - return sb.toString(); - } - - /** - * Get the pitch number of a note given its String. - * - * @param pitchString A pitch String, like C##4, or Ab1, or G7. - * @return The number of the given pitch, with A4 = 440Hz = 69. - * - * @throws IOException If a parse error occurs. - */ - private static int getPitchFromString(String pitchString) throws IOException { - char pitchChar = pitchString.charAt(0); - int pitch; - switch (pitchChar) { - case 'C': - pitch = 0; - break; - - case 'D': - pitch = 2; - break; - - case 'E': - pitch = 4; - break; - - case 'F': - pitch = 5; - break; - - case 'G': - pitch = 7; - break; - - case 'A': - pitch = 9; - break; - - case 'B': - pitch = 11; - break; - - default: - throw new IOException("Pith " + pitchChar + " not recognized."); - } - - int accidental = pitchString.length() - pitchString.replace("#", "").length(); - accidental -= pitchString.length() - pitchString.replace("b", "").length(); - - int octave = Integer.parseInt(pitchString.substring(1).replace("#", "").replace("b", "")); - - return (octave + 1) * 12 + pitch + accidental; - } -} From 10c794cee8e6464c863bb480b7b1316b6563de14 Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Tue, 8 Dec 2020 20:20:48 +0100 Subject: [PATCH 04/13] Added AlignmentNode. Should enumerate them. --- src/mv2h/Main.java | 11 +++--- src/mv2h/tools/Aligner.java | 58 ++++++++++++++++++------------- src/mv2h/tools/AlignmentNode.java | 57 ++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 src/mv2h/tools/AlignmentNode.java diff --git a/src/mv2h/Main.java b/src/mv2h/Main.java index e7b58d5..12bdb30 100644 --- a/src/mv2h/Main.java +++ b/src/mv2h/Main.java @@ -5,12 +5,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Scanner; -import java.util.Set; import mv2h.objects.MV2H; import mv2h.objects.Music; import mv2h.objects.Note; import mv2h.tools.Aligner; +import mv2h.tools.AlignmentNode; /** * The Main class is the class called to evaluate anything with the MV2H package. @@ -180,10 +180,12 @@ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionF MV2H best = new MV2H(0, 0, 0, 0, 0); List bestAlignment = new ArrayList(); - Set> alignments = Aligner.getPossibleAlignments(groundTruth, transcription); + List alignmentNodes = Aligner.getPossibleAlignments(groundTruth, transcription); int i = 0; - for (List alignment : alignments) { - System.out.print("Evaluating alignment " + (i++) + " / " + alignments.size() + "\r"); + for (AlignmentNode alignmentNode : alignmentNodes) { + System.out.print("Evaluating alignment " + (++i) + " / " + alignmentNodes.size() + "\r"); + + List alignment = alignmentNode.getAlignment(); MV2H candidate = groundTruth.evaluateTranscription(transcription.align(groundTruth, alignment)); @@ -192,6 +194,7 @@ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionF bestAlignment = alignment; } } + System.out.println(); if (PRINT_ALIGNMENT) { System.out.println("ALIGNMENT"); diff --git a/src/mv2h/tools/Aligner.java b/src/mv2h/tools/Aligner.java index dfd9290..40762ff 100644 --- a/src/mv2h/tools/Aligner.java +++ b/src/mv2h/tools/Aligner.java @@ -1,11 +1,9 @@ package mv2h.tools; import java.util.ArrayList; -import java.util.HashSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.Map.Entry; import mv2h.Main; @@ -29,13 +27,25 @@ public class Aligner { * @param gt The ground truth. * @param m The transcription. * - * @return A Set of all possible alignments of the transcription to the ground truth. - * An alignment is a list containing, for each ground truth note list, the index of the transcription - * note list to which it is aligned, or -1 if it was not aligned with any transcription note. + * @return A List of all possible alignments of the transcription to the ground truth. + * An alignment is a list containing, for each ground truth note, the index of the transcription + * note to which it is aligned, or -1 if it was not aligned with any transcription note. */ - public static Set> getPossibleAlignments(Music gt, Music m) { + public static List getPossibleAlignments(Music gt, Music m) { + System.out.println("Calculating alignment matrix..."); List>> previousCells = getAlignmentMatrix(gt.getNoteLists(), m.getNoteLists()); - return getPossibleAlignmentsFromMatrix(previousCells.size() - 1, previousCells.get(0).size() - 1, previousCells); + + System.out.println("Calculating alignment paths..."); + List>> alignmentCache = new ArrayList>>(previousCells.size()); + for (int i = 0; i < previousCells.size(); i++) { + List> nestedList = new ArrayList>(previousCells.get(0).size()); + for (int j = 0; j < previousCells.get(0).size(); j++) { + nestedList.add(new ArrayList()); + } + alignmentCache.add(nestedList); + } + + return getPossibleAlignmentsFromMatrix(previousCells.size() - 1, previousCells.get(0).size() - 1, previousCells, alignmentCache); } /** @@ -47,39 +57,39 @@ public static Set> getPossibleAlignments(Music gt, Music m) { * @param j The second index, representing the ground truth note index. * @param previousCells The previous cells matrix from {@link #getAlignmentMatrix(List, List)}. * - * @return A Set of all possible alignments given the previous cells matrix, up to notes i, j. + * @return A List of all possible alignments given the previous cells matrix, up to notes i, j. * An alignment is a list containing, for each ground truth note list, the index of the transcription * note list to which it is aligned, or -1 if it was not aligned with any transcription note. */ - private static Set> getPossibleAlignmentsFromMatrix(int i, int j, List>> previousCells) { - Set> alignments = new HashSet>(); + private static List getPossibleAlignmentsFromMatrix(int i, int j, List>> previousCells, List>> alignmentCache) { + List alignments = alignmentCache.get(i).get(j); + if (!alignments.isEmpty()) { + return alignments; + } // Base case. we are at the beginning and nothing else needs to be aligned. if (i == 0 && j == 0) { - alignments.add(new ArrayList()); + alignments.add(null); return alignments; } for (int previousCell : previousCells.get(i).get(j)) { if (previousCell == -1) { - // This transcription note was aligned with nothing in the ground truth. Add -1. - for (List list : getPossibleAlignmentsFromMatrix(i - 1, j, previousCells)) { - list.add(-1); - alignments.add(list); + // This transcription note was aligned with nothing in the ground truth. + for (AlignmentNode prev : getPossibleAlignmentsFromMatrix(i - 1, j, previousCells, alignmentCache)) { + alignments.add(new AlignmentNode(prev, -1)); } } else if (previousCell == 1) { - // This ground truth note was aligned with nothing in the transcription. Skip it. - for (List list : getPossibleAlignmentsFromMatrix(i, j - 1, previousCells)) { - alignments.add(list); + // This ground truth note was aligned with nothing in the transcription. + for (AlignmentNode prev : getPossibleAlignmentsFromMatrix(i, j - 1, previousCells, alignmentCache)) { + alignments.add(new AlignmentNode(prev, AlignmentNode.NO_ALIGNMENT)); } } else { - // The current transcription and ground truth notes were aligned. Add the current ground - // truth index to the alignment list. - for (List list : getPossibleAlignmentsFromMatrix(i - 1, j - 1, previousCells)) { - list.add(j - 1); - alignments.add(list); + // The current transcription and ground truth notes were aligned. + for (AlignmentNode prev : getPossibleAlignmentsFromMatrix(i - 1, j - 1, previousCells, alignmentCache)) { + alignments.add(new AlignmentNode(prev, j - 1)); } } } @@ -354,4 +364,4 @@ private static int convertTime(int time, int gtPreviousAnchor, int gtNextAnchor, return (int) Math.round(rate * (time - mPreviousTime) + gtPreviousTime); } -} \ No newline at end of file +} diff --git a/src/mv2h/tools/AlignmentNode.java b/src/mv2h/tools/AlignmentNode.java new file mode 100644 index 0000000..12cf762 --- /dev/null +++ b/src/mv2h/tools/AlignmentNode.java @@ -0,0 +1,57 @@ +package mv2h.tools; + +import java.util.ArrayList; +import java.util.List; + +/** + * The AlignmentNode class is used to help when aligning musical scores + * ({@link mv2h.opbjects.Music} objects). It implements a backwards-linked list of alignments, + * and can be used to generate an alignment list at each node. + * + * @author Andrew McLeod + */ +public class AlignmentNode { + /** + * The previous AlignmentNode in the backwards-linked list. + */ + public final AlignmentNode prev; + + /** + * The value to add to the alignment list for this node. Use {@link #NO_ALIGNMENT} + * for no aligned transcription note. + */ + public final int value; + + /** + * The value to use to designate no aligned transcription note. + */ + public static final int NO_ALIGNMENT = -2; + + /** + * Create a new AlignmentNode. + * + * @param prev {@link #prev} + * @param value {@link #value} + */ + public AlignmentNode(AlignmentNode prev, int value) { + this.prev = prev; + this.value = value; + } + + /** + * Generate the alignment list from the node. + * + * @return An alignment for this node. + * An alignment is a list containing, for each ground truth note, the index of the transcription + * note to which it is aligned, or -1 if it was not aligned with any transcription note. + */ + public List getAlignment() { + List list = prev == null ? new ArrayList() : prev.getAlignment(); + + if (value != NO_ALIGNMENT) { + list.add(value); + } + + return list; + } +} From 22d6f3127e1fa0e89998ca28e1f11df020a25abf Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Wed, 9 Dec 2020 12:01:58 +0100 Subject: [PATCH 05/13] AlignmentNode LL now efficient --- src/mv2h/Main.java | 19 +++++++---- src/mv2h/tools/Aligner.java | 13 +++----- src/mv2h/tools/AlignmentNode.java | 55 ++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/mv2h/Main.java b/src/mv2h/Main.java index 12bdb30..c191812 100644 --- a/src/mv2h/Main.java +++ b/src/mv2h/Main.java @@ -181,17 +181,24 @@ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionF List bestAlignment = new ArrayList(); List alignmentNodes = Aligner.getPossibleAlignments(groundTruth, transcription); + int total = 0; + for (AlignmentNode alignmentNode : alignmentNodes) { + total += alignmentNode.count; + } + int i = 0; for (AlignmentNode alignmentNode : alignmentNodes) { - System.out.print("Evaluating alignment " + (++i) + " / " + alignmentNodes.size() + "\r"); + for (int alignmentIndex = 0; alignmentIndex < alignmentNode.count; alignmentIndex++) { + System.out.print("Evaluating alignment " + (++i) + " / " + total + "\r"); - List alignment = alignmentNode.getAlignment(); + List alignment = alignmentNode.getAlignment(alignmentIndex); - MV2H candidate = groundTruth.evaluateTranscription(transcription.align(groundTruth, alignment)); + MV2H candidate = groundTruth.evaluateTranscription(transcription.align(groundTruth, alignment)); - if (candidate.compareTo(best) > 0) { - best = candidate; - bestAlignment = alignment; + if (candidate.compareTo(best) > 0) { + best = candidate; + bestAlignment = alignment; + } } } System.out.println(); diff --git a/src/mv2h/tools/Aligner.java b/src/mv2h/tools/Aligner.java index 40762ff..d44f222 100644 --- a/src/mv2h/tools/Aligner.java +++ b/src/mv2h/tools/Aligner.java @@ -69,28 +69,25 @@ private static List getPossibleAlignmentsFromMatrix(int i, int j, // Base case. we are at the beginning and nothing else needs to be aligned. if (i == 0 && j == 0) { - alignments.add(null); return alignments; } for (int previousCell : previousCells.get(i).get(j)) { if (previousCell == -1) { // This transcription note was aligned with nothing in the ground truth. - for (AlignmentNode prev : getPossibleAlignmentsFromMatrix(i - 1, j, previousCells, alignmentCache)) { - alignments.add(new AlignmentNode(prev, -1)); - } + alignments.add(new AlignmentNode(getPossibleAlignmentsFromMatrix(i - 1, j, previousCells, alignmentCache), -1)); } else if (previousCell == 1) { // This ground truth note was aligned with nothing in the transcription. for (AlignmentNode prev : getPossibleAlignmentsFromMatrix(i, j - 1, previousCells, alignmentCache)) { - alignments.add(new AlignmentNode(prev, AlignmentNode.NO_ALIGNMENT)); + if (prev.value != -1) { + alignments.add(prev); + } } } else { // The current transcription and ground truth notes were aligned. - for (AlignmentNode prev : getPossibleAlignmentsFromMatrix(i - 1, j - 1, previousCells, alignmentCache)) { - alignments.add(new AlignmentNode(prev, j - 1)); - } + alignments.add(new AlignmentNode(getPossibleAlignmentsFromMatrix(i - 1, j - 1, previousCells, alignmentCache), j - 1)); } } diff --git a/src/mv2h/tools/AlignmentNode.java b/src/mv2h/tools/AlignmentNode.java index 12cf762..399d4f0 100644 --- a/src/mv2h/tools/AlignmentNode.java +++ b/src/mv2h/tools/AlignmentNode.java @@ -12,9 +12,9 @@ */ public class AlignmentNode { /** - * The previous AlignmentNode in the backwards-linked list. + * A List of previous AlignmentNodes in the backwards-linked list. */ - public final AlignmentNode prev; + public final List prevList; /** * The value to add to the alignment list for this node. Use {@link #NO_ALIGNMENT} @@ -23,35 +23,60 @@ public class AlignmentNode { public final int value; /** - * The value to use to designate no aligned transcription note. + * How many alignment lists pass through this node. */ - public static final int NO_ALIGNMENT = -2; + public final int count; /** * Create a new AlignmentNode. * - * @param prev {@link #prev} + * @param prevList {@link #prevList} * @param value {@link #value} */ - public AlignmentNode(AlignmentNode prev, int value) { - this.prev = prev; - this.value = value; + public AlignmentNode(List prevList, int value) { + this.prevList = prevList; + this.value = value; + + int count = 0; + for (AlignmentNode prev : this.prevList) { + count += prev.count; + } + this.count = Math.max(count, 1); } /** - * Generate the alignment list from the node. + * Generate an alignment list from the node. + * + * @param index The index of the alignment node to return (since multiple lists pass + * through this node). * * @return An alignment for this node. * An alignment is a list containing, for each ground truth note, the index of the transcription * note to which it is aligned, or -1 if it was not aligned with any transcription note. */ - public List getAlignment() { - List list = prev == null ? new ArrayList() : prev.getAlignment(); + public List getAlignment(int index) { + List alignment = null; + + if (prevList.isEmpty()) { + // Base case + alignment = new ArrayList(); + + } else { + // Find the correct previous node based on the index + for (AlignmentNode prev : prevList) { + if (index < prev.count) { + // Previous node found. Get prev list. + alignment = prev.getAlignment(index); + break; + } + + // Previous node not yet found. Decrememnt index and find the previous list. + index -= prev.count; + } + } - if (value != NO_ALIGNMENT) { - list.add(value); - } + alignment.add(value); - return list; + return alignment; } } From 4c06e085cefa556f954e5d7c50a6c825e073a08e Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Wed, 9 Dec 2020 14:52:25 +0100 Subject: [PATCH 06/13] Added -p and raised default insert/delete error --- src/mv2h/Main.java | 23 ++++++++++++++++++++++- src/mv2h/tools/Aligner.java | 4 ++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/mv2h/Main.java b/src/mv2h/Main.java index c191812..5c04046 100644 --- a/src/mv2h/Main.java +++ b/src/mv2h/Main.java @@ -60,6 +60,13 @@ public class Main { */ private static boolean PRINT_ALIGNMENT = false; + /** + * The penalty assigned for insertion and deletion errors when performing alignment. + * The default value of 1 leads to a reasonably fast, but not exhaustive + * search through alignments. Can be set with the -p flag. + */ + public static double NON_ALIGNMENT_PENALTY = 1.0; + /** * Run the program. There are 2 different modes. *
@@ -112,6 +119,18 @@ public static void main(String[] args) throws IOException { PERFORM_ALIGNMENT = true; break; + case 'p': + i++; + if (args.length <= i) { + argumentError("No non-alignment penalty given with -p."); + } + try { + NON_ALIGNMENT_PENALTY = Double.parseDouble(args[i]); + } catch (NumberFormatException e) { + argumentError("Non-alignment penalty must be a decimal value. Given: " + args[i]); + } + break; + // Evaluate! case 'g': i++; @@ -392,7 +411,9 @@ private static void argumentError(String message) { sb.append("-g FILE = Use the given FILE as the ground truth (defaults to std in).\n"); sb.append("-t FILE = Use the given FILE as the transcription (defaults to std in).\n"); - sb.append("Either -g or -t (or both) must be given to evaluate, since both cannot be read from std in.\n"); + sb.append("Either -g or -t (or both) must be given to evaluate, since both cannot be read from std in.\n\n"); + + sb.append("-p DOUBLE = Use the given value as the insertion and deletion penalty for alignment.\n"); sb.append("-F = Combine the scores from std in (from this program's output) into final"); sb.append(" global mean and standard deviation distributions for each score.\n"); diff --git a/src/mv2h/tools/Aligner.java b/src/mv2h/tools/Aligner.java index d44f222..e8dd1d6 100644 --- a/src/mv2h/tools/Aligner.java +++ b/src/mv2h/tools/Aligner.java @@ -134,8 +134,8 @@ private static List>> getAlignmentMatrix(List> gtN for (int i = 1; i < distances.length; i++) { double distance = getDistance(gtNoteMaps.get(i - 1), mNoteMaps.get(j - 1)); - double distance_i_1 = distances[i - 1][j] + 0.6; - double distance_j_1 = distances[i][j - 1] + 0.6; + double distance_i_1 = distances[i - 1][j] + Main.NON_ALIGNMENT_PENALTY; + double distance_j_1 = distances[i][j - 1] + Main.NON_ALIGNMENT_PENALTY; double distance_i_j_1 = distances[i - 1][j - 1] + distance; double min_distance = Math.min(Math.min(distance_i_1, distance_j_1), distance_i_j_1); From 39934ebe314a47f56be1261ec859f24c920e5383 Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Wed, 9 Dec 2020 14:59:28 +0100 Subject: [PATCH 07/13] Removed some printing --- src/mv2h/Main.java | 1 - src/mv2h/tools/Aligner.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/mv2h/Main.java b/src/mv2h/Main.java index 5c04046..f978361 100644 --- a/src/mv2h/Main.java +++ b/src/mv2h/Main.java @@ -220,7 +220,6 @@ public static void evaluateGroundTruth(File groundTruthFile, File transcriptionF } } } - System.out.println(); if (PRINT_ALIGNMENT) { System.out.println("ALIGNMENT"); diff --git a/src/mv2h/tools/Aligner.java b/src/mv2h/tools/Aligner.java index e8dd1d6..873ed8c 100644 --- a/src/mv2h/tools/Aligner.java +++ b/src/mv2h/tools/Aligner.java @@ -32,10 +32,8 @@ public class Aligner { * note to which it is aligned, or -1 if it was not aligned with any transcription note. */ public static List getPossibleAlignments(Music gt, Music m) { - System.out.println("Calculating alignment matrix..."); List>> previousCells = getAlignmentMatrix(gt.getNoteLists(), m.getNoteLists()); - System.out.println("Calculating alignment paths..."); List>> alignmentCache = new ArrayList>>(previousCells.size()); for (int i = 0; i < previousCells.size(); i++) { List> nestedList = new ArrayList>(previousCells.get(0).size()); From feaca5f2ad07141a3c05f3fa85a904c4f7733370 Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Fri, 18 Dec 2020 18:27:52 +0100 Subject: [PATCH 08/13] Removed XML converter and dataset --- .gitignore | 3 +- README.md | 101 +- dataset/converted/C-1.xml.conv | 612 - dataset/converted/C-10.xml.conv | 364 - dataset/converted/C-12.xml.conv | 234 - dataset/converted/C-13.xml.conv | 303 - dataset/converted/C-14.xml.conv | 420 - dataset/converted/C-15.xml.conv | 322 - dataset/converted/C-16.xml.conv | 140 - dataset/converted/C-17.xml.conv | 641 - dataset/converted/C-18.xml.conv | 584 - dataset/converted/C-19.xml.conv | 577 - dataset/converted/C-2.xml.conv | 308 - dataset/converted/C-3.xml.conv | 374 - dataset/converted/C-8.xml.conv | 295 - dataset/converted/C-9.xml.conv | 335 - dataset/converted/F-1.xml.conv | 1187 - dataset/converted/F-10.xml.conv | 850 - dataset/converted/F-11.xml.conv | 903 - dataset/converted/F-12.xml.conv | 773 - dataset/converted/F-13.xml.conv | 423 - dataset/converted/F-14.xml.conv | 877 - dataset/converted/F-15.xml.conv | 805 - dataset/converted/F-16.xml.conv | 754 - dataset/converted/F-17.xml.conv | 1046 - dataset/converted/F-18.xml.conv | 970 - dataset/converted/F-19.xml.conv | 947 - dataset/converted/F-2.xml.conv | 775 - dataset/converted/F-3.xml.conv | 904 - dataset/converted/F-4.xml.conv | 794 - dataset/converted/F-5.xml.conv | 785 - dataset/converted/F-6.xml.conv | 759 - dataset/converted/F-7.xml.conv | 944 - dataset/converted/F-8.xml.conv | 902 - dataset/converted/F-9.xml.conv | 969 - dataset/converted/G-1.xml.conv | 1168 - dataset/converted/G-10.xml.conv | 728 - dataset/converted/G-11.xml.conv | 782 - dataset/converted/G-12.xml.conv | 403 - dataset/converted/G-13.xml.conv | 518 - dataset/converted/G-14.xml.conv | 816 - dataset/converted/G-15.xml.conv | 558 - dataset/converted/G-16.xml.conv | 340 - dataset/converted/G-17.xml.conv | 1018 - dataset/converted/G-2.xml.conv | 605 - dataset/converted/G-3.xml.conv | 827 - dataset/converted/G-4.xml.conv | 582 - dataset/converted/G-7.xml.conv | 855 - dataset/converted/G-8.xml.conv | 846 - dataset/converted/G-9.xml.conv | 719 - dataset/converted/K-1.xml.conv | 1135 - dataset/converted/K-10.xml.conv | 287 - dataset/converted/K-12.xml.conv | 166 - dataset/converted/K-13.xml.conv | 295 - dataset/converted/K-14.xml.conv | 418 - dataset/converted/K-15.xml.conv | 326 - dataset/converted/K-16.xml.conv | 138 - dataset/converted/K-17.xml.conv | 798 - dataset/converted/K-19.xml.conv | 478 - dataset/converted/K-2.xml.conv | 209 - dataset/converted/K-3.xml.conv | 587 - dataset/converted/K-4.xml.conv | 480 - dataset/converted/K-5.xml.conv | 301 - dataset/converted/K-6.xml.conv | 230 - dataset/converted/K-7.xml.conv | 581 - dataset/converted/K-8.xml.conv | 200 - dataset/converted/K-9.xml.conv | 358 - dataset/converted/M-1.xml.conv | 3840 --- dataset/converted/M-10.xml.conv | 4976 ---- dataset/converted/M-11.xml.conv | 18459 -------------- dataset/converted/M-12.xml.conv | 11612 --------- dataset/converted/M-13.xml.conv | 626 - dataset/converted/M-14.xml.conv | 6963 ----- dataset/converted/M-15.xml.conv | 4739 ---- dataset/converted/M-16.xml.conv | 1100 - dataset/converted/M-17.xml.conv | 38722 ---------------------------- dataset/converted/M-18.xml.conv | 40558 ------------------------------ dataset/converted/M-19.xml.conv | 7085 ------ dataset/converted/M-2.xml.conv | 1769 -- dataset/converted/M-3.xml.conv | 4882 ---- dataset/converted/M-4.xml.conv | 3132 --- dataset/converted/M-5.xml.conv | 925 - dataset/converted/M-6.xml.conv | 663 - dataset/converted/M-7.xml.conv | 588 - dataset/converted/M-8.xml.conv | 391 - dataset/converted/M-9.xml.conv | 4854 ---- dataset/evaluations/1.csv | 83 - dataset/evaluations/2.csv | 1 - dataset/evaluations/3.csv | 1 - dataset/evaluations/4.csv | 83 - dataset/evaluations/5.csv | 83 - dataset/evaluations/key.txt | 133 - dataset/evaluations/regress.csv | 234 - dataset/outs/C.txt | 97 - dataset/outs/F.txt | 121 - dataset/outs/G.txt | 103 - dataset/outs/K.txt | 121 - dataset/outs/M.txt | 121 - dataset/parsed-xml/C-1.xml.txt | 382 - dataset/parsed-xml/C-10.xml.txt | 194 - dataset/parsed-xml/C-12.xml.txt | 137 - dataset/parsed-xml/C-13.xml.txt | 204 - dataset/parsed-xml/C-14.xml.txt | 175 - dataset/parsed-xml/C-15.xml.txt | 168 - dataset/parsed-xml/C-16.xml.txt | 97 - dataset/parsed-xml/C-17.xml.txt | 394 - dataset/parsed-xml/C-18.xml.txt | 271 - dataset/parsed-xml/C-19.xml.txt | 358 - dataset/parsed-xml/C-2.xml.txt | 175 - dataset/parsed-xml/C-3.xml.txt | 225 - dataset/parsed-xml/C-8.xml.txt | 139 - dataset/parsed-xml/C-9.xml.txt | 196 - dataset/parsed-xml/F-1.xml.txt | 301 - dataset/parsed-xml/F-10.xml.txt | 185 - dataset/parsed-xml/F-11.xml.txt | 218 - dataset/parsed-xml/F-12.xml.txt | 108 - dataset/parsed-xml/F-13.xml.txt | 156 - dataset/parsed-xml/F-14.xml.txt | 170 - dataset/parsed-xml/F-15.xml.txt | 141 - dataset/parsed-xml/F-16.xml.txt | 91 - dataset/parsed-xml/F-17.xml.txt | 320 - dataset/parsed-xml/F-18.xml.txt | 289 - dataset/parsed-xml/F-19.xml.txt | 240 - dataset/parsed-xml/F-2.xml.txt | 115 - dataset/parsed-xml/F-3.xml.txt | 212 - dataset/parsed-xml/F-4.xml.txt | 123 - dataset/parsed-xml/F-5.xml.txt | 141 - dataset/parsed-xml/F-6.xml.txt | 115 - dataset/parsed-xml/F-7.xml.txt | 176 - dataset/parsed-xml/F-8.xml.txt | 148 - dataset/parsed-xml/F-9.xml.txt | 193 - dataset/parsed-xml/G-1.xml.txt | 331 - dataset/parsed-xml/G-10.xml.txt | 203 - dataset/parsed-xml/G-11.xml.txt | 199 - dataset/parsed-xml/G-12.xml.txt | 113 - dataset/parsed-xml/G-13.xml.txt | 168 - dataset/parsed-xml/G-14.xml.txt | 204 - dataset/parsed-xml/G-15.xml.txt | 150 - dataset/parsed-xml/G-16.xml.txt | 74 - dataset/parsed-xml/G-17.xml.txt | 340 - dataset/parsed-xml/G-2.xml.txt | 126 - dataset/parsed-xml/G-3.xml.txt | 233 - dataset/parsed-xml/G-4.xml.txt | 141 - dataset/parsed-xml/G-7.xml.txt | 235 - dataset/parsed-xml/G-8.xml.txt | 198 - dataset/parsed-xml/G-9.xml.txt | 217 - dataset/parsed-xml/K-1.xml.txt | 304 - dataset/parsed-xml/K-10.xml.txt | 142 - dataset/parsed-xml/K-12.xml.txt | 72 - dataset/parsed-xml/K-13.xml.txt | 161 - dataset/parsed-xml/K-14.xml.txt | 125 - dataset/parsed-xml/K-15.xml.txt | 120 - dataset/parsed-xml/K-16.xml.txt | 85 - dataset/parsed-xml/K-17.xml.txt | 302 - dataset/parsed-xml/K-19.xml.txt | 264 - dataset/parsed-xml/K-2.xml.txt | 75 - dataset/parsed-xml/K-3.xml.txt | 195 - dataset/parsed-xml/K-4.xml.txt | 110 - dataset/parsed-xml/K-5.xml.txt | 117 - dataset/parsed-xml/K-6.xml.txt | 82 - dataset/parsed-xml/K-7.xml.txt | 142 - dataset/parsed-xml/K-8.xml.txt | 104 - dataset/parsed-xml/K-9.xml.txt | 127 - dataset/parsed-xml/M-1.xml.txt | 381 - dataset/parsed-xml/M-10.xml.txt | 197 - dataset/parsed-xml/M-11.xml.txt | 264 - dataset/parsed-xml/M-12.xml.txt | 126 - dataset/parsed-xml/M-13.xml.txt | 191 - dataset/parsed-xml/M-14.xml.txt | 180 - dataset/parsed-xml/M-15.xml.txt | 174 - dataset/parsed-xml/M-16.xml.txt | 115 - dataset/parsed-xml/M-17.xml.txt | 381 - dataset/parsed-xml/M-18.xml.txt | 326 - dataset/parsed-xml/M-19.xml.txt | 296 - dataset/parsed-xml/M-2.xml.txt | 114 - dataset/parsed-xml/M-3.xml.txt | 275 - dataset/parsed-xml/M-4.xml.txt | 174 - dataset/parsed-xml/M-5.xml.txt | 162 - dataset/parsed-xml/M-6.xml.txt | 92 - dataset/parsed-xml/M-7.xml.txt | 245 - dataset/parsed-xml/M-8.xml.txt | 110 - dataset/parsed-xml/M-9.xml.txt | 248 - evaluate_xml.bash | 15 +- 183 files changed, 53 insertions(+), 212826 deletions(-) delete mode 100644 dataset/converted/C-1.xml.conv delete mode 100644 dataset/converted/C-10.xml.conv delete mode 100644 dataset/converted/C-12.xml.conv delete mode 100644 dataset/converted/C-13.xml.conv delete mode 100644 dataset/converted/C-14.xml.conv delete mode 100644 dataset/converted/C-15.xml.conv delete mode 100644 dataset/converted/C-16.xml.conv delete mode 100644 dataset/converted/C-17.xml.conv delete mode 100644 dataset/converted/C-18.xml.conv delete mode 100644 dataset/converted/C-19.xml.conv delete mode 100644 dataset/converted/C-2.xml.conv delete mode 100644 dataset/converted/C-3.xml.conv delete mode 100644 dataset/converted/C-8.xml.conv delete mode 100644 dataset/converted/C-9.xml.conv delete mode 100644 dataset/converted/F-1.xml.conv delete mode 100644 dataset/converted/F-10.xml.conv delete mode 100644 dataset/converted/F-11.xml.conv delete mode 100644 dataset/converted/F-12.xml.conv delete mode 100644 dataset/converted/F-13.xml.conv delete mode 100644 dataset/converted/F-14.xml.conv delete mode 100644 dataset/converted/F-15.xml.conv delete mode 100644 dataset/converted/F-16.xml.conv delete mode 100644 dataset/converted/F-17.xml.conv delete mode 100644 dataset/converted/F-18.xml.conv delete mode 100644 dataset/converted/F-19.xml.conv delete mode 100644 dataset/converted/F-2.xml.conv delete mode 100644 dataset/converted/F-3.xml.conv delete mode 100644 dataset/converted/F-4.xml.conv delete mode 100644 dataset/converted/F-5.xml.conv delete mode 100644 dataset/converted/F-6.xml.conv delete mode 100644 dataset/converted/F-7.xml.conv delete mode 100644 dataset/converted/F-8.xml.conv delete mode 100644 dataset/converted/F-9.xml.conv delete mode 100644 dataset/converted/G-1.xml.conv delete mode 100644 dataset/converted/G-10.xml.conv delete mode 100644 dataset/converted/G-11.xml.conv delete mode 100644 dataset/converted/G-12.xml.conv delete mode 100644 dataset/converted/G-13.xml.conv delete mode 100644 dataset/converted/G-14.xml.conv delete mode 100644 dataset/converted/G-15.xml.conv delete mode 100644 dataset/converted/G-16.xml.conv delete mode 100644 dataset/converted/G-17.xml.conv delete mode 100644 dataset/converted/G-2.xml.conv delete mode 100644 dataset/converted/G-3.xml.conv delete mode 100644 dataset/converted/G-4.xml.conv delete mode 100644 dataset/converted/G-7.xml.conv delete mode 100644 dataset/converted/G-8.xml.conv delete mode 100644 dataset/converted/G-9.xml.conv delete mode 100644 dataset/converted/K-1.xml.conv delete mode 100644 dataset/converted/K-10.xml.conv delete mode 100644 dataset/converted/K-12.xml.conv delete mode 100644 dataset/converted/K-13.xml.conv delete mode 100644 dataset/converted/K-14.xml.conv delete mode 100644 dataset/converted/K-15.xml.conv delete mode 100644 dataset/converted/K-16.xml.conv delete mode 100644 dataset/converted/K-17.xml.conv delete mode 100644 dataset/converted/K-19.xml.conv delete mode 100644 dataset/converted/K-2.xml.conv delete mode 100644 dataset/converted/K-3.xml.conv delete mode 100644 dataset/converted/K-4.xml.conv delete mode 100644 dataset/converted/K-5.xml.conv delete mode 100644 dataset/converted/K-6.xml.conv delete mode 100644 dataset/converted/K-7.xml.conv delete mode 100644 dataset/converted/K-8.xml.conv delete mode 100644 dataset/converted/K-9.xml.conv delete mode 100644 dataset/converted/M-1.xml.conv delete mode 100644 dataset/converted/M-10.xml.conv delete mode 100644 dataset/converted/M-11.xml.conv delete mode 100644 dataset/converted/M-12.xml.conv delete mode 100644 dataset/converted/M-13.xml.conv delete mode 100644 dataset/converted/M-14.xml.conv delete mode 100644 dataset/converted/M-15.xml.conv delete mode 100644 dataset/converted/M-16.xml.conv delete mode 100644 dataset/converted/M-17.xml.conv delete mode 100644 dataset/converted/M-18.xml.conv delete mode 100644 dataset/converted/M-19.xml.conv delete mode 100644 dataset/converted/M-2.xml.conv delete mode 100644 dataset/converted/M-3.xml.conv delete mode 100644 dataset/converted/M-4.xml.conv delete mode 100644 dataset/converted/M-5.xml.conv delete mode 100644 dataset/converted/M-6.xml.conv delete mode 100644 dataset/converted/M-7.xml.conv delete mode 100644 dataset/converted/M-8.xml.conv delete mode 100644 dataset/converted/M-9.xml.conv delete mode 100644 dataset/evaluations/1.csv delete mode 100644 dataset/evaluations/2.csv delete mode 100644 dataset/evaluations/3.csv delete mode 100644 dataset/evaluations/4.csv delete mode 100644 dataset/evaluations/5.csv delete mode 100644 dataset/evaluations/key.txt delete mode 100644 dataset/evaluations/regress.csv delete mode 100644 dataset/outs/C.txt delete mode 100644 dataset/outs/F.txt delete mode 100644 dataset/outs/G.txt delete mode 100644 dataset/outs/K.txt delete mode 100644 dataset/outs/M.txt delete mode 100644 dataset/parsed-xml/C-1.xml.txt delete mode 100644 dataset/parsed-xml/C-10.xml.txt delete mode 100644 dataset/parsed-xml/C-12.xml.txt delete mode 100644 dataset/parsed-xml/C-13.xml.txt delete mode 100644 dataset/parsed-xml/C-14.xml.txt delete mode 100644 dataset/parsed-xml/C-15.xml.txt delete mode 100644 dataset/parsed-xml/C-16.xml.txt delete mode 100644 dataset/parsed-xml/C-17.xml.txt delete mode 100644 dataset/parsed-xml/C-18.xml.txt delete mode 100644 dataset/parsed-xml/C-19.xml.txt delete mode 100644 dataset/parsed-xml/C-2.xml.txt delete mode 100644 dataset/parsed-xml/C-3.xml.txt delete mode 100644 dataset/parsed-xml/C-8.xml.txt delete mode 100644 dataset/parsed-xml/C-9.xml.txt delete mode 100644 dataset/parsed-xml/F-1.xml.txt delete mode 100644 dataset/parsed-xml/F-10.xml.txt delete mode 100644 dataset/parsed-xml/F-11.xml.txt delete mode 100644 dataset/parsed-xml/F-12.xml.txt delete mode 100644 dataset/parsed-xml/F-13.xml.txt delete mode 100644 dataset/parsed-xml/F-14.xml.txt delete mode 100644 dataset/parsed-xml/F-15.xml.txt delete mode 100644 dataset/parsed-xml/F-16.xml.txt delete mode 100644 dataset/parsed-xml/F-17.xml.txt delete mode 100644 dataset/parsed-xml/F-18.xml.txt delete mode 100644 dataset/parsed-xml/F-19.xml.txt delete mode 100644 dataset/parsed-xml/F-2.xml.txt delete mode 100644 dataset/parsed-xml/F-3.xml.txt delete mode 100644 dataset/parsed-xml/F-4.xml.txt delete mode 100644 dataset/parsed-xml/F-5.xml.txt delete mode 100644 dataset/parsed-xml/F-6.xml.txt delete mode 100644 dataset/parsed-xml/F-7.xml.txt delete mode 100644 dataset/parsed-xml/F-8.xml.txt delete mode 100644 dataset/parsed-xml/F-9.xml.txt delete mode 100644 dataset/parsed-xml/G-1.xml.txt delete mode 100644 dataset/parsed-xml/G-10.xml.txt delete mode 100644 dataset/parsed-xml/G-11.xml.txt delete mode 100644 dataset/parsed-xml/G-12.xml.txt delete mode 100644 dataset/parsed-xml/G-13.xml.txt delete mode 100644 dataset/parsed-xml/G-14.xml.txt delete mode 100644 dataset/parsed-xml/G-15.xml.txt delete mode 100644 dataset/parsed-xml/G-16.xml.txt delete mode 100644 dataset/parsed-xml/G-17.xml.txt delete mode 100644 dataset/parsed-xml/G-2.xml.txt delete mode 100644 dataset/parsed-xml/G-3.xml.txt delete mode 100644 dataset/parsed-xml/G-4.xml.txt delete mode 100644 dataset/parsed-xml/G-7.xml.txt delete mode 100644 dataset/parsed-xml/G-8.xml.txt delete mode 100644 dataset/parsed-xml/G-9.xml.txt delete mode 100644 dataset/parsed-xml/K-1.xml.txt delete mode 100644 dataset/parsed-xml/K-10.xml.txt delete mode 100644 dataset/parsed-xml/K-12.xml.txt delete mode 100644 dataset/parsed-xml/K-13.xml.txt delete mode 100644 dataset/parsed-xml/K-14.xml.txt delete mode 100644 dataset/parsed-xml/K-15.xml.txt delete mode 100644 dataset/parsed-xml/K-16.xml.txt delete mode 100644 dataset/parsed-xml/K-17.xml.txt delete mode 100644 dataset/parsed-xml/K-19.xml.txt delete mode 100644 dataset/parsed-xml/K-2.xml.txt delete mode 100644 dataset/parsed-xml/K-3.xml.txt delete mode 100644 dataset/parsed-xml/K-4.xml.txt delete mode 100644 dataset/parsed-xml/K-5.xml.txt delete mode 100644 dataset/parsed-xml/K-6.xml.txt delete mode 100644 dataset/parsed-xml/K-7.xml.txt delete mode 100644 dataset/parsed-xml/K-8.xml.txt delete mode 100644 dataset/parsed-xml/K-9.xml.txt delete mode 100644 dataset/parsed-xml/M-1.xml.txt delete mode 100644 dataset/parsed-xml/M-10.xml.txt delete mode 100644 dataset/parsed-xml/M-11.xml.txt delete mode 100644 dataset/parsed-xml/M-12.xml.txt delete mode 100644 dataset/parsed-xml/M-13.xml.txt delete mode 100644 dataset/parsed-xml/M-14.xml.txt delete mode 100644 dataset/parsed-xml/M-15.xml.txt delete mode 100644 dataset/parsed-xml/M-16.xml.txt delete mode 100644 dataset/parsed-xml/M-17.xml.txt delete mode 100644 dataset/parsed-xml/M-18.xml.txt delete mode 100644 dataset/parsed-xml/M-19.xml.txt delete mode 100644 dataset/parsed-xml/M-2.xml.txt delete mode 100644 dataset/parsed-xml/M-3.xml.txt delete mode 100644 dataset/parsed-xml/M-4.xml.txt delete mode 100644 dataset/parsed-xml/M-5.xml.txt delete mode 100644 dataset/parsed-xml/M-6.xml.txt delete mode 100644 dataset/parsed-xml/M-7.xml.txt delete mode 100644 dataset/parsed-xml/M-8.xml.txt delete mode 100644 dataset/parsed-xml/M-9.xml.txt diff --git a/.gitignore b/.gitignore index 2e0657a..5fdef23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.classpath -.project +.vscode/** bin/** MusicXMLParser/MusicXMLToFmt1x diff --git a/README.md b/README.md index 30a3c0b..fbdf21a 100644 --- a/README.md +++ b/README.md @@ -38,16 +38,16 @@ There is now a bash script that will perform this evaluation in one command: `ev It automatically removes all of the intermediate files as well. If you would like to save them, you can remove those lines from the script, or perform the process manually with the following steps: -1. Convert MusicXML into a text-based format: -`./MusicXMLParser/MusicXMLToFmt1x gt.xml gt_xml.txt` +1. Convert MusicXML into a text-based format: +`./MusicXMLParser/MusicXMLToFmt1x gt.xml gt_xml.txt` (The C++ converter must be compiled first using `./compile.sh` in the `MusicXMLParser` directory.) -2. Convert that text-based format into the MV2H format: -`java -cp bin mv2h.tools.Converter -x gt_converted.txt` -Input and output files can also be specified with `-i FILE` and `-o FILE`. +2. Convert that text-based format into the MV2H format: +`java -cp bin mv2h.tools.Converter -x gt_converted.txt` +Input and output files can also be specified with `-i FILE` and `-o FILE`. Different parsed voices can be generated using `--part` (instrument/part), `--staff`, and/or `--voice`. Default uses all 3. -3. Evaluate with alignment using the `-a` flag: +3. Evaluate with alignment using the `-a` flag: `java -cp bin mv2h.Main -g gt_converted.txt -t trans_converted.txt -a` Chord symbols will not be parsed, and all key signatures will be major. @@ -55,13 +55,13 @@ Chord symbols will not be parsed, and all key signatures will be major. See [Dataset](#dataset) for examples. #### MIDI -1. Convert a MIDI file into the MV2H format: -`java -cp bin mv2h.tools.Converter -m -i gt.mid >gt_converted.txt` -`-a INT` can be used to set the anacrusis (pick-up bar) length to INT sub beats. -`-o FILE` can also be used to specify an output file (instead of standard output). +1. Convert a MIDI file into the MV2H format: +`java -cp bin mv2h.tools.Converter -m -i gt.mid >gt_converted.txt` +`-a INT` can be used to set the anacrusis (pick-up bar) length to INT sub beats. +`-o FILE` can also be used to specify an output file (instead of standard output). Different parsed voices can be generated using `--track` or `--channel`. Default uses both. -2. Evaluate with alignment using the `-a` flag: +2. Evaluate with alignment using the `-a` flag: `java -cp bin mv2h.Main -g gt_converted.txt -t trans_converted.txt -a` Chord symbols will not be parsed. @@ -75,65 +75,54 @@ To get the averages of many MV2H evaluations: ## Examples The examples directory contains two example transcriptions of an ground truth. To perform evaluation, run the following commands and you should get the results shown: - * `java -cp bin mv2h.Main -g examples/GroundTruth.txt -t examples/Transcription1.txt` -Multi-pitch: 0.9302325581395349 -Voice: 0.8125 -Meter: 0.7368421052631577 -Value: 0.9642857142857143 -Harmony: 1.0 -MV2H: 0.8887720755376813 - - * `java -cp bin mv2h.Main -g examples/GroundTruth.txt -t examples/Transcription2.txt` -Multi-pitch: 0.7727272727272727 -Voice: 1.0 -Meter: 1.0 -Value: 1.0 -Harmony: 0.5 -MV2H: 0.8545454545454545 - - * `java -cp bin mv2h.Main -F $1.conv.txt -rm $1.txt +musescore3 -o $1.mid $1 +musescore3 -o $2.mid $2 -./MusicXMLParser/MusicXMLToFmt1x $2 $2.txt -java -cp bin mv2h.tools.Converter -x <$2.txt >$2.conv.txt -rm $2.txt +java -cp bin mv2h.tools.Converter -i $1.mid -o $1.mid.txt +java -cp bin mv2h.tools.Converter -i $2.mid -o $2.mid.txt +rm $1.mid $2.mid -java -cp bin mv2h.Main -g $1.conv.txt -t $2.conv.txt -a -rm $1.conv.txt $2.conv.txt +java -cp bin mv2h.Main -g $1.mid.txt -t $2.mid.txt -a +rm $1.mid.txt $2.mid.txt From 139673b036d8e95f06e7e033743ff63fd564ec18 Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Sun, 20 Dec 2020 11:07:29 +0100 Subject: [PATCH 09/13] 2 time signature fixes: - Fix for small denominators returning 0 beats per quarter note. - Fix for musescore3 outputting dummy 1/1 TS. Now converted to 4/4. --- src/mv2h/tools/midi/TimeSignature.java | 52 +++++------ src/mv2h/tools/midi/TimeTracker.java | 121 ++++++++++++++----------- 2 files changed, 93 insertions(+), 80 deletions(-) diff --git a/src/mv2h/tools/midi/TimeSignature.java b/src/mv2h/tools/midi/TimeSignature.java index 9270d3d..f298e10 100644 --- a/src/mv2h/tools/midi/TimeSignature.java +++ b/src/mv2h/tools/midi/TimeSignature.java @@ -5,11 +5,11 @@ /** * A TimeSignature represents some MIDI data's beat structure (time signature). * Equality is based only on the numerator and denominator. - * + * * @author Andrew McLeod - 11 Feb, 2015 */ public class TimeSignature { - + /** * The numerator used to signify an irregular meter. It can be used with any denominator (4, for example). */ @@ -19,34 +19,34 @@ public class TimeSignature { * The numerator of the time signature. */ private final int numerator; - + /** * The denominator of the time signature. */ private final int denominator; - + /** * Create a new default TimeSignature (4/4 time) */ public TimeSignature() { this(new byte[] {4, 2, 24, 8}); } - + /** * Create a new TimeSignature from the given data array. - * + * * @param data Data array, parsed directly from midi. */ public TimeSignature(byte[] data) { numerator = data[0]; denominator = (int) Math.pow(2, data[1]); } - + /** * Create a new TimeSignature with the given numerator and denominator. * Using this, {@link #metronomeTicksPerBeat} will be 24, and {@link #notes32PerQuarter} * will be 8. - * + * * @param numerator {@link #numerator} * @param denominator {@link #denominator} */ @@ -54,49 +54,49 @@ public TimeSignature(int numerator, int denominator) { this.numerator = numerator; this.denominator = denominator; } - + /** * Get the numerator of this time signature. - * + * * @return {@link #numerator} */ public int getNumerator() { return numerator; } - + /** * Get the denominator of this time signature. - * + * * @return {@link #denominator} */ public int getDenominator() { return denominator; } - + /** * Get the number of sub beats per quarter note. - * + * * @return The number of sub beats per quarter note. */ - public int getSubBeatsPerQuarter() { + public double getSubBeatsPerQuarter() { // Simple meter if (numerator <= 4 || numerator % 3 != 0) { - return denominator / 2; - + return ((double) denominator) / 2.0; + // Compound meter } else { - return denominator / 4; + return ((double) denominator) / 4.0; } } - + @Override public boolean equals(Object other) { if (!(other instanceof TimeSignature)) { return false; } - + TimeSignature ts = (TimeSignature) other; - + return getDenominator() == ts.getDenominator() && getNumerator() == ts.getNumerator(); } @@ -104,21 +104,21 @@ public boolean equals(Object other) { * Get the Hierarchy of this time signature. * @param time The time of this hierarchy. * @param anacrusisLengthSubBeats The length of this Hierarchy's anacrusis, in sub beats. - * + * * @return The Hierarchy of this time signature. */ public Hierarchy getHierarchy(int time, int anacrusisLengthSubBeats) { int beatsPerMeasure = numerator; int subBeatsPerBeat = 2; - + // Check for compound if (numerator > 3 && numerator % 3 == 0) { beatsPerMeasure = numerator / 3; - subBeatsPerBeat = 3; + subBeatsPerBeat = 3; } - + Hierarchy hierarchy = new Hierarchy(beatsPerMeasure, subBeatsPerBeat, 1, anacrusisLengthSubBeats, time); - + return hierarchy; } } \ No newline at end of file diff --git a/src/mv2h/tools/midi/TimeTracker.java b/src/mv2h/tools/midi/TimeTracker.java index 3a0978a..b346370 100644 --- a/src/mv2h/tools/midi/TimeTracker.java +++ b/src/mv2h/tools/midi/TimeTracker.java @@ -17,7 +17,7 @@ * A TimeTracker is able to interpret MIDI tempo, key, and time signature change events and keep track * of the song timing in seconds, instead of just using ticks as MIDI events do. It does this by using * a LinkedList of {@link TimeTrackerNode} objects. - * + * * @author Andrew McLeod - 23 October, 2014 */ public class TimeTracker { @@ -25,32 +25,32 @@ public class TimeTracker { * Pulses (ticks) per Quarter note, as in the current Midi song's header. */ private double PPQ = 120.0; - + /** * The LInkedList of TimeTrackerNodes of this TimeTracker, ordered by time. */ private final LinkedList nodes; - + /** * The number of sub beats which lie before the first full measure in this song. */ private int anacrusisLengthSubBeats; - + /** * The last tick for any event in this song, initially 0. */ private long lastTick = 0; - + /** * Create a new TimeTracker. */ public TimeTracker() { this(-1); } - + /** * Create a new TimeTracker with the given sub beat length. - * + * * @param subBeatLength {@link #subBeatLength} */ public TimeTracker(int subBeatLength) { @@ -58,106 +58,119 @@ public TimeTracker(int subBeatLength) { nodes = new LinkedList(); nodes.add(new TimeTrackerNode()); } - + /** * A TimeSignature event was detected. Deal with it. - * + * * @param event The event. * @param mm The message from the event. */ public void addTimeSignatureChange(MidiEvent event, MetaMessage mm) { - TimeSignature ts = new TimeSignature(mm.getData()); - + TimeSignature ts = new TimeSignature(mm.getData()); + + /*************** + * SPECIAL CASE + * ------------ + * Musescore3 outputs a single 1/1 time signature at the beginning of a MIDI file if the + * score had no time signature. + * + * This assumes any 1/1 time signature at time 0 is from that, and defaults instead to 4/4, + * like the MIDI standard. + ***************/ + if (event.getTick() == 0 && ts.getNumerator() == 1 && ts.getDenominator() == 1) { + ts = new TimeSignature(4, 4); + } + if (nodes.getLast().getStartTick() > event.getTick()) { return; } - + if (nodes.getLast().getStartTick() == event.getTick()) { // If we're at the same time as a prior time change, combine this with that node. nodes.getLast().setTimeSignature(ts); - + } else if (!ts.equals(nodes.getLast().getTimeSignature())) { // Some change has been made nodes.add(new TimeTrackerNode(nodes.getLast(), event.getTick(), PPQ)); nodes.getLast().setTimeSignature(ts); } - + nodes.getLast().setIsTimeSignatureDummy(false); } - + /** * A Tempo event was detected. Deal with it. - * + * * @param event The event. * @param mm The message from the event. */ public void addTempoChange(MidiEvent event, MetaMessage mm) { Tempo t = new Tempo(mm.getData()); - + if (nodes.getLast().getStartTick() > event.getTick()) { return; } - + if (nodes.getLast().getStartTick() == event.getTick()) { // If we're at the same time as a prior time change, combine this with that node. nodes.getLast().setTempo(t); - + } else if (!t.equals(nodes.getLast().getTempo())) { nodes.add(new TimeTrackerNode(nodes.getLast(), event.getTick(), PPQ)); nodes.getLast().setTempo(t); } } - + /** * A Key event was detected. Deal with it. - * + * * @param event The event. * @param mm The message from the event. */ public void addKeyChange(MidiEvent event, MetaMessage mm) { int numSharps = mm.getData()[0]; boolean major = mm.getData()[1] == 0; - + int keyNumber = (7 * numSharps + 100 * 12) % 12; Key ks = new Key(keyNumber, major); - + if (nodes.getLast().getStartTick() > event.getTick()) { return; } - + if (nodes.getLast().getStartTick() == event.getTick()) { // If we're at the same time as a prior time change, combine this with that node. nodes.getLast().setKey(ks); - + } else if (!ks.equals(nodes.getLast().getKey())) { nodes.add(new TimeTrackerNode(nodes.getLast(), event.getTick(), PPQ)); nodes.getLast().setKey(ks); } } - + /** * Returns the time in milliseconds at a given tick. - * + * * @param tick The tick number to calculate the time of. * @return The time of the given tick number, measured in milliseconds since time 0. */ public double getTimeAtTick(long tick) { return getNodeAtTick(tick).getTimeAtTick(tick, PPQ); } - + /** * Get the TimeTrackerNode which is valid at the given tick. - * + * * @param tick The tick. * @return The valid TimeTrackerNode. */ private TimeTrackerNode getNodeAtTick(long tick) { ListIterator iterator = nodes.listIterator(); - + TimeTrackerNode node = iterator.next(); while (iterator.hasNext()) { node = iterator.next(); - + if (node.getStartTick() > tick) { iterator.previous(); return iterator.previous(); @@ -166,26 +179,26 @@ private TimeTrackerNode getNodeAtTick(long tick) { return node; } - + /** * Get a List of all of the key signatures of this TimeTracker. - * + * * @return A List of all of the key signatures of this TimeTracker. */ public List getAllKeySignatures() { List keys = new ArrayList(); - + for (TimeTrackerNode node : nodes) { Key key = node.getKey(); - + // First key if (keys.isEmpty()) { keys.add(key); - + // Duplicate time } else if (keys.get(keys.size() - 1).time == key.time) { keys.set(keys.size() - 1, key); - + // Check if keys are equal } else { Key oldKey = keys.get(keys.size() - 1); @@ -194,31 +207,31 @@ public List getAllKeySignatures() { } } } - + return keys; } - + /** * Get the Meter of this piece, according to this time tracker. - * + * * @return The meter of this piece. */ public Meter getMeter() { Meter meter = new Meter(); - + // Add Hierarchies Hierarchy current = null; for (TimeTrackerNode node : nodes) { current = node.getTimeSignature().getHierarchy((int) node.getStartTime(), node.getStartTime() == 0 ? anacrusisLengthSubBeats : 0); - + // First hierarchy if (meter.getHierarchies().isEmpty()) { meter.addHierarchy(current); - + // Same time -- overwrite } else if (meter.getHierarchies().get(meter.getHierarchies().size() - 1).time == current.time) { meter.addHierarchy(current); - + // New time -- add if changed } else { Hierarchy old = meter.getHierarchies().get(meter.getHierarchies().size() - 1); @@ -227,49 +240,49 @@ public Meter getMeter() { } } } - + // Add Tatums (sub-beats) double propFinished = 1.0; for (int i = 1; i < nodes.size(); i++) { TimeTrackerNode prevNode = nodes.get(i - 1); TimeTrackerNode nextNode = nodes.get(i); - + for (Tatum tatum : prevNode.getSubBeatsUntil(propFinished, nextNode.getStartTime())) { meter.addTatum(tatum); } propFinished = prevNode.getPropFinished(propFinished, nextNode.getStartTime()); } - + // Add last tatums TimeTrackerNode lastNode = nodes.get(nodes.size() - 1); for (Tatum tatum : lastNode.getSubBeatsUntil(propFinished, lastNode.getTimeAtTick(lastTick, PPQ))) { meter.addTatum(tatum); } - + return meter; } - + /** * Set the anacrusis length of this song to the given number of sub beats. - * + * * @param ticks The anacrusis length of this song, measured in sub beats. */ public void setAnacrusis(int length) { anacrusisLengthSubBeats = length; } - + /** * Set the last tick for this song to the given value. - * + * * @param lastTick {@link #lastTick} */ public void setLastTick(long lastTick) { this.lastTick = lastTick; } - + /** * Set the PPQ for this TimeTracker. - * + * * @param ppq {@link #PPQ} */ public void setPPQ(double ppq) { From 211c077f7f6e1c69864f66b27c08eada3737fa46 Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Sun, 20 Dec 2020 22:32:45 +0100 Subject: [PATCH 10/13] Updated readme --- README.md | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index fbdf21a..fc23a37 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ This is the code for the MV2H metric, originally proposed in my 2018 ISMIR paper - [v2.0](https://github.com/apmcleod/MV2H/releases/tag/v2.0) added support for non-time-aligned transcriptions (i.e. musical score to musical score evaluation), as detailed in the technical report on [arXiv](https://arxiv.org/abs/1906.00566). - [v2.1](https://github.com/apmcleod/MV2H/releases/tag/v2.1) added support for evaluating homophonic and polyphonic voice separation (see [Homophony.md](https://github.com/apmcleod/MV2H/blob/master/Homophony.md)). +- [v2.2](https://github.com/apmcleod/MV2H/releases/tag/v2.2) added the ability to tune the insertion/deletion penalty during DTW alignment, fixed a bug with time signatures with small denominators (e.g. 2 or 1), and sped up the DTW process significantly. If you use the metric, please cite it: @@ -33,36 +34,22 @@ To evaluate a time-aligned transcription and ground truth: ### Other File Formats #### MusicXML -There is now a bash script that will perform this evaluation in one command: `evaluate_xml.bash gt.xml transcription.xml` +There is now a bash script that will perform this evaluation in one command (if you have musescore3): `evaluate_xml.bash gt.xml transcription.xml` -It automatically removes all of the intermediate files as well. If you would like to save them, you can remove those lines -from the script, or perform the process manually with the following steps: - -1. Convert MusicXML into a text-based format: -`./MusicXMLParser/MusicXMLToFmt1x gt.xml gt_xml.txt` -(The C++ converter must be compiled first using `./compile.sh` in the `MusicXMLParser` directory.) - -2. Convert that text-based format into the MV2H format: -`java -cp bin mv2h.tools.Converter -x gt_converted.txt` -Input and output files can also be specified with `-i FILE` and `-o FILE`. -Different parsed voices can be generated using `--part` (instrument/part), `--staff`, and/or `--voice`. Default uses all 3. - -3. Evaluate with alignment using the `-a` flag: -`java -cp bin mv2h.Main -g gt_converted.txt -t trans_converted.txt -a` - -Chord symbols will not be parsed, and all key signatures will be major. - -See [Dataset](#dataset) for examples. +You can also perform the process manually by first converting the MusicXML files into MIDI, and then following the [instructions for MIDI files](#MIDI). + - The recommended way to convert MusicXML to MIDI is to use Musescore3: +`musescore3 -o file.mid file.xml` + - Other methods may also work, but not all will handle anacrusis (pick-up) measures correctly (`music21`, for example, did not when I tested it and will require manual setting during the MIDI conversion with `-a INT`). + - MusicXML files without a time signature are treated as 4/4. #### MIDI 1. Convert a MIDI file into the MV2H format: -`java -cp bin mv2h.tools.Converter -m -i gt.mid >gt_converted.txt` -`-a INT` can be used to set the anacrusis (pick-up bar) length to INT sub beats. -`-o FILE` can also be used to specify an output file (instead of standard output). -Different parsed voices can be generated using `--track` or `--channel`. Default uses both. +`java -cp bin mv2h.tools.Converter -m -i gt.mid -o gt_converted.txt` +`-a INT` can be used to set the anacrusis (pick-up bar) length to INT sub beats, in case it is not aligned correctly in the MIDI. +Different parsed voices can be generated using `--channel` or `--track`. Default uses both. 2. Evaluate with alignment using the `-a` flag: -`java -cp bin mv2h.Main -g gt_converted.txt -t trans_converted.txt -a` +`java -cp bin mv2h.Main -g gt_converted.txt -t transcription_converted.txt -a` Chord symbols will not be parsed. From 5bba2563567a4dbf7fc951d81a370e76869ce5de Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Sun, 20 Dec 2020 22:37:03 +0100 Subject: [PATCH 11/13] Update README.md --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index fc23a37..d1143e4 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,12 @@ You can also perform the process manually by first converting the MusicXML files - MusicXML files without a time signature are treated as 4/4. #### MIDI -1. Convert a MIDI file into the MV2H format: -`java -cp bin mv2h.tools.Converter -m -i gt.mid -o gt_converted.txt` -`-a INT` can be used to set the anacrusis (pick-up bar) length to INT sub beats, in case it is not aligned correctly in the MIDI. +1. Convert a MIDI file into the MV2H format: +`java -cp bin mv2h.tools.Converter -m -i gt.mid -o gt_converted.txt` +`-a INT` can be used to set the anacrusis (pick-up bar) length to INT sub beats, in case it is not aligned correctly in the MIDI. Different parsed voices can be generated using `--channel` or `--track`. Default uses both. -2. Evaluate with alignment using the `-a` flag: +2. Evaluate with alignment using the `-a` flag: `java -cp bin mv2h.Main -g gt_converted.txt -t transcription_converted.txt -a` Chord symbols will not be parsed. @@ -62,49 +62,49 @@ To get the averages of many MV2H evaluations: ## Examples The examples directory contains two example transcriptions of an ground truth. To perform evaluation, run the following commands and you should get the results shown: - * `java -cp bin mv2h.Main -g examples/GroundTruth.txt -t examples/Transcription1.txt` -Multi-pitch: 0.9302325581395349 -Voice: 0.8125 -Meter: 0.7368421052631577 -Value: 0.9642857142857143 -Harmony: 1.0 -MV2H: 0.8887720755376813 - - * `java -cp bin mv2h.Main -g examples/GroundTruth.txt -t examples/Transcription2.txt` -Multi-pitch: 0.7727272727272727 -Voice: 1.0 -Meter: 1.0 -Value: 1.0 -Harmony: 0.5 -MV2H: 0.8545454545454545 - - * `java -cp bin mv2h.Main -F Date: Sun, 20 Dec 2020 23:27:51 +0100 Subject: [PATCH 12/13] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d1143e4..e2ccac3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,12 @@ To compile the code, simply run `make` in the base directory. ### Non-aligned Data Use the `-a` flag to evaluate a non-time-aligned transcription: -* `java -cp bin mv2h.Main -g gt.txt -t transcription.txt -a` +* `java -cp bin mv2h.Main -g gt.txt -t transcription.txt [-a|-A] [-p DOUBLE]` + +* `-a` or `-A`: Perform normal (`-a`) or verbose (`-A`, will also print out note-by-note alignment details) alignment. + +* `-p DOUBLE` (new in v2.2): Set the DTW insertion and deletion penalties to the given `DOUBLE` (default 1.0). +Higher values will force the DTW alignment to align as many notes as possible, even if they don't match well. Lower values will make it only rarely align notes that don't match exactly. ### Aligned Data To evaluate a time-aligned transcription and ground truth: From c684e25f86f273663b7f278518c527929862bb2a Mon Sep 17 00:00:00 2001 From: Andrew McLeod Date: Sun, 20 Dec 2020 23:38:20 +0100 Subject: [PATCH 13/13] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e2ccac3..55f4eb1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,11 @@ Use the `-a` flag to evaluate a non-time-aligned transcription: * `-a` or `-A`: Perform normal (`-a`) or verbose (`-A`, will also print out note-by-note alignment details) alignment. * `-p DOUBLE` (new in v2.2): Set the DTW insertion and deletion penalties to the given `DOUBLE` (default 1.0). -Higher values will force the DTW alignment to align as many notes as possible, even if they don't match well. Lower values will make it only rarely align notes that don't match exactly. +Higher values will be faster, but force the DTW alignment to align as many notes as possible, even if they don't match well. +Lower values will be slower, but make it only rarely align notes that don't match exactly. +The default of 1.0 should be good in most cases. Values of 0.5 and 1.0 are sort of inflection points +(you will find large changes in the number of potential alignments around these values). +_NOTE: You should use the same value throughout your whole evaluation for a fair comparison._ ### Aligned Data To evaluate a time-aligned transcription and ground truth: