Source code for music21.alpha.analysis.fixer

# ------------------------------------------------------------------------------
# Name:         alpha/analysis/fixer.py
# Purpose:      Fixes two streams given a list of changes between them
#
# Authors:      Emily Zhang
#
# Copyright:    Copyright © 2016 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations

from copy import deepcopy
import unittest

from music21 import duration
from music21 import expressions
from music21 import interval
from music21 import note
from music21 import pitch
from music21 import stream

from music21.alpha.analysis import aligner
from music21.alpha.analysis import ornamentRecognizer

[docs] class OMRMidiFixer: ''' Base class for future fixers changes is a list of changes associated with the midiStream and omrStream, not a list of lists ''' def __init__(self, changes, midiStream, omrStream): self.changes = changes self.midiStream = midiStream self.omrStream = omrStream
[docs] def fix(self): pass
[docs] def checkIfNoteInstance(self, midiRef, omrRef): if isinstance(midiRef, note.Note) and isinstance(omrRef, note.Note): return True return False
[docs] class DeleteFixer(OMRMidiFixer): ''' The DeleteFixer was designed to fit the specifications of the OpenScore project. The goal of the OpenScore project is to open-source music with open source software (like music21!). OpenScore will use a combination of computer and human power to digitize classical music scores and put them in the public domain. One idea is that software can identify wrong recognized notes in a scanned score and mark or delete the entire measure that that note is in and pass it off to a human corrector to re-transcribe the entire measure. The DeleteFixer could be the computer power in this method of score correction that OpenScore is using. CAUTION: this does really weird things still. '''
[docs] def fix(self): super().fix() for (midiRef, omrRef, op) in self.changes: if self.checkIfNoteInstance(midiRef, omrRef) is False: continue # if they are the same, don't bother to try changing it # 3 is the number of noChange Ops if isinstance(op, aligner.ChangeOps) and op == aligner.ChangeOps.NoChange: continue m = omrRef.getContextByClass(stream.Measure) self.omrStream.remove(m)
[docs] class EnharmonicFixer(OMRMidiFixer): ''' Fixes incorrectly spelled enharmonics initialized with self.changes -- a list of tuples in this form: (MIDIReference, OMRReference, op) MIDIReference and OMRReference are actual note/rest/chord object in some stream op is a ChangeOp that relates the two references TEST 1, no changes in OMR stream >>> omrStream1 = stream.Stream() >>> midiStream1 = stream.Stream() >>> omrNote1 = note.Note('A#4') >>> omrNote2 = note.Note('A#4') >>> midiNote1 = note.Note('B-4') >>> midiNote2 = note.Note('B-4') >>> omrStream1.append([omrNote1, omrNote2]) >>> midiStream1.append([midiNote1, midiNote2]) >>> subOp = alpha.analysis.aligner.ChangeOps.Substitution >>> ct1 = (midiNote1, omrNote1, subOp) >>> ct2 = (midiNote2, omrNote2, subOp) >>> changes1 = [ct1, ct2] >>> fixer1 = alpha.analysis.fixer.EnharmonicFixer(changes1, None, None) >>> fixer1.fix() >>> omrStream1[0] <music21.note.Note A#> >>> omrStream1[1] <music21.note.Note A#> TEST 2, no changes in OMR stream >>> omrStream2 = stream.Stream() >>> midiStream2 = stream.Stream() >>> omr2Note1 = note.Note('A#4') >>> omr2Note2 = note.Note('A#4') >>> midi2Note1 = note.Note('A#4') >>> midi2Note2 = note.Note('A#4') >>> omrStream2.append([omr2Note1, omr2Note2]) >>> midiStream2.append([midi2Note1, midi2Note2]) >>> ncOp = alpha.analysis.aligner.ChangeOps.NoChange >>> ct2_1 = (midi2Note1, omr2Note1, ncOp) >>> ct2_2 = (midi2Note2, omr2Note2, ncOp) >>> changes2 = [ct2_1, ct2_2] >>> fixer2 = alpha.analysis.fixer.EnharmonicFixer(changes2, None, None) >>> fixer2.fix() >>> omrStream2[0] <music21.note.Note A#> >>> omrStream1[1] <music21.note.Note A#> TEST 3 (case 1) >>> midiNote3 = note.Note('A4') >>> omrNote3 = note.Note('An4') >>> subOp = alpha.analysis.aligner.ChangeOps.Substitution >>> ct3 = (midiNote3, omrNote3, subOp) >>> changes3 = [ct3] >>> omrNote3.pitch.accidental <music21.pitch.Accidental natural> >>> fixer3 = alpha.analysis.fixer.EnharmonicFixer(changes3, None, None) >>> fixer3.fix() >>> omrNote3.pitch.accidental TEST 4 (case 2-1) e.g MIDI = g#, ground truth = a-, OMR = an >>> midiNote4 = note.Note('G#4') >>> omrNote4 = note.Note('An4') >>> subOp = alpha.analysis.aligner.ChangeOps.Substitution >>> ct4 = (midiNote4, omrNote4, subOp) >>> changes4 = [ct4] >>> omrNote4.pitch.accidental <music21.pitch.Accidental natural> >>> fixer4 = alpha.analysis.fixer.EnharmonicFixer(changes4, None, None) >>> fixer4.fix() >>> omrNote4.pitch.accidental <music21.pitch.Accidental flat> TEST 5 (case 2-2) e.g midi = g-, gt = f#, omr = fn >>> midiNote5 = note.Note('G-4') >>> omrNote5 = note.Note('Fn4') >>> subOp = alpha.analysis.aligner.ChangeOps.Substitution >>> ct5 = (midiNote5, omrNote5, subOp) >>> changes5 = [ct5] >>> omrNote5.pitch.accidental <music21.pitch.Accidental natural> >>> fixer5 = alpha.analysis.fixer.EnharmonicFixer(changes5, None, None) >>> fixer5.fix() >>> omrNote5.pitch.accidental <music21.pitch.Accidental sharp> TEST 6.1 (case 3) e.g. midi = g#, gt = g#, omr = gn or omr = g- >>> midiNote6_1 = note.Note('G#4') >>> midiNote6_2 = note.Note('G#4') >>> omrNote6_1 = note.Note('Gn4') >>> omrNote6_2 = note.Note('G-4') >>> subOp6_1 = alpha.analysis.aligner.ChangeOps.Substitution >>> subOp6_2 = alpha.analysis.aligner.ChangeOps.Substitution >>> ct6_1 = (midiNote6_1, omrNote6_1, subOp6_1) >>> ct6_2 = (midiNote6_2, omrNote6_2, subOp6_2) >>> changes6 = [ct6_1, ct6_2] >>> omrNote6_1.pitch.accidental <music21.pitch.Accidental natural> >>> omrNote6_2.pitch.accidental <music21.pitch.Accidental flat> >>> fixer6 = alpha.analysis.fixer.EnharmonicFixer(changes6, None, None) >>> fixer6.fix() >>> omrNote6_1.pitch.accidental <music21.pitch.Accidental sharp> >>> omrNote6_2.pitch.accidental <music21.pitch.Accidental sharp> TEST 7 (case 4-1, 4-2) notes are on different step, off by an interval of 2: * 4-1: e.g. midi = g#, gt = a-, omr = a# * 4-2: e.g. midi = a-, gt = g#, omr = g- >>> midiNote7_1 = note.Note('G#4') >>> omrNote7_1 = note.Note('A#4') >>> midiNote7_2 = note.Note('A-4') >>> omrNote7_2 = note.Note('G-4') >>> subOp7_1 = alpha.analysis.aligner.ChangeOps.Substitution >>> subOp7_2 = alpha.analysis.aligner.ChangeOps.Substitution >>> ct7_1 = (midiNote7_1, omrNote7_1, subOp7_1) >>> ct7_2 = (midiNote7_2, omrNote7_2, subOp7_2) >>> changes7 = [ct7_1, ct7_2] >>> omrNote7_1.pitch.accidental <music21.pitch.Accidental sharp> >>> omrNote7_2.pitch.accidental <music21.pitch.Accidental flat> >>> fixer7 = alpha.analysis.fixer.EnharmonicFixer(changes7, None, None) >>> fixer7.fix() >>> omrNote7_1.pitch.step 'A' >>> omrNote7_1.pitch.accidental <music21.pitch.Accidental flat> >>> omrNote7_2.pitch.step 'G' >>> omrNote7_2.pitch.accidental <music21.pitch.Accidental sharp> '''
[docs] def fix(self): ''' Fixes the enharmonic errors in the OMR by changing the pitch of the note. ''' super().fix() for (midiRef, omrRef, op) in self.changes: omrRef.style.color = 'black' # if they're not notes, don't bother with rest if self.checkIfNoteInstance(midiRef, omrRef) is False: continue # if they are the same, don't bother to try changing it # 3 is the number of noChange Ops if isinstance(op, aligner.ChangeOps) and op == aligner.ChangeOps.NoChange: continue # don't bother with notes with too big of an interval between them if self.intervalTooBig(midiRef, omrRef, setInt=5): continue # case 1: omr has extraneous natural sign in front of it, get rid of it if self.hasNatAcc(omrRef): if self.isEnharmonic(midiRef, omrRef): omrRef.pitch.accidental = None else: # case 2-1: midi note is sharp, omr note is one step higher and natural, # should be a flat instead. e.g midi = g#, gt = a-, omr = an # omr note has higher ps than midi-- on a higher # line or space than midi note if omrRef.pitch > midiRef.pitch: if omrRef.pitch.transpose(interval.Interval(-1) ).isEnharmonic(midiRef.pitch): omrRef.pitch.accidental = pitch.Accidental('flat') # case 2-2: midi note is flat, omr note is one step lower and natural, # should be a flat instead. e.g midi = g-, gt = f#, omr = fn # omr note has lower ps than midi-- on a higher line # or space than midi note elif omrRef.pitch < midiRef.pitch: if omrRef.pitch.transpose(interval.Interval(1) ).isEnharmonic(midiRef.pitch): omrRef.pitch.accidental = pitch.Accidental('sharp') # case 3: notes are on same step, but omr got read wrong. # e.g. midi = g#, gt = g#, omr = gn or omr = g- elif self.hasSharpFlatAcc(omrRef) and self.stepEq(midiRef, omrRef): if self.hasAcc(omrRef): omrRef.pitch.accidental = midiRef.pitch.accidental else: omrRef.pitch.accidental = None elif self.hasSharpFlatAcc(omrRef) and self.stepNotEq(midiRef, omrRef): # case 4-1: notes are on different step, off by an interval of 2, # omr note is higher and sharp # e.g. midi = g#, gt = a-, omr = a# if omrRef.pitch > midiRef.pitch: if omrRef.pitch.accidental == pitch.Accidental('sharp'): if omrRef.pitch.transpose(interval.Interval(-2) ).isEnharmonic(midiRef.pitch): omrRef.pitch.accidental = pitch.Accidental('flat') # case 4-2: notes are on different step, off by an interval of 2, # omr note is lower and flat # e.g. midi = a-, gt = g#, omr = g- elif omrRef.pitch < midiRef.pitch: if omrRef.pitch.accidental == pitch.Accidental('flat'): if omrRef.pitch.transpose(interval.Interval(2) ).isEnharmonic(midiRef.pitch): omrRef.pitch.accidental = pitch.Accidental('sharp') # case 5: same step, MIDI has accidental, # omr was read wrong (e.g. key signature not parsed) # e.g. midi = b-, gt = b-, omr= elif (omrRef.pitch != midiRef.pitch and self.hasSharpFlatAcc(midiRef) and self.stepEq(midiRef, omrRef)): omrRef.pitch = midiRef.pitch
[docs] def isEnharmonic(self, midiRef, omrRef): ''' Returns True if the omrRef is enharmonic to the midiRef, False otherwise ''' return midiRef.pitch.isEnharmonic(omrRef.pitch)
[docs] def hasAcc(self, omrRef): ''' Returns True if the omrRef has an accidental, False otherwise ''' return omrRef.pitch.accidental is not None
[docs] def hasNatAcc(self, omrRef): ''' Returns True if the omrRef has a natural accidental, False otherwise ''' return self.hasAcc(omrRef) and omrRef.pitch.accidental.name == 'natural'
[docs] def hasSharpFlatAcc(self, omrRef): ''' Returns True if the omrRef has a sharp or flat accidental, False otherwise ''' return self.hasAcc(omrRef) and omrRef.pitch.accidental.name != 'natural'
[docs] def stepEq(self, midiRef, omrRef): ''' Returns True if the steps are equal, False otherwise ''' return midiRef.step == omrRef.step
[docs] def stepNotEq(self, midiRef, omrRef): ''' Returns True if the steps are not equal, False otherwise ''' return midiRef.step != omrRef.step
[docs] def intervalTooBig(self, midiRef, omrRef, setInt=5): ''' Returns True if the intervalClass between the two notes is greater than setInt. Note that intervalClass and not actual interval size is used. ''' if interval.notesToChromatic(midiRef, omrRef).intervalClass > setInt: return True return False
[docs] class OrnamentFixer(OMRMidiFixer): ''' Fixes missed ornaments in OMR using expanded ornaments in MIDI initialized with self.changes -- a list of tuples in this form: (MIDIReference, OMRReference, op) MIDIReference and OMRReference are actual note/rest/chord object in some stream op is a ChangeOp that relates the two references recognizers can be a single recognizer or list of recognizers, but self.recognizers is always a list. recognizers take in stream of busy notes and optional stream of simple note(s) and return False or an instance of the ornament recognized ''' def __init__(self, changes, midiStream, omrStream, recognizers, markChangeColor='blue'): super().__init__(changes, midiStream, omrStream) if not isinstance(recognizers, list): self.recognizers = [recognizers] else: self.recognizers = recognizers self.markChangeColor = markChangeColor
[docs] def findOrnament(self, busyNotes, simpleNotes) -> expressions.Ornament|None: ''' Finds an ornament in busyNotes based from simpleNote using provided recognizers. :param busyNotes: stream of notes :param simpleNotes: stream of notes Tries the recognizers in order, returning the result of the first one to successfully recognize an ornament. ''' for r in self.recognizers: ornament = r.recognize(busyNotes, simpleNotes=simpleNotes) if ornament: return ornament return None
[docs] def addOrnament(self, selectedNote: note.Note, ornament: expressions.Ornament, *, show=False) -> bool: ''' Adds the ornament to the selectedNote when selectedNote has no ornaments already. * selectedNote: Note.note to add ornament to * ornament: Expressions.ornament to add to note * show: True when note should be colored blue Returns True if added successfully, or False if there was already an ornament on the note and it wasn't added. ''' if not any(isinstance(e, expressions.Ornament) for e in selectedNote.expressions): selectedNote.expressions.append(ornament) if show: selectedNote.style.color = self.markChangeColor return True return False
[docs] def fix(self, *, show=False, inPlace=True) -> OMRMidiFixer|None: ''' Corrects missed ornaments in omrStream according to midiStream :param show: Whether to show results :param inPlace: Whether to make changes to own omr stream or return a new OrnamentFixer with changes ''' changes = self.changes sa: aligner.StreamAligner|None = None omrNotesLabeledOrnament = [] midiNotesAlreadyFixedForOrnament = [] if not inPlace: omrStreamCopy = deepcopy(self.omrStream) midiStreamCopy = deepcopy(self.midiStream) sa = aligner.StreamAligner(sourceStream=omrStreamCopy, targetStream=midiStreamCopy) sa.align() changes = sa.changes for midiNoteRef, omrNoteRef, change in changes: # reasonable changes if change is aligner.ChangeOps.NoChange or change is aligner.ChangeOps.Deletion: continue # get relevant notes if omrNoteRef in omrNotesLabeledOrnament: continue busyNotes = getNotesWithinDuration(midiNoteRef, omrNoteRef.duration) busyNoteAlreadyUsed = False for busyNote in busyNotes: if busyNote in midiNotesAlreadyFixedForOrnament: busyNoteAlreadyUsed = True break if busyNoteAlreadyUsed: continue # try to recognize ornament ornamentFound = self.findOrnament(busyNotes, [deepcopy(omrNoteRef)]) # mark ornament if ornamentFound: midiNotesAlreadyFixedForOrnament += busyNotes omrNotesLabeledOrnament.append(omrNoteRef) self.addOrnament(omrNoteRef, ornamentFound, show=show) if show: self.omrStream.show() self.midiStream.show() if not inPlace and sa is not None: # the "and sa is not None" is just for mypy/typing. return TrillFixer(sa.changes, sa.targetStream, sa.sourceStream) else: return None
[docs] def getNotesWithinDuration(startingGeneralNote, totalDuration, referenceStream=None): ''' Returns maximal stream of deepcopies of notes, rests, and chords following (and including) startingNote which occupy no more than totalDuration combined. * startingGeneralNote is a GeneralNote (could be a note, rest, or chord) * totalDuration is a duration * referenceStream is optionally a stream which the startingGeneralNote's active site should be set to when provided ''' if referenceStream: startingGeneralNote.activeSite = referenceStream notes = stream.Stream() # even startingNote is too long if startingGeneralNote.duration.quarterLength > totalDuration.quarterLength: return notes durationQlLeft = totalDuration.quarterLength - startingGeneralNote.duration.quarterLength nextGeneralNote = startingGeneralNote.next('GeneralNote', activeSiteOnly=True) notes.append(deepcopy(startingGeneralNote)) while nextGeneralNote and durationQlLeft >= nextGeneralNote.duration.quarterLength: currentGeneralNote = nextGeneralNote nextGeneralNote = currentGeneralNote.next('GeneralNote', activeSiteOnly=True) durationQlLeft -= currentGeneralNote.duration.quarterLength notes.append(deepcopy(currentGeneralNote)) return notes
[docs] class TrillFixer(OrnamentFixer): ''' Fixes missed trills in OMR using expanded ornaments in MIDI. initialized with self.changes -- a list of tuples in this form: (MIDIReference, OMRReference, op) MIDIReference and OMRReference are actual note/rest/chord object in some stream op is a ChangeOp that relates the two references ''' def __init__(self, changes, midiStream, omrStream): defaultOrnamentRecognizer = ornamentRecognizer.TrillRecognizer() nachschlagOrnamentRecognizer = ornamentRecognizer.TrillRecognizer() nachschlagOrnamentRecognizer.checkNachschlag = True recognizers = [defaultOrnamentRecognizer, nachschlagOrnamentRecognizer] super().__init__(changes, midiStream, omrStream, recognizers)
[docs] class TurnFixer(OrnamentFixer): ''' Fixes missed turns/inverted turns in OMR using expanded ornaments in MIDI. initialized with self.changes -- a list of tuples in this form: (MIDIReference, OMRReference, op) MIDIReference and OMRReference are actual note/rest/chord object in some stream op is a ChangeOp that relates the two references ''' def __init__(self, changes, midiStream, omrStream): recognizer = ornamentRecognizer.TurnRecognizer() super().__init__(changes, midiStream, omrStream, recognizer)
class Test(unittest.TestCase): def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) def measuresEqual(self, m1, m2): ''' Returns a tuple of (bool, reason) where the first element is whether the measures are equal and the second (`reason`) is a string explaining why they are unequal. Reason is an empty string if the measures are equal. ''' if len(m1) != len(m2): msg = 'not equal length' return False, msg for i in range(len(m1)): if len(m1[i].expressions) != len(m2[i].expressions): msg = f'Expressions {i} unequal ({m1[i].expressions} != {m2[i].expressions})' return False, msg if m1[i] != m2[i]: msg = f'Elements {i} are unequal ({m1[i]} != {m2[i]})' return False, msg return True, '' def checkFixerHelper(self, testCase, testFixer): ''' testCase is a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': fixed measure stream, } testFixer is an OMRMidiFixer ''' omr = testCase['omr'] midi = testCase['midi'] expectedOmr = testCase['expected'] testingName = testCase['name'] # set up aligner sa = aligner.StreamAligner(sourceStream=omr, targetStream=midi) sa.align() omrCopy = deepcopy(omr) assertionCheck = 'Expect no changes from creating and aligning aligner.' self.assertTrue(self.measuresEqual(omrCopy, sa.sourceStream)[0], assertionCheck) # set up fixer fixer = testFixer(sa.changes, sa.targetStream, sa.sourceStream) assertionCheck = 'Expect no changes from creating fixer.' self.assertTrue(self.measuresEqual(omrCopy, sa.sourceStream)[0], assertionCheck) # test fixing not in place notInPlaceResult = fixer.fix(inPlace=False) assertionCheck = '. Expect no changes to aligner source stream, but unequal because ' isEqual, reason = self.measuresEqual(omrCopy, sa.sourceStream) self.assertTrue(isEqual, testingName + assertionCheck + reason) assertionCheck = '. Expect no changes to fixer omr stream, but unequal because ' isEqual, reason = self.measuresEqual(omrCopy, fixer.omrStream) self.assertTrue(isEqual, testingName + assertionCheck + reason) assertionCheck = '. Appropriate changes in new fixer, but unequal because ' isEqual, reason = self.measuresEqual(notInPlaceResult.omrStream, expectedOmr) self.assertTrue(isEqual, testingName + assertionCheck + reason) # test fixing in place fixerInPlaceResult = fixer.fix(inPlace=True) self.assertIsNone(fixerInPlaceResult, testingName) assertionCheck = ". Expect changes in fixer's omr stream, but unequal because " isEqual, reason = self.measuresEqual(expectedOmr, fixer.omrStream) self.assertTrue(isEqual, testingName + assertionCheck + reason) assertionCheck = '. Expect changes in original omr stream, but unequal because ' isEqual, reason = self.measuresEqual(expectedOmr, omr) self.assertTrue(isEqual, testingName + assertionCheck + reason) def testGetNotesWithinDuration(self): n1 = note.Note('C') n1.duration = duration.Duration('quarter') m1 = stream.Stream() m1.append(n1) result = getNotesWithinDuration(n1, duration.Duration('quarter')) self.assertIsInstance(result, stream.Stream) self.assertListEqual([n1], list(result.notes), 'starting note occupies full duration') result = getNotesWithinDuration(n1, duration.Duration('half')) self.assertListEqual([n1], list(result.notes), 'starting note occupies partial duration') result = getNotesWithinDuration(n1, duration.Duration('eighth')) self.assertListEqual([], list(result.notes), 'starting note too long') m2 = stream.Measure() n2 = note.Note('D') n2.duration = duration.Duration('eighth') n3 = note.Note('E') n3.duration = duration.Duration('eighth') m2.append([n1, n2, n3]) result = getNotesWithinDuration(n1, duration.Duration('quarter')) self.assertListEqual([n1], list(result.notes), 'starting note occupies full duration') result = getNotesWithinDuration(n1, duration.Duration('half')) self.assertListEqual([n1, n2, n3], list(result.notes), 'all notes fill up full duration') result = getNotesWithinDuration(n1, duration.Duration('whole')) self.assertListEqual([n1, n2, n3], list(result.notes), 'all notes fill up partial duration') result = getNotesWithinDuration(n1, duration.Duration(1.5)) self.assertListEqual([n1, n2], list(result.notes), 'some notes fill up full duration') result = getNotesWithinDuration(n1, duration.Duration(1.75)) self.assertListEqual([n1, n2], list(result.notes), 'some notes fill up partial duration') # set active site from m2 to m1 (which runs out of notes to fill up) result = getNotesWithinDuration(n1, duration.Duration('half'), referenceStream=m1) self.assertListEqual([n1], list(result.notes), 'partial fill up from reference stream m1') m3 = stream.Measure() m3.id = 'm3' r1 = note.Rest() r1.duration = duration.Duration('quarter') m3.append([n1, r1]) # n1 active site now with m2 result = getNotesWithinDuration(n1, duration.Duration('half')) msg = 'note and rest fill up full duration' self.assertListEqual([n1, r1], list(result.notesAndRests), msg) # set active site from m3 to m2 result = getNotesWithinDuration(n1, duration.Duration('half'), referenceStream=m2) self.assertListEqual([n1, n2, n3], list(result.notes), 'fill up from reference stream m2') def testTrillFixer(self): def createDoubleTrillMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' noteDuration = duration.Duration('quarter') # GAGA Trill trill1NoteDuration = duration.Duration(.25) n0 = note.Note('G') n0.duration = noteDuration n1 = note.Note('G') n1.duration = trill1NoteDuration n2 = note.Note('A') n2.duration = trill1NoteDuration trill1 = [n1, n2, deepcopy(n1), deepcopy(n2)] # G A G A # C B C B Trill trill2NoteDuration = duration.Duration(.0625) n3 = note.Note('B3') # omr n3.duration = noteDuration n4 = note.Note('B3') n4.duration = trill2NoteDuration n5 = note.Note('C') n5.duration = trill2NoteDuration trill2 = [n5, n4, deepcopy(n5), deepcopy(n4), deepcopy(n5), deepcopy(n4), deepcopy(n5), deepcopy(n4)] midiMeasure = stream.Measure() midiMeasure.append(trill1) midiMeasure.append(trill2) omrMeasure = stream.Measure() omrMeasure.append([n0, n3]) expectedFixedOmrMeasure = stream.Measure() n0WithTrill = deepcopy(n0) n0Trill = expressions.Trill() n0Trill.size = interval.Interval('m-2') n0Trill.quarterLength = trill1NoteDuration.quarterLength n0WithTrill.expressions.append(n0Trill) n1WithTrill = deepcopy(n3) n1Trill = expressions.Trill() n1Trill.size = interval.Interval('M2') n1Trill.quarterLength = trill2NoteDuration.quarterLength n1WithTrill.expressions.append(n0Trill) expectedFixedOmrMeasure.append([n0WithTrill, n1WithTrill]) returnDict = { 'name': 'Double Trill Measure', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': expectedFixedOmrMeasure, } return returnDict def createWrongTrillMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' noteDuration = duration.Duration('quarter') n0 = note.Note('C') # omr n0.duration = noteDuration n1 = note.Note('C') n1.duration = duration.Duration(.25) n2 = note.Note('A') n2.duration = duration.Duration(.25) nonTrill = [n1, n2, deepcopy(n1), deepcopy(n2)] midiMeasure = stream.Measure() midiMeasure.append(nonTrill) omrMeasure = stream.Measure() omrMeasure.append(n0) returnDict = { 'name': 'Non-Trill Measure Wrong Oscillate Interval', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': deepcopy(omrMeasure), } return returnDict def createNonTrillMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' noteDuration = duration.Duration('quarter') n0 = note.Note('A') # omr n0.duration = noteDuration n1 = note.Note('C') n1.duration = duration.Duration(.25) n2 = note.Note('D') n2.duration = duration.Duration(.25) nonTrill = [n1, n2, deepcopy(n1), deepcopy(n2)] midiMeasure = stream.Measure() midiMeasure.append(nonTrill) omrMeasure = stream.Measure() omrMeasure.append(n0) returnDict = { 'name': 'Non-Trill Measure Wrong Notes', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': deepcopy(omrMeasure), } return returnDict def createNachschlagTrillMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' noteDuration = duration.Duration('quarter') trillDuration = duration.Duration(.125) n0 = note.Note('E') n0.duration = noteDuration tn1 = note.Note('E') tn1.duration = trillDuration tn2 = note.Note('F') tn2.duration = trillDuration tn3 = note.Note('D') tn3.duration = trillDuration firstHalfTrill = [tn1, tn2, deepcopy(tn1), deepcopy(tn2)] secondHalfTrill = [deepcopy(tn1), deepcopy(tn2), deepcopy(tn1), tn3] expandedTrill = firstHalfTrill + secondHalfTrill midiMeasure = stream.Measure() midiMeasure.append(expandedTrill) omrMeasure = stream.Measure() omrMeasure.append(n0) nachschlagTrill = expressions.Trill() nachschlagTrill.nachschlag = True nachschlagTrill.quarterLength = trillDuration.quarterLength expectedFixedOmrMeasure = stream.Measure() noteWithTrill = deepcopy(n0) noteWithTrill.expressions.append(deepcopy(nachschlagTrill)) expectedFixedOmrMeasure.append(noteWithTrill) returnDict = { 'name': 'Nachschlag Trill', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': expectedFixedOmrMeasure, } return returnDict def createMeasureWithTrillAlready(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' noteDuration = duration.Duration('quarter') trillDuration = duration.Duration(.125) noteWithTrill = note.Note('F') noteWithTrill.duration = noteDuration trill = expressions.Trill() trill.quarterLength = trillDuration.quarterLength noteWithTrill.expressions.append(trill) tn1 = note.Note('F') tn1.duration = trillDuration tn2 = note.Note('G') tn2.duration = trillDuration expandedTrill = [tn1, tn2, deepcopy(tn1), deepcopy(tn2)] midiMeasure = stream.Measure() midiMeasure.append(expandedTrill) omrMeasure = stream.Measure() omrMeasure.append(noteWithTrill) returnDict = { 'name': 'OMR with Trill Notation', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': deepcopy(omrMeasure), } return returnDict testConditions = [ createDoubleTrillMeasure(), createWrongTrillMeasure(), createNonTrillMeasure(), createNachschlagTrillMeasure(), createMeasureWithTrillAlready(), ] for testCase in testConditions: self.checkFixerHelper(testCase, TrillFixer) def testTurnFixer(self): def createSingleTurnMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' omrMeasure = stream.Measure() omrNote = note.Note('F') omrNote.duration = duration.Duration('whole') omrMeasure.append(omrNote) expectedFixedOmrMeasure = stream.Stream() expectedOmrNote = deepcopy(omrNote) expectedOmrNote.expressions.append(expressions.Turn()) expectedFixedOmrMeasure.append(expectedOmrNote) midiMeasure = stream.Measure() turn = [note.Note('G'), note.Note('F'), note.Note('E'), note.Note('F')] midiMeasure.append(turn) returnDict = { 'name': 'Single Turn Measure', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': expectedFixedOmrMeasure, } return returnDict def createDoubleInvertedTurnMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' omrMeasure = stream.Measure() omrNote1 = note.Note('B-') middleNote = note.Note('G') omrNote2 = note.Note('B-') # enharmonic to trill omrMeasure.append([omrNote1, middleNote, omrNote2]) expectedFixedOmrMeasure = stream.Stream() expectOmrNote1 = deepcopy(omrNote1) expectOmrNote1.expressions.append(expressions.InvertedTurn()) expectOmrNote2 = deepcopy(omrNote2) expectOmrNote2.expressions.append(expressions.InvertedTurn()) expectedFixedOmrMeasure.append([expectOmrNote1, deepcopy(middleNote), expectOmrNote2]) midiMeasure = stream.Measure() turn1 = [note.Note('A'), note.Note('B-'), note.Note('C5'), note.Note('B-')] turn2 = [note.Note('G#'), note.Note('A#'), note.Note('B'), note.Note('A#')] for n in turn1: n.duration = duration.Duration(.25) for n in turn2: n.duration = duration.Duration(.25) midiMeasure.append([*turn1, deepcopy(middleNote), *turn2]) returnDict = { 'name': 'Inverted turns with accidentals separated By non-ornament Note', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': expectedFixedOmrMeasure, } return returnDict def createNonTurnMeasure(): ''' Returns a dictionary with the following keys returnDict = { 'name': string, 'midi': measure stream, 'omr': measure stream, 'expected': measure stream, } ''' omrMeasure = stream.Measure() omrNote = note.Note('A') omrNote.duration = duration.Duration('whole') omrMeasure.append(omrNote) midiMeasure = stream.Measure() turn = [note.Note('B'), note.Note('A'), note.Note('G'), note.Note('F')] midiMeasure.append(turn) returnDict = { 'name': 'Non-Turn Measure', 'midi': midiMeasure, 'omr': omrMeasure, 'expected': deepcopy(omrMeasure), } return returnDict testConditions = [createSingleTurnMeasure(), createDoubleInvertedTurnMeasure(), createNonTurnMeasure()] for testCase in testConditions: self.checkFixerHelper(testCase, TurnFixer) if __name__ == '__main__': import music21 music21.mainTest(Test)