Source code for music21.alpha.analysis.ornamentRecognizer

# ------------------------------------------------------------------------------
# Name:         alpha/analysis/ornamentRecognizer.py
# Purpose:      Identifies expanded ornaments
#
# Authors:      Janelle Sands
#
# Copyright:    Copyright © 2016 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations

from copy import deepcopy
import unittest

from music21.common.numberTools import opFrac
from music21.common.types import OffsetQL
from music21 import duration
from music21 import expressions
from music21 import interval
from music21 import note
from music21 import stream


[docs] class OrnamentRecognizer: ''' An object to identify if a stream of notes is an expanded ornament. Busy notes refer to the expanded ornament notes. Simple note(s) refer to the base note of ornament which is often shown with the ornament marking on it. '''
[docs] def calculateOrnamentNoteQl( self, busyNotes, simpleNotes=None ): ''' Finds the quarter length value for each ornament note assuming busy notes all are an expanded ornament. Expanded ornament total duration is time of all busy notes combined or duration of the first note in simpleNotes when provided. ''' numOrnamentNotes = len(busyNotes) totalDurationQuarterLength = self.calculateOrnamentTotalQl(busyNotes, simpleNotes) return opFrac(totalDurationQuarterLength / numOrnamentNotes)
[docs] def calculateOrnamentTotalQl( self, busyNotes: list[note.GeneralNote], simpleNotes: list[note.GeneralNote]|None = None ) -> OffsetQL: ''' Returns total length of trill assuming busy notes are all an expanded trill. This is either the time of all busy notes combined or duration of the first note in simpleNotes when provided. ''' if simpleNotes: return simpleNotes[0].duration.quarterLength trillQl: OffsetQL = 0.0 for n in busyNotes: trillQl += float(n.duration.quarterLength) return opFrac(trillQl)
[docs] class TrillRecognizer(OrnamentRecognizer): ''' An object to identify if a stream of ("busy") notes is an expanded trill. By default, does not consider Nachschlag trills, but setting checkNachschlag will consider. When optional stream of simpleNotes are provided, considers if busyNotes are an expansion of a trill which would be denoted on the first note in simpleNotes. ''' def __init__(self, checkNachschlag=False): self.checkNachschlag = checkNachschlag self.acceptableInterval = 3 self.minimumLengthForNachschlag = 5
[docs] def recognize(self, busyNotes, simpleNotes=None) -> bool|expressions.Trill: ''' Tries to identify the busy notes as a trill. When simple notes is provided, tries to identify busy notes as the trill shortened by simple notes. Currently only supports one simple note in simple notes. Only when checkNachschlag is true, allows last few notes to break trill rules. Trill interval size is interval between busy notes. Returns: False if not possible or the Trill Expression ''' # Enough notes to trill if len(busyNotes) <= 2: return False # Oscillation pitches n1 = busyNotes[0] n2 = busyNotes[1] if not n1.isNote or not n2.isNote: return False if abs(n1.pitch.midi - n2.pitch.midi) > self.acceptableInterval: return False twoNoteOscillation = True i = 0 for i in range(len(busyNotes)): noteConsidering = busyNotes[i] if not noteConsidering.isNote: return False if i % 2 == 0 and noteConsidering.pitch != n1.pitch: twoNoteOscillation = False break elif i % 2 != 0 and noteConsidering.pitch != n2.pitch: twoNoteOscillation = False break isNachschlag = False if twoNoteOscillation: pass elif not self.checkNachschlag: return False else: lengthOk = len(busyNotes) >= self.minimumLengthForNachschlag notTooMuchNachschlag = i >= len(busyNotes) / 2 if lengthOk and notTooMuchNachschlag: isNachschlag = True else: return False if not simpleNotes: # set up trill (goes up) or inverted trill (goes down) if n1.pitch.midi <= n2.pitch.midi: trill = expressions.Trill() else: trill = expressions.InvertedTrill() trill.quarterLength = self.calculateOrnamentNoteQl(busyNotes, simpleNotes) if isNachschlag: trill.nachschlag = True if n2.pitch.accidental is not None: trill.accidental = n2.pitch.accidental trill.resolveOrnamentalPitches(n1) return trill # currently ignore other notes in simpleNotes simpleNote = simpleNotes[0] # enharmonic invariant checker if simpleNote.pitch.midi not in (n1.pitch.midi, n2.pitch.midi): return False endNote = n2 startNote = n1 if simpleNote.pitch.midi == n2.pitch.midi: endNote = n1 startNote = n2 # set up trill (goes up) or inverted trill (goes down) if startNote.pitch.midi <= endNote.pitch.midi: trill = expressions.Trill() else: trill = expressions.InvertedTrill() trill.quarterLength = self.calculateOrnamentNoteQl(busyNotes, simpleNotes) if isNachschlag: trill.nachschlag = True if endNote.pitch.accidental is not None: trill.accidental = endNote.pitch.accidental trill.resolveOrnamentalPitches(startNote) return trill
[docs] class TurnRecognizer(OrnamentRecognizer): def __init__(self, ): self.acceptableInterval = 3 self.minimumLengthForNachschlag = 6 self.acceptableIntervals = [ interval.Interval('M2'), interval.Interval('M-2'), interval.Interval('m2'), interval.Interval('m-2'), interval.Interval('A2'), interval.Interval('A-2'), ]
[docs] def isAcceptableInterval(self, intervalToCheck: interval.Interval) -> bool: ''' Returns whether that interval can occur in a turn ''' return intervalToCheck in self.acceptableIntervals
[docs] def recognize( self, busyNotes, simpleNotes=None, ) -> bool|expressions.Turn|expressions.InvertedTurn: ''' Tries to identify the busy notes as a turn or inverted turn. When simple notes is provided, tries to identify busy notes as the turn shortened by simple notes. Currently only supports one simple note in simple notes. Turns and inverted turns have four notes separated by m2, M2, A2. Turns: start above base note go down to base note, go down again, and go back up to base note Inverted Turns: start below base note go up to base note, go up again, and go back down to base note When going up or down, must go to the adjacent note name, so A goes down to G, G#, G flat, G##, etc Returns: False if not possible or the Turn/Inverted Turn Expression ''' # number of notes/ duration of notes ok if len(busyNotes) != 4: return False if simpleNotes: eps = 0.1 totalBusyNotesDuration = 0 for n in busyNotes: totalBusyNotesDuration += n.duration.quarterLength if abs(simpleNotes[0].duration.quarterLength - totalBusyNotesDuration) > eps: return False # pitches ok if busyNotes[1].pitch.midi != busyNotes[3].pitch.midi: return False if simpleNotes and simpleNotes[0].pitch.midi != busyNotes[1].pitch.midi: return False # intervals ok firstInterval = interval.Interval(noteStart=busyNotes[0], noteEnd=busyNotes[1]) if not self.isAcceptableInterval(firstInterval): return False secondInterval = interval.Interval(noteStart=busyNotes[1], noteEnd=busyNotes[2]) if not self.isAcceptableInterval(secondInterval): return False thirdInterval = interval.Interval(noteStart=busyNotes[2], noteEnd=busyNotes[3]) if not self.isAcceptableInterval(thirdInterval): return False # goes in same direction if firstInterval.direction != secondInterval.direction: return False # and then in opposite direction if secondInterval.direction == thirdInterval.direction: return False # decide direction of turn to return if firstInterval.direction == interval.Interval('M-2').direction: # down turn = expressions.Turn() else: turn = expressions.InvertedTurn() turn.quarterLength = self.calculateOrnamentNoteQl(busyNotes, simpleNotes) return turn
class _TestCondition: def __init__( self, name, busyNotes, isOrnament, simpleNotes=None, ornamentSize=None, isNachschlag=False, isInverted=False ): self.name = name self.busyNotes = busyNotes self.isOrnament = isOrnament self.simpleNotes = simpleNotes self.ornamentSize = ornamentSize self.isNachschlag = isNachschlag self.isInverted = isInverted class Test(unittest.TestCase): def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) def testRecognizeTurn(self): # set up experiment testConditions = [] n1 = note.Note('F#') n1Enharmonic = note.Note('G-') noteInTurnNotBase = note.Note('G') noteNotInTurn = note.Note('A') evenTurn = [note.Note('G'), note.Note('F#'), note.Note('E'), note.Note('F#')] for n in evenTurn: n.duration.quarterLength = n1.duration.quarterLength / len(evenTurn) delayedTurn = [note.Note('G'), note.Note('F#'), note.Note('E'), note.Note('F#')] delayedTurn[0].duration.quarterLength = 2 * n1.duration.quarterLength / len(delayedTurn) for i in range(1, len(delayedTurn)): smallerDuration = n1.duration.quarterLength / (2 * len(delayedTurn)) delayedTurn[i].duration.quarterLength = smallerDuration rubatoTurn = [note.Note('G'), note.Note('F#'), note.Note('E'), note.Note('F#')] # durations all different, add up to 1 rubatoTurn[0].duration.quarterLength = .25 rubatoTurn[1].duration.quarterLength = .15 rubatoTurn[2].duration.quarterLength = .2 rubatoTurn[3].duration.quarterLength = .4 invertedTurn = [note.Note('E'), note.Note('F#'), note.Note('G'), note.Note('F#')] for n in invertedTurn: n.duration.quarterLength = n1.duration.quarterLength / len(invertedTurn) testConditions.append( _TestCondition( name='even turn no simple note', busyNotes=evenTurn, isOrnament=True) ) testConditions.append( _TestCondition( name='even turn with simple note', busyNotes=evenTurn, simpleNotes=[n1], isOrnament=True) ) testConditions.append( _TestCondition( name='even turn with enharmonic simple note', busyNotes=evenTurn, simpleNotes=[n1Enharmonic], isOrnament=True) ) testConditions.append( _TestCondition( name='even turn with wrong simple note still in turn', busyNotes=evenTurn, simpleNotes=[noteInTurnNotBase], isOrnament=False) ) testConditions.append( _TestCondition( name='even turn with wrong simple note not in turn', busyNotes=evenTurn, simpleNotes=[noteNotInTurn], isOrnament=False) ) testConditions.append( _TestCondition( name='rubato turn with all notes different length', busyNotes=rubatoTurn, isOrnament=True) ) testConditions.append( _TestCondition( name='delayed turn', busyNotes=delayedTurn, isOrnament=True) ) testConditions.append( _TestCondition( name='inverted turn', busyNotes=invertedTurn, isInverted=True, isOrnament=True) ) testConditions.append( _TestCondition( name='one wrong note', busyNotes=[note.Note('G'), note.Note('F#'), note.Note('E'), note.Note('D')], isOrnament=False) ) testConditions.append( _TestCondition( name='non-adjacent note jump', busyNotes=[note.Note('E'), note.Note('G'), note.Note('A'), note.Note('G')], isOrnament=False) ) testConditions.append( _TestCondition( name='trill is not a turn', busyNotes=[note.Note('G'), note.Note('F#'), note.Note('G'), note.Note('F#')], isOrnament=False) ) testConditions.append( _TestCondition( name='too many notes for turn', busyNotes=[note.Note('G'), note.Note('F#'), note.Note('E'), note.Note('F#'), note.Note('E')], isOrnament=False) ) testConditions.append( _TestCondition( name='too few notes for turn', busyNotes=[note.Note('G'), note.Note('F#'), note.Note('E')], isOrnament=False) ) testConditions.append( _TestCondition( name='total turn notes length longer than simple note', busyNotes=[note.Note('G'), note.Note('F#'), note.Note('E'), note.Note('F#')], simpleNotes=[n1], isOrnament=False) ) # run test for cond in testConditions: turnRecognizer = TurnRecognizer() if cond.simpleNotes: turn = turnRecognizer.recognize(cond.busyNotes, simpleNotes=cond.simpleNotes) else: turn = turnRecognizer.recognize(cond.busyNotes) if cond.isOrnament: if cond.isInverted: self.assertIsInstance(turn, expressions.InvertedTurn, cond.name) else: self.assertIsInstance(turn, expressions.Turn, cond.name) else: self.assertFalse(turn, cond.name) def testRecognizeTrill(self): # set up the experiment testConditions = [] n1Duration = duration.Duration('quarter') t1NumNotes = 4 t1UpInterval = interval.Interval('M2') t1DownInterval = interval.Interval('M-2') n1Lower = note.Note('G') n1Lower.duration = n1Duration n1Upper = note.Note('A') n1Upper.duration = n1Duration t1 = expressions.Trill() t1NoteDuration = calculateTrillNoteDuration(t1NumNotes, n1Duration) t1.quarterLength = t1NoteDuration t1Notes = t1.realize(n1Lower)[0] # GAGA t1NotesWithRest = deepcopy(t1Notes) # GA_A r1 = note.Rest() r1.duration = duration.Duration(t1NoteDuration) t1NotesWithRest[2] = r1 testConditions.append( _TestCondition( name='even whole step trill up without simple note', busyNotes=t1Notes, isOrnament=True, ornamentSize=t1UpInterval) ) testConditions.append( _TestCondition( name='even whole step trill up from simple note', busyNotes=t1Notes, simpleNotes=[n1Lower], isOrnament=True, ornamentSize=t1UpInterval) ) testConditions.append( _TestCondition( name='even whole step trill up to simple note', busyNotes=t1Notes, simpleNotes=[n1Upper], isOrnament=True, ornamentSize=t1DownInterval) ) testConditions.append( _TestCondition( name='valid trill up to enharmonic simple note', busyNotes=t1Notes, simpleNotes=[note.Note('G##')], # A isOrnament=True, ornamentSize=t1DownInterval) ) testConditions.append( _TestCondition( name='valid trill but not with simple note', busyNotes=t1Notes, simpleNotes=[note.Note('E')], isOrnament=False) ) testConditions.append( _TestCondition( name='invalid trill has rest inside', busyNotes=t1NotesWithRest, isOrnament=False) ) n2Duration = duration.Duration('half') t2NumNotes = 5 t2UpInterval = interval.Interval('m2') t2DownInterval = interval.Interval('m-2') n2Lower = note.Note('G#') n2Lower.duration = n2Duration n2Upper = note.Note('A') n2Upper.duration = n2Duration t2NoteDuration = duration.Duration(calculateTrillNoteDuration(t2NumNotes, n2Duration)) t2n1 = note.Note('A') # trill2note1 t2n1.duration = t2NoteDuration t2n2 = note.Note('G#') t2n2.duration = t2NoteDuration t2Notes = stream.Stream() # A G# A G# A t2Notes.append([t2n1, t2n2, deepcopy(t2n1), deepcopy(t2n2), deepcopy(t2n1)]) testConditions.append( _TestCondition( name='odd half step trill down without simple note', busyNotes=t2Notes, isOrnament=True, ornamentSize=t2DownInterval) ) testConditions.append( _TestCondition( name='odd half step trill down to simple note', busyNotes=t2Notes, simpleNotes=[n2Lower], isOrnament=True, ornamentSize=t2UpInterval) ) testConditions.append( _TestCondition( name='odd trill down from simple note', busyNotes=t2Notes, simpleNotes=[n2Upper], isOrnament=True, ornamentSize=t2DownInterval) ) n3Duration = duration.Duration('quarter') t3NumNotes = 8 t3UpInterval = interval.Interval('m2') t3DownInterval = interval.Interval('m-2') n3 = note.Note('B') n3.duration = n3Duration t3NoteDuration = duration.Duration(calculateTrillNoteDuration(t3NumNotes, n3Duration)) t3n1 = note.Note('C5') t3n1.duration = t3NoteDuration t3n2 = note.Note('B') t3n2.duration = t3NoteDuration nachschlagN1 = note.Note('D5') nachschlagN1.duration = t3NoteDuration nachschlagN2 = note.Note('E5') nachschlagN2.duration = t3NoteDuration nachschlagN3 = note.Note('F5') nachschlagN3.duration = t3NoteDuration t3Notes = stream.Stream() # C B C B C D E F t3Notes.append( [t3n1, t3n2, deepcopy(t3n1), deepcopy(t3n2), deepcopy(t3n1), nachschlagN1, nachschlagN2, nachschlagN3] ) testConditions.append( _TestCondition( name='Nachschlag trill when not checking for nachschlag', busyNotes=t3Notes, isOrnament=False) ) testConditions.append( _TestCondition( name='Nachschlag trill when checking for nachschlag', busyNotes=t3Notes, isNachschlag=True, isOrnament=True, ornamentSize=t3DownInterval) ) testConditions.append( _TestCondition( name='Nachschlag trill when checking for nachschlag up to simple note', busyNotes=t3Notes, simpleNotes=[n3], isNachschlag=True, isOrnament=True, ornamentSize=t3UpInterval) ) t4Duration = duration.Duration('eighth') t4n1 = note.Note('A') t4n1.duration = t4Duration t4n2 = note.Note('G') t4n2.duration = t4Duration testConditions.append( _TestCondition( name='One note not a trill', busyNotes=[t4n1], isOrnament=False) ) testConditions.append( _TestCondition( name='Two notes not a trill', busyNotes=[t4n1, t4n2], isOrnament=False) ) t5NoteDuration = duration.Duration('eighth') t5n1 = note.Note('A') # trill2note1 t5n1.duration = t5NoteDuration t5n2 = note.Note('C') t5n2.duration = t5NoteDuration t5Notes = stream.Stream() # A C A C t5Notes.append([t5n1, t5n2, deepcopy(t5n1), deepcopy(t5n2)]) testConditions.append( _TestCondition( name='Too big of oscillating interval to be trill', busyNotes=t5Notes, isOrnament=False) ) t6NoteDuration = duration.Duration('eighth') t6n1 = note.Note('F') # trill2note1 t6n1.duration = t6NoteDuration t6n2 = note.Note('E') t6n2.duration = t6NoteDuration t6n3 = note.Note('G') t6n3.duration = t2NoteDuration t5Notes = stream.Stream() # F E F G t5Notes.append([t6n1, t6n2, deepcopy(t6n1), t6n3]) testConditions.append( _TestCondition( name='Right interval but not oscillating between same notes', busyNotes=t5Notes, isOrnament=False) ) # run test for cond in testConditions: trillRecognizer = TrillRecognizer() if cond.isNachschlag: trillRecognizer.checkNachschlag = True if cond.simpleNotes: trill = trillRecognizer.recognize(cond.busyNotes, simpleNotes=cond.simpleNotes) else: trill = trillRecognizer.recognize(cond.busyNotes) if cond.isOrnament: self.assertIsInstance(trill, expressions.Trill, cond.name) # ensure trill is correct self.assertEqual(trill.nachschlag, cond.isNachschlag, cond.name) if cond.ornamentSize: if cond.simpleNotes: if cond.simpleNotes[0].pitch.midi == cond.busyNotes[1].pitch.midi: size = trill.getSize(cond.busyNotes[1]) else: size = trill.getSize(cond.busyNotes[0]) else: size = trill.getSize(cond.busyNotes[0]) self.assertEqual(size, cond.ornamentSize, cond.name) else: self.assertFalse(trill, cond.name)
[docs] def calculateTrillNoteDuration( numTrillNotes: int, totalDuration: duration.Duration ) -> OffsetQL: return opFrac(totalDuration.quarterLength / numTrillNotes)
if __name__ == '__main__': import music21 music21.mainTest(Test)