diff --git a/music21/clef.py b/music21/clef.py index 00f5cdf0c2..5ac4429b22 100644 --- a/music21/clef.py +++ b/music21/clef.py @@ -1015,128 +1015,7 @@ def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) - def testConversionClassMatch(self): - from xml.etree.ElementTree import fromstring as El - from music21.musicxml.xmlToM21 import MeasureParser - from music21 import clef - # need to get music21.clef.X, not X, because - # we are comparing the result to a translation outside - # clef.py - src = [ - [('G', 1, 0), clef.FrenchViolinClef], - [('G', 2, 0), clef.TrebleClef], - [('G', 2, -1), clef.Treble8vbClef], - [('G', 2, 1), clef.Treble8vaClef], - [('G', 3, 0), clef.GSopranoClef], - [('C', 1, 0), clef.SopranoClef], - [('C', 2, 0), clef.MezzoSopranoClef], - [('C', 3, 0), clef.AltoClef], - [('C', 4, 0), clef.TenorClef], - [('C', 5, 0), clef.CBaritoneClef], - [('F', 3, 0), clef.FBaritoneClef], - [('F', 4, 0), clef.BassClef], - [('F', 4, 1), clef.Bass8vaClef], - [('F', 4, -1), clef.Bass8vbClef], - [('F', 5, 0), clef.SubBassClef], - [('TAB', 5, 0), clef.TabClef] - ] - - MP = MeasureParser() - - for params, className in src: - sign, line, octaveChange = params - mxClef = El(r'' - + sign + '' - + str(line) + '' - + '' - + str(octaveChange) - + '') - c = MP.xmlToClef(mxClef) - - # environLocal.printDebug([type(c).__name__]) - - self.assertEqual(c.sign, params[0]) - self.assertEqual(c.line, params[1]) - self.assertEqual(c.octaveChange, params[2]) - self.assertIsInstance(c, className, - f'Failed Conversion of classes: {c} is not a {className}') - - def testContexts(self): - from music21 import stream - from music21 import note - from music21 import meter - - n1 = note.Note('C') - n1.offset = 10 - c1 = AltoClef() - c1.offset = 0 - s1 = stream.Stream([c1, n1]) - - self.assertIs(s1.recurse().notes[0].getContextByClass(Clef), c1) - # equally good: getContextsByClass(Clef)[0] - - del s1 - - n2 = note.Note('D') - n2.duration.type = 'whole' - n3 = note.Note('E') - n3.duration.type = 'whole' - ts1 = meter.TimeSignature('4/4') - s2 = stream.Stream() - s2.append(c1) - s2.append(ts1) - s2.append(n2) - s2.append(n3) - s2.makeMeasures() - self.assertIs(n2.getContextByClass(Clef), c1) - - del s2 - - n4 = note.Note('F') - n4.duration.type = 'half' - n5 = note.Note('G') - n5.duration.type = 'half' - n6 = note.Note('A') - n6.duration.type = 'whole' - - ts2 = meter.TimeSignature('4/4') - bc1 = BassClef() - tc1 = TrebleClef() - - s3 = stream.Stream() - s3.append(bc1) - s3.append(ts2) - s3.append(n4) - s3.append(tc1) - s3.append(n5) - s3.append(n6) - s3.makeMeasures() - - self.assertIs(n4.getContextByClass(stream.Measure), n5.getContextByClass(stream.Measure)) - self.assertIs(n4.getContextByClass(Clef), bc1) - self.assertIs(n5.getContextByClass(Clef), tc1) - self.assertIs(n6.getContextByClass(Clef), tc1) - - def testTabClefBeamDirections(self): - - from music21 import stream - from music21 import clef - from music21 import meter - from music21 import note - - m = stream.Measure() - - n1 = note.Note(64, quarterLength=0.25) - n2 = note.Note(67, quarterLength=0.25) - - m.append(clef.TabClef()) - m.append(meter.TimeSignature('4/4')) - m.append(n1) - m.append(n2) - m.makeBeams(inPlace=True) - - self.assertEqual(m.notes[0].stemDirection, 'down') - + # all other tests in test/test_clef # ------------------------------------------------------------------------------ # define presented order in documentation diff --git a/music21/expressions.py b/music21/expressions.py index 0baed86a63..6625ca9e5b 100644 --- a/music21/expressions.py +++ b/music21/expressions.py @@ -26,7 +26,6 @@ import copy import string -import unittest import typing as t from music21 import base @@ -37,6 +36,7 @@ from music21 import spanner from music21 import style + if t.TYPE_CHECKING: from music21 import note @@ -1593,7 +1593,7 @@ def noteExtremes(self) -> tuple[t.Optional[note.Note], (, ) ''' from music21 import chord - from music21 import note + from music21 import note # pylint: disable=redefined-outer-name notes = [] for n_or_ch in self: if isinstance(n_or_ch, note.Note): @@ -1604,168 +1604,7 @@ def noteExtremes(self) -> tuple[t.Optional[note.Note], # ------------------------------------------------------------------------------ -class Test(unittest.TestCase): - - def testRealize(self): - from music21 import note - from music21 import stream - n1 = note.Note('D4') - n1.quarterLength = 4 - n1.expressions.append(WholeStepMordent()) - expList = realizeOrnaments(n1) - st1 = stream.Stream() - st1.append(expList) - st1n = st1.notes - self.assertEqual(st1n[0].name, 'D') - self.assertEqual(st1n[0].quarterLength, 0.125) - self.assertEqual(st1n[1].name, 'C') - self.assertEqual(st1n[1].quarterLength, 0.125) - self.assertEqual(st1n[2].name, 'D') - self.assertEqual(st1n[2].quarterLength, 3.75) - - def testGetRepeatExpression(self): - from music21 import expressions - - te = expressions.TextExpression('lightly') - # no repeat expression is possible - self.assertEqual(te.getRepeatExpression(), None) - - te = expressions.TextExpression('d.c.') - self.assertEqual(str(te.getRepeatExpression()), - "") - re = te.getRepeatExpression() - self.assertEqual(re.getTextExpression().content, 'd.c.') - - te = expressions.TextExpression('DC al coda') - self.assertEqual(str(te.getRepeatExpression()), - "") - re = te.getRepeatExpression() - self.assertEqual(re.getTextExpression().content, 'DC al coda') - - te = expressions.TextExpression('DC al fine') - self.assertEqual(str(te.getRepeatExpression()), - "") - re = te.getRepeatExpression() - self.assertEqual(re.getTextExpression().content, 'DC al fine') - - te = expressions.TextExpression('ds al coda') - self.assertEqual(str(te.getRepeatExpression()), - "") - re = te.getRepeatExpression() - self.assertEqual(re.getTextExpression().content, 'ds al coda') - - te = expressions.TextExpression('d.s. al fine') - self.assertEqual(str(te.getRepeatExpression()), - "") - re = te.getRepeatExpression() - self.assertEqual(re.getTextExpression().content, 'd.s. al fine') - - def testExpandTurns(self): - from music21 import note - from music21 import stream - from music21 import clef - from music21 import key - from music21 import meter - p1 = stream.Part() - m1 = stream.Measure() - m2 = stream.Measure() - p1.append(clef.TrebleClef()) - p1.append(key.Key('F', 'major')) - p1.append(meter.TimeSignature('2/4')) - n1 = note.Note('C5', type='half') - turn0 = Turn() - n1.expressions.append(turn0) - n2 = note.Note('B4', type='quarter') - n2.duration.dots = 1 - - n2.expressions.append(InvertedTurn()) - m1.append(n1) - m2.append(key.KeySignature(5)) - m2.append(n2) - m2.append(note.Rest('eighth')) - p1.append(m1) - p1.append(m2) - realized1 = realizeOrnaments(n1) - realized2 = realizeOrnaments(n2) - self.assertEqual('C5 D5 C5 B-4 C5', ' '.join(n.pitch.nameWithOctave for n in realized1)) - self.assertEqual('B4 A#4 B4 C#5 B4', ' '.join(n.pitch.nameWithOctave for n in realized2)) - self.assertEqual(realized1[0].quarterLength, 1.0) - self.assertEqual(realized1[1].quarterLength, 0.25) - self.assertEqual(realized2[0].quarterLength, 0.5) - self.assertEqual(realized2[1].quarterLength, 0.25) - - turn0.quarterLength = 0.125 - realized1b = realizeOrnaments(n1) - self.assertEqual(realized1b[0].quarterLength, 1.5) - self.assertEqual(realized1b[1].quarterLength, 0.125) - - - def testExpandTrills(self): - from music21 import note - from music21 import stream - from music21 import clef - from music21 import key - from music21 import meter - p1 = stream.Part() - m1 = stream.Measure() - p1.append(clef.TrebleClef()) - p1.append(key.Key('D', 'major')) - p1.append(meter.TimeSignature('1/4')) - n1 = note.Note('E4', type='eighth') - n1.expressions.append(Trill()) - m1.append(n1) - p1.append(m1) - realized = realizeOrnaments(n1) - self.assertIsInstance(realized, list) - self.assertEqual(len(realized), 4) - self.assertIsInstance(realized[0], note.Note) - self.assertEqual(realized[0].quarterLength, 0.125) - self.assertEqual('E4 F#4 E4 F#4', ' '.join(n.pitch.nameWithOctave for n in realized)) - - - def testTrillExtensionA(self): - '''Test basic wave line creation and output, as well as passing - objects through make measure calls. - ''' - from music21 import stream - from music21 import note - from music21 import chord - from music21 import expressions - from music21.musicxml import m21ToXml - s = stream.Stream() - s.repeatAppend(note.Note(), 12) - n1 = s.notes[0] - n2 = s.notes[-1] - sp1 = expressions.TrillExtension(n1, n2) - s.append(sp1) - raw = m21ToXml.GeneralObjectExporter().parse(s) - self.assertEqual(raw.count(b'wavy-line'), 2) - - s = stream.Stream() - s.repeatAppend(chord.Chord(['c-3', 'g4']), 12) - n1 = s.notes[0] - n2 = s.notes[-1] - sp1 = expressions.TrillExtension(n1, n2) - s.append(sp1) - raw = m21ToXml.GeneralObjectExporter().parse(s) - # s.show() - self.assertEqual(raw.count(b'wavy-line'), 2) - - def testUnpitchedUnsupported(self): - from music21 import note - - unp = note.Unpitched() - mord = Mordent() - with self.assertRaises(TypeError): - mord.realize(unp) # type: ignore - - -# class TestExternal(unittest.TestCase): -# def testCPEBachRealizeOrnaments(self): -# from music21 import corpus -# cpe = corpus.parse('cpebach/h186').parts[0].measures(1, 4) -# cpe2 = cpe.realizeOrnaments() -# cpe2.show() +# Tests moved to test/test_expressions # ------------------------------------------------------------------------------ @@ -1774,5 +1613,5 @@ def testUnpitchedUnsupported(self): if __name__ == '__main__': import music21 - music21.mainTest(Test) + music21.mainTest() diff --git a/music21/graph/__init__.py b/music21/graph/__init__.py index ab3d55b3c8..7d140ae37b 100644 --- a/music21/graph/__init__.py +++ b/music21/graph/__init__.py @@ -184,7 +184,7 @@ def testAll(self): def testPlotChordsC(self): from music21 import dynamics from music21 import note - from music21 import stream + from music21 import stream # pylint: disable=redefined-outer-name # only in TYPE_CHECKING from music21 import scale sc = scale.MajorScale('c4') @@ -219,7 +219,7 @@ def testPlotChordsC(self): def testHorizontalInstrumentationB(self): from music21 import corpus from music21 import dynamics - from music21 import stream + from music21 import stream # pylint: disable=redefined-outer-name # only in TYPE_CHECKING s = corpus.parse('bwv66.6') dyn = ['p', 'mf', 'f', 'ff', 'mp', 'fff', 'ppp'] i = 0 diff --git a/music21/interval.py b/music21/interval.py index 1ecbd8b1b4..be98ef2e60 100644 --- a/music21/interval.py +++ b/music21/interval.py @@ -26,7 +26,6 @@ import enum import math import re -import unittest import typing as t from music21 import base @@ -35,7 +34,13 @@ from music21.common.types import StepName from music21 import environment from music21 import exceptions21 -# from music21 import pitch # SHOULD NOT, b/c of enharmonics + + +if t.TYPE_CHECKING: + from music21 import key + from music21 import note + from music21 import pitch + environLocal = environment.Environment('interval') @@ -308,8 +313,8 @@ class IntervalException(exceptions21.Music21Exception): # some utility functions def _extractPitch( - nOrP: t.Union['music21.note.Note', 'music21.pitch.Pitch'] -) -> 'music21.pitch.Pitch': + nOrP: t.Union[note.Note, pitch.Pitch] +) -> pitch.Pitch: ''' utility function to return either the object itself or the `.pitch` if it's a Note. @@ -637,7 +642,7 @@ def convertSemitoneToSpecifierGeneric(count: t.Union[int, float]) -> tuple[Speci return convertSemitoneToSpecifierGenericMicrotone(count)[:2] -_pythagorean_cache: dict[str, tuple['music21.pitch.Pitch', Fraction]] = {} +_pythagorean_cache: dict[str, tuple[pitch.Pitch, Fraction]] = {} def intervalToPythagoreanRatio(intervalObj: Interval) -> Fraction: @@ -1416,8 +1421,8 @@ def transposePitch(self, p: 'music21.pitch.Pitch', *, inPlace=False): def transposePitchKeyAware( self, - p: 'music21.pitch.Pitch', - k: t.Optional['music21.key.KeySignature'] = None, + p: pitch.Pitch, + k: t.Optional[key.KeySignature] = None, *, inPlace: bool = False ): @@ -1488,7 +1493,7 @@ def transposePitchKeyAware( Does not take into account harmonic or melodic minor. ''' - from music21 import pitch + from music21 import pitch # pylint: disable=redefined-outer-name # only in TYPE_CHECKING if k is None: return self.transposePitch(p, inPlace=inPlace) @@ -2587,8 +2592,8 @@ def _stringToDiatonicChromatic( def notesToGeneric( - n1: t.Union['music21.pitch.Pitch', 'music21.note.Note'], - n2: t.Union['music21.pitch.Pitch', 'music21.note.Note'] + n1: t.Union[pitch.Pitch, note.Note], + n2: t.Union[pitch.Pitch, note.Note] ) -> GenericInterval: ''' Given two :class:`~music21.note.Note` objects, @@ -2615,8 +2620,8 @@ def notesToGeneric( return GenericInterval(genDist) def notesToChromatic( - n1: t.Union['music21.pitch.Pitch', 'music21.note.Note'], - n2: t.Union['music21.pitch.Pitch', 'music21.note.Note'] + n1: t.Union[pitch.Pitch, note.Note], + n2: t.Union[pitch.Pitch, note.Note] ) -> ChromaticInterval: ''' Given two :class:`~music21.note.Note` objects, @@ -2982,18 +2987,18 @@ def __init__(self, arg0: t.Union[str, int, float, - 'music21.pitch.Pitch', - 'music21.note.Note', + pitch.Pitch, + note.Note, None] = None, - arg1: t.Union['music21.pitch.Pitch', 'music21.note.Note', None] = None, + arg1: t.Union[pitch.Pitch, note.Note, None] = None, /, *, diatonic: t.Optional[DiatonicInterval] = None, chromatic: t.Optional[ChromaticInterval] = None, - pitchStart: t.Optional['music21.pitch.Pitch'] = None, - pitchEnd: t.Optional['music21.pitch.Pitch'] = None, - noteStart: t.Union['music21.note.Note', 'music21.pitch.Pitch', None] = None, - noteEnd: t.Union['music21.note.Note', 'music21.pitch.Pitch', None] = None, + pitchStart: t.Optional[pitch.Pitch] = None, + pitchEnd: t.Optional[pitch.Pitch] = None, + noteStart: t.Union[note.Note, pitch.Pitch, None] = None, + noteEnd: t.Union[note.Note, pitch.Pitch, None] = None, name: t.Optional[str] = None, **keywords): super().__init__(**keywords) @@ -3070,13 +3075,13 @@ def __init__(self, self.chromatic: ChromaticInterval = chromatic # these can be accessed through pitchStart and pitchEnd properties - self._pitchStart: t.Optional['music21.pitch.Pitch'] = pitchStart - self._pitchEnd: t.Optional['music21.pitch.Pitch'] = pitchEnd + self._pitchStart: t.Optional[pitch.Pitch] = pitchStart + self._pitchEnd: t.Optional[pitch.Pitch] = pitchEnd self.intervalType: t.Literal['harmonic', 'melodic', ''] = '' def _reprInternal(self): - from music21 import pitch + from music21 import pitch # pylint: disable=redefined-outer-name # only in TYPE_CHECKING try: shift = self._diatonicIntervalCentShift() except AttributeError: @@ -3640,7 +3645,7 @@ def noteStart(self) -> t.Optional[music21.note.Note]: if p and p._client: return p._client elif p: - from music21 import note + from music21 import note # pylint: disable=redefined-outer-name return note.Note(pitch=p) @noteStart.setter @@ -3660,7 +3665,7 @@ def noteEnd(self) -> t.Optional[music21.note.Note]: if p and p._client: return p._client elif p: - from music21 import note + from music21 import note # pylint: disable=redefined-outer-name return note.Note(pitch=p) @noteEnd.setter @@ -3672,9 +3677,9 @@ def noteEnd(self, n: t.Optional[music21.note.Note]): # ------------------------------------------------------------------------------ -def getWrittenHigherNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pitch'], - note2: t.Union['music21.note.Note', 'music21.pitch.Pitch'] - ) -> t.Union['music21.note.Note', 'music21.pitch.Pitch']: +def getWrittenHigherNote(note1: t.Union[note.Note, pitch.Pitch], + note2: t.Union[note.Note, pitch.Pitch] + ) -> t.Union[note.Note, pitch.Pitch]: ''' Given two :class:`~music21.pitch.Pitch` or :class:`~music21.note.Note` objects, this function returns the higher element based on diatonic note @@ -3710,9 +3715,9 @@ def getWrittenHigherNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pitc return getAbsoluteHigherNote(note1, note2) -def getAbsoluteHigherNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pitch'], - note2: t.Union['music21.note.Note', 'music21.pitch.Pitch'] - ) -> t.Union['music21.note.Note', 'music21.pitch.Pitch']: +def getAbsoluteHigherNote(note1: t.Union[note.Note, pitch.Pitch], + note2: t.Union[note.Note, pitch.Pitch] + ) -> t.Union[note.Note, pitch.Pitch]: ''' Given two :class:`~music21.pitch.Pitch` or :class:`~music21.note.Note` objects, returns the higher element based on sounding pitch. @@ -3733,9 +3738,9 @@ def getAbsoluteHigherNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pit return note1 -def getWrittenLowerNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pitch'], - note2: t.Union['music21.note.Note', 'music21.pitch.Pitch'] - ) -> t.Union['music21.note.Note', 'music21.pitch.Pitch']: +def getWrittenLowerNote(note1: t.Union[note.Note, pitch.Pitch], + note2: t.Union[note.Note, pitch.Pitch] + ) -> t.Union[note.Note, pitch.Pitch]: ''' Given two :class:`~music21.pitch.Pitch` or :class:`~music21.note.Note` objects, returns the lower element based on diatonic note @@ -3765,9 +3770,9 @@ def getWrittenLowerNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pitch return getAbsoluteLowerNote(note1, note2) -def getAbsoluteLowerNote(note1: t.Union['music21.note.Note', 'music21.pitch.Pitch'], - note2: t.Union['music21.note.Note', 'music21.pitch.Pitch'] - ) -> t.Union['music21.note.Note', 'music21.pitch.Pitch']: +def getAbsoluteLowerNote(note1: t.Union[note.Note, pitch.Pitch], + note2: t.Union[note.Note, pitch.Pitch] + ) -> t.Union[note.Note, pitch.Pitch]: ''' Given two :class:`~music21.note.Note` or :class:`~music21.pitch.Pitch` objects, returns the lower element based on actual pitch. @@ -3914,7 +3919,7 @@ def add(intervalList): >>> interval.add([P5, 'P-4']) ''' - from music21 import pitch + from music21 import pitch # pylint: disable=redefined-outer-name # only in TYPE_CHECKING if not intervalList: raise IntervalException('Cannot add an empty set of intervals') @@ -3962,7 +3967,7 @@ def subtract(intervalList): -1 ''' - from music21 import pitch + from music21 import pitch # pylint: disable=redefined-outer-name # only in TYPE_CHECKING if not intervalList: raise IntervalException('Cannot add an empty set of intervals') @@ -3980,470 +3985,9 @@ def subtract(intervalList): # print(n1.nameWithOctave, n2.nameWithOctave) return Interval(noteStart=n1, noteEnd=n2) -# ------------------------------------------------------------------------------ - - -class Test(unittest.TestCase): - - def testConstructorPitches(self): - from music21 import interval - from music21 import pitch - p1 = pitch.Pitch('A4') - p2 = pitch.Pitch('E5') - intv = interval.Interval(p1, p2) - self.assertEqual(intv.name, 'P5') - self.assertIs(intv.pitchStart, p1) - self.assertIs(intv.pitchEnd, p2) - - # same with keywords, but reversed - intv = interval.Interval(pitchStart=p2, pitchEnd=p1) - self.assertEqual(intv.name, 'P5') - self.assertEqual(intv.directedName, 'P-5') - self.assertIs(intv.pitchStart, p2) - self.assertIs(intv.pitchEnd, p1) - - def testFirst(self): - from music21.note import Note - from music21.pitch import Accidental - n1 = Note() - n2 = Note() - - n1.step = 'C' - n1.octave = 4 - - n2.step = 'B' - n2.octave = 5 - n2.pitch.accidental = Accidental('-') - - int0 = Interval(noteStart=n1, noteEnd=n2) - dInt0 = int0.diatonic - gInt0 = dInt0.generic - - self.assertFalse(gInt0.isDiatonicStep) - self.assertTrue(gInt0.isSkip) - - n1.pitch.accidental = Accidental('#') - int1 = Interval(noteStart=n1, noteEnd=n2) - - cInt1 = notesToChromatic(n1, n2) # returns music21.interval.ChromaticInterval object - cInt2 = int1.chromatic # returns same as cInt1 -- a different way of thinking of things - self.assertEqual(cInt1.semitones, cInt2.semitones) - - self.assertEqual(int1.simpleNiceName, 'Diminished Seventh') - - self.assertEqual(int1.directedSimpleNiceName, 'Ascending Diminished Seventh') - self.assertEqual(int1.name, 'd14') - self.assertEqual(int1.specifier, Specifier.DIMINISHED) - - # returns same as gInt1 -- just a different way of thinking of things - dInt1 = int1.diatonic - gInt1 = dInt1.generic - - self.assertEqual(gInt1.directed, 14) - self.assertEqual(gInt1.undirected, 14) - self.assertEqual(gInt1.simpleDirected, 7) - self.assertEqual(gInt1.simpleUndirected, 7) - - self.assertEqual(cInt1.semitones, 21) - self.assertEqual(cInt1.undirected, 21) - self.assertEqual(cInt1.mod12, 9) - self.assertEqual(cInt1.intervalClass, 3) - - n4 = Note() - n4.step = 'D' - n4.octave = 3 - n4.pitch.accidental = '-' - - # n3 = interval.transposePitch(n4, 'AA8') - # if n3.pitch.accidental is not None: - # print(n3.step, n3.pitch.accidental.name, n3.octave) - # else: - # print(n3.step, n3.octave) - # print(n3.name) - # print() - - cI = ChromaticInterval(-14) - self.assertEqual(cI.semitones, -14) - self.assertEqual(cI.cents, -1400) - self.assertEqual(cI.undirected, 14) - self.assertEqual(cI.mod12, 10) - self.assertEqual(cI.intervalClass, 2) - - lowB = Note() - lowB.name = 'B' - highBb = Note() - highBb.name = 'B-' - highBb.octave = 5 - dimOct = Interval(lowB, highBb) - self.assertEqual(dimOct.niceName, 'Diminished Octave') - - noteA1 = Note() - noteA1.name = 'E-' - noteA1.octave = 4 - noteA2 = Note() - noteA2.name = 'F#' - noteA2.octave = 5 - intervalA1 = Interval(noteA1, noteA2) - - noteA3 = Note() - noteA3.name = 'D' - noteA3.octave = 1 - - noteA4 = transposeNote(noteA3, intervalA1) - self.assertEqual(noteA4.name, 'E#') - self.assertEqual(noteA4.octave, 2) - - interval1 = Interval('-P5') - - n5 = transposeNote(n4, interval1) - n6 = transposeNote(n4, 'P-5') - self.assertEqual(n5.name, 'G-') - self.assertEqual(n6.name, n5.name) - - # same thing using newer syntax: - - interval1 = Interval('P-5') - - n5 = transposeNote(n4, interval1) - self.assertEqual(n5.name, 'G-') - n7 = Note() - n8 = transposeNote(n7, 'P8') - self.assertEqual(n8.name, 'C') - self.assertEqual(n8.octave, 5) - - n9 = transposeNote(n7, 'm7') # should be B- - self.assertEqual(n9.name, 'B-') - self.assertEqual(n9.octave, 4) - n10 = transposeNote(n7, 'dd-2') # should be B## - self.assertEqual(n10.name, 'B##') - self.assertEqual(n10.octave, 3) - - # test getWrittenHigherNote functions - (nE, nESharp, nFFlat, nF1, nF2) = (Note(), Note(), Note(), Note(), Note()) - - nE.name = 'E' - nESharp.name = 'E#' - nFFlat.name = 'F-' - nF1.name = 'F' - nF2.name = 'F' - - higher1 = getWrittenHigherNote(nE, nESharp) - higher2 = getWrittenHigherNote(nESharp, nFFlat) - higher3 = getWrittenHigherNote(nF1, nF2) - - self.assertEqual(higher1, nESharp) - self.assertEqual(higher2, nFFlat) - self.assertEqual(higher3, nF1) # in case of ties, first is returned - - higher4 = getAbsoluteHigherNote(nE, nESharp) - higher5 = getAbsoluteHigherNote(nESharp, nFFlat) - higher6 = getAbsoluteHigherNote(nESharp, nF1) - higher7 = getAbsoluteHigherNote(nF1, nESharp) - - self.assertEqual(higher4, nESharp) - self.assertEqual(higher5, nESharp) - self.assertEqual(higher6, nESharp) - self.assertEqual(higher7, nF1) - - lower1 = getWrittenLowerNote(nESharp, nE) - lower2 = getWrittenLowerNote(nFFlat, nESharp) - lower3 = getWrittenLowerNote(nF1, nF2) - - self.assertEqual(lower1, nE) - self.assertEqual(lower2, nESharp) - self.assertEqual(lower3, nF1) # still returns first. - - lower4 = getAbsoluteLowerNote(nESharp, nE) - lower5 = getAbsoluteLowerNote(nFFlat, nESharp) - lower6 = getAbsoluteLowerNote(nESharp, nF1) - - self.assertEqual(lower4, nE) - self.assertEqual(lower5, nFFlat) - self.assertEqual(lower6, nESharp) - - middleC = Note() - lowerC = Note() - lowerC.octave = 3 - descendingOctave = Interval(middleC, lowerC) - self.assertEqual(descendingOctave.generic.simpleDirected, 1) - # no descending unisons ever - self.assertEqual(descendingOctave.generic.semiSimpleDirected, -8) - # no descending unisons ever - self.assertEqual(descendingOctave.directedName, 'P-8') - self.assertEqual(descendingOctave.directedSimpleName, 'P1') - - lowerG = Note() - lowerG.name = 'G' - lowerG.octave = 3 - descendingFourth = Interval(middleC, lowerG) - self.assertEqual(descendingFourth.diatonic.directedNiceName, - 'Descending Perfect Fourth') - self.assertEqual(descendingFourth.diatonic.directedSimpleName, 'P-4') - self.assertEqual(descendingFourth.diatonic.simpleName, 'P4') - self.assertEqual(descendingFourth.diatonic.mod7, 'P5') - - perfectFifth = descendingFourth.complement - self.assertEqual(perfectFifth.niceName, 'Perfect Fifth') - self.assertEqual(perfectFifth.diatonic.simpleName, 'P5') - self.assertEqual(perfectFifth.diatonic.mod7, 'P5') - self.assertEqual(perfectFifth.complement.niceName, 'Perfect Fourth') - - def testCreateIntervalFromPitch(self): - from music21 import pitch - p1 = pitch.Pitch('c') - p2 = pitch.Pitch('g') - i = Interval(p1, p2) - self.assertEqual(i.intervalClass, 5) - - def testTransposeImported(self): - - def collectAccidentalDisplayStatus(s_inner): - post = [] - for e in s_inner.flatten().notes: - if e.pitch.accidental is not None: - post.append(e.pitch.accidental.displayStatus) - else: # mark as not having an accidental - post.append('x') - return post - - from music21 import corpus - s = corpus.parse('bach/bwv66.6') - # this has accidentals in measures 2 and 6 - sSub = s.parts[3].measures(2, 6) - - self.assertEqual(collectAccidentalDisplayStatus(sSub), - ['x', False, 'x', 'x', True, False, 'x', False, False, False, - False, False, False, 'x', 'x', 'x', False, False, False, - 'x', 'x', 'x', 'x', True, False]) - - sTransposed = sSub.flatten().transpose('p5') - # sTransposed.show() - - self.assertEqual(collectAccidentalDisplayStatus(sTransposed), - ['x', None, 'x', 'x', None, None, None, None, None, - None, None, None, None, 'x', None, None, None, None, - None, 'x', 'x', 'x', None, None, None]) - - def testIntervalMicrotonesA(self): - from music21 import interval - from music21 import pitch - - i = interval.Interval('m3') - self.assertEqual(i.chromatic.cents, 300) - self.assertEqual(i.cents, 300.0) - - i = interval.Interval('p5') - self.assertEqual(i.chromatic.cents, 700) - self.assertEqual(i.cents, 700.0) - - i = interval.Interval(8) - self.assertEqual(i.chromatic.cents, 800) - self.assertEqual(i.cents, 800.0) - - i = interval.Interval(8.5) - self.assertEqual(i.chromatic.cents, 850.0) - self.assertEqual(i.cents, 850.0) - - i = interval.Interval(5.25) # a sharp p4 - self.assertEqual(i.cents, 525.0) - # we can subtract the two to get an offset - self.assertEqual(i.cents, 525.0) - self.assertEqual(str(i), '') - self.assertEqual(i._diatonicIntervalCentShift(), 25) - - i = interval.Interval(4.75) # a flat p4 - self.assertEqual(str(i), '') - self.assertEqual(i._diatonicIntervalCentShift(), -25) - - i = interval.Interval(4.48) # a sharp M3 - self.assertEqual(str(i), '') - self.assertAlmostEqual(i._diatonicIntervalCentShift(), 48.0) - - i = interval.Interval(4.5) # a sharp M3 - self.assertEqual(str(i), '') - self.assertAlmostEqual(i._diatonicIntervalCentShift(), 50.0) - - i = interval.Interval(5.25) # a sharp p4 - p1 = pitch.Pitch('c4') - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'F4(+25c)') - - i = interval.Interval(5.80) # a sharp p4 - p1 = pitch.Pitch('c4') - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'F#4(-20c)') - - i = interval.Interval(6.00) # an exact Tritone - p1 = pitch.Pitch('c4') - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'F#4') - - i = interval.Interval(5) # a chromatic p4 - p1 = pitch.Pitch('c4') - p1.microtone = 10 # c+20 - self.assertEqual(str(p1), 'C4(+10c)') - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'F4(+10c)') - - i = interval.Interval(7.20) # a sharp P5 - p1 = pitch.Pitch('c4') - p1.microtone = -20 # c+20 - self.assertEqual(str(p1), 'C4(-20c)') - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'G4') - - i = interval.Interval(7.20) # a sharp P5 - p1 = pitch.Pitch('c4') - p1.microtone = 80 # c+20 - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'G#4') - - i = interval.Interval(0.20) # a sharp unison - p1 = pitch.Pitch('e4') - p1.microtone = 10 - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'E~4(-20c)') - - i = interval.Interval(0.05) # a tiny bit sharp unison - p1 = pitch.Pitch('e4') - p1.microtone = 5 - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'E4(+10c)') - - i = interval.Interval(12.05) # a tiny bit sharp octave - p1 = pitch.Pitch('e4') - p1.microtone = 5 - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'E5(+10c)') - - i = interval.Interval(11.85) # a flat octave - p1 = pitch.Pitch('e4') - p1.microtone = 5 - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'E5(-10c)') - - i = interval.Interval(11.85) # a flat octave - p1 = pitch.Pitch('e4') - p1.microtone = -20 - p2 = i.transposePitch(p1) - self.assertEqual(str(p2), 'E`5(+15c)') - - def testIntervalMicrotonesB(self): - from music21 import interval - from music21 import note - i = interval.Interval(note.Note('c4'), note.Note('c#4')) - self.assertEqual(str(i), '') - - i = interval.Interval(note.Note('c4'), note.Note('c~4')) - self.assertEqual(str(i), '') - - def testDescendingAugmentedUnison(self): - from music21 import interval - from music21 import note - ns = note.Note('C4') - ne = note.Note('C-4') - i = interval.Interval(noteStart=ns, noteEnd=ne) - directedNiceName = i.directedNiceName - self.assertEqual(directedNiceName, 'Descending Diminished Unison') - - def testTransposeWithChromaticInterval(self): - from music21 import interval - from music21 import note - ns = note.Note('C4') - i = interval.ChromaticInterval(5) - n2 = ns.transpose(i) - self.assertEqual(n2.nameWithOctave, 'F4') - - ns = note.Note('B#3') - i = interval.ChromaticInterval(5) - n2 = ns.transpose(i) - self.assertEqual(n2.nameWithOctave, 'F4') - - def testTransposeImplicit(self): - from music21 import interval - from music21 import pitch - p = pitch.Pitch('D#4') - i = interval.Interval(0) - self.assertTrue(i.implicitDiatonic) - p2 = i.transposePitch(p) - self.assertEqual(p.ps, p2.ps) - self.assertNotEqual(p.name, p2.name) - - def testRepeatedTransposePitch(self): - from music21 import interval - from music21 import pitch - p = pitch.Pitch('C1') - intv = interval.Interval('P5') - out = [] - for _ in range(33): - out.append(p.nameWithOctave) - intv.transposePitch(p, maxAccidental=None, inPlace=True) - self.assertEqual( - out, - ['C1', 'G1', 'D2', 'A2', 'E3', 'B3', 'F#4', - 'C#5', 'G#5', 'D#6', 'A#6', 'E#7', 'B#7', 'F##8', - 'C##9', 'G##9', 'D##10', 'A##10', 'E##11', 'B##11', 'F###12', - 'C###13', 'G###13', 'D###14', 'A###14', 'E###15', 'B###15', 'F####16', - 'C####17', 'G####17', 'D####18', 'A####18', 'E####19', - ] - ) - with self.assertRaisesRegex(pitch.AccidentalException, - '5.0 is not a supported accidental type'): - intv.transposePitch(p, maxAccidental=None) - p2 = intv.transposePitch(p) - self.assertEqual(p2.nameWithOctave, 'B-20') - - - def testIntervalWithOneNoteGiven(self): - from music21 import interval - from music21 import note - noteC = note.Note('C4') - with self.assertRaises(ValueError): - i = interval.Interval(name='P4', noteStart=noteC) - i = interval.Interval(name='P4') - i.noteStart = noteC - self.assertEqual(i.noteEnd.nameWithOctave, 'F4') - noteF = i.noteEnd - - # giving noteStart and noteEnd and a name where the name does not match - # the notes is an exception. - with self.assertRaises(ValueError): - interval.Interval(name='d5', noteStart=noteC, noteEnd=noteF) - - # same with chromatic only intervals - with self.assertRaises(ValueError): - interval.Interval(chromatic=interval.ChromaticInterval(6), - noteStart=noteC, - noteEnd=noteF) - - # but these should work - i2 = interval.Interval(name='P4', noteStart=noteC, noteEnd=noteF) - self.assertIs(i2.noteStart, noteC) - self.assertIs(i2.noteEnd, noteF) - self.assertEqual(i2.name, 'P4') - self.assertEqual(i2.semitones, 5) - - i3 = interval.Interval(chromatic=interval.ChromaticInterval(5), - noteStart=noteC, - noteEnd=noteF) - self.assertIs(i3.noteStart, noteC) - self.assertIs(i3.noteEnd, noteF) - self.assertEqual(i3.name, 'P4') - self.assertEqual(i3.semitones, 5) - - - def testEmptyIntervalProperties(self): - ''' - As of v8, an empty Interval is equal to P1 - ''' - empty = DiatonicInterval() - self.assertEqual(empty.cents, 0.0) - - empty = Interval() - self.assertEqual(empty.cents, 0.0) - self.assertEqual(empty.intervalClass, 0) - self.assertEqual(empty.name, 'P1') +# ------------------------------------------------------------------------------ +# tests in test/test_interval # ------------------------------------------------------------------------------ # define presented order in documentation @@ -4453,8 +3997,7 @@ def testEmptyIntervalProperties(self): if __name__ == '__main__': - # sys.arg test options will be used in mainTest() import music21 - music21.mainTest(Test) + music21.mainTest() diff --git a/music21/note.py b/music21/note.py index 1abb3edbfd..54a79d74a1 100644 --- a/music21/note.py +++ b/music21/note.py @@ -2071,345 +2071,6 @@ def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) - def testLyricRepr(self): - from music21 import note - ly = note.Lyric() - self.assertEqual(repr(ly), '') - ly.text = 'hi' - self.assertEqual(repr(ly), "") - ly.identifier = 'verse' - self.assertEqual(repr(ly), "") - ly.text = None - self.assertEqual(repr(ly), "") - - def testComplex(self): - from music21 import note - from music21.duration import DurationTuple - note1 = note.Note() - note1.duration.clear() - d1 = DurationTuple('whole', 0, 4.0) - d2 = DurationTuple('quarter', 0, 1.0) - note1.duration.addDurationTuple(d1) - note1.duration.addDurationTuple(d2) - self.assertEqual(note1.duration.quarterLength, 5.0) - self.assertEqual(note1.duration.componentIndexAtQtrPosition(2), 0) - self.assertEqual(note1.duration.componentIndexAtQtrPosition(4), 1) - self.assertEqual(note1.duration.componentIndexAtQtrPosition(4.5), 1) - note1.duration.sliceComponentAtPosition(1.0) - - matchStr = "c'4~\nc'2.~\nc'4" - from music21.lily.translate import LilypondConverter - conv = LilypondConverter() - conv.appendM21ObjectToContext(note1) - outStr = str(conv.context).replace(' ', '').strip() - # print(outStr) - self.assertEqual(matchStr, outStr) - i = 0 - for thisNote in note1.splitAtDurations(): - matchSub = matchStr.split('\n')[i] # pylint: disable=use-maxsplit-arg - conv = LilypondConverter() - conv.appendM21ObjectToContext(thisNote) - outStr = str(conv.context).replace(' ', '').strip() - self.assertEqual(matchSub, outStr) - i += 1 - - def testNote(self): - from music21 import note - note2 = note.Rest() - self.assertTrue(note2.isRest) - note3 = note.Note() - note3.pitch.name = 'B-' - # not sure how to test not None - # self.assertFalse (note3.pitch.accidental, None) - self.assertEqual(note3.pitch.accidental.name, 'flat') - self.assertEqual(note3.pitch.pitchClass, 10) - - a5 = note.Note() - a5.name = 'A' - a5.octave = 5 - self.assertAlmostEqual(a5.pitch.frequency, 880.0) - self.assertEqual(a5.pitch.pitchClass, 9) - - def testCopyNote(self): - from music21 import note - a = note.Note() - a.quarterLength = 3.5 - a.name = 'D' - b = copy.deepcopy(a) - self.assertEqual(b.name, a.name) - - def testMusicXMLFermata(self): - from music21 import corpus - a = corpus.parse('bach/bwv5.7') - found = [] - for n in a.flatten().notesAndRests: - for obj in n.expressions: - if isinstance(obj, expressions.Fermata): - found.append(obj) - self.assertEqual(len(found), 24) - - def testNoteBeatProperty(self): - from music21 import meter - from music21 import note - from music21 import stream - - data = [ - ['3/4', 0.5, 6, [1.0, 1.5, 2.0, 2.5, 3.0, 3.5], - [1.0] * 6, ], - ['3/4', 0.25, 8, [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75], - [1.0] * 8], - ['3/2', 0.5, 8, [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75], - [2.0] * 8], - - ['6/8', 0.5, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], - [1.5] * 6], - ['9/8', 0.5, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], - [1.5] * 6], - ['12/8', 0.5, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], - [1.5] * 6], - - ['6/16', 0.25, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], - [0.75] * 6], - - ['5/4', 1, 5, [1.0, 2.0, 3.0, 4.0, 5.0], - [1.] * 5], - - ['2/8+3/8+2/8', 0.5, 6, [1.0, 1.5, 2.0, 2.33333, 2.66666, 3.0], - [1., 1., 1.5, 1.5, 1.5, 1.]], - - ] - - # one measure case - for tsStr, nQL, nCount, matchBeat, matchBeatDur in data: - n = note.Note() # need fully qualified name - n.quarterLength = nQL - m = stream.Measure() - m.timeSignature = meter.TimeSignature(tsStr) - m.repeatAppend(n, nCount) - - self.assertEqual(len(m), nCount + 1) - - # test matching beat proportion value - post = [m.notesAndRests[i].beat for i in range(nCount)] - for i in range(len(matchBeat)): - self.assertAlmostEqual(post[i], matchBeat[i], 4) - - # test getting beat duration - post = [m.notesAndRests[i].beatDuration.quarterLength for i in range(nCount)] - - for i in range(len(matchBeat)): - self.assertAlmostEqual(post[i], matchBeatDur[i], 4) - - # two measure case - for tsStr, nQL, nCount, matchBeat, matchBeatDur in data: - p = stream.Part() - n = note.Note() - n.quarterLength = nQL - - # m1 has time signature - m1 = stream.Measure() - m1.timeSignature = meter.TimeSignature(tsStr) - p.append(m1) - - # m2 does not have time signature - m2 = stream.Measure() - m2.repeatAppend(n, nCount) - self.assertEqual(len(m2), nCount) - self.assertEqual(len(m2.notesAndRests), nCount) - - p.append(m2) - - # test matching beat proportion value - post = [m2.notesAndRests[i].beat for i in range(nCount)] - for i in range(len(matchBeat)): - self.assertAlmostEqual(post[i], matchBeat[i], 4) - # test getting beat duration - post = [m2.notesAndRests[i].beatDuration.quarterLength for i in range(nCount)] - for i in range(len(matchBeat)): - self.assertAlmostEqual(post[i], matchBeatDur[i], 4) - - def testNoteBeatPropertyCorpus(self): - data = [['bach/bwv255', [4.0, 1.0, 2.5, 3.0, 4.0, 4.5, 1.0, 1.5]], - ['bach/bwv153.9', [1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 3.0, 1.0]] - ] - - for work, match in data: - from music21 import corpus - s = corpus.parse(work) - # always use tenor line - found = [] - for n in s.parts[2].flatten().notesAndRests: - n.lyric = n.beatStr - found.append(n.beat) - - for i in range(len(match)): - self.assertEqual(match[i], found[i]) - - # s.show() - - def testNoteEquality(self): - from music21 import articulations - from music21 import note - - n1 = note.Note('A#') - n2 = note.Note('G') - n3 = note.Note('A-') - n4 = note.Note('A#') - - self.assertNotEqual(n1, n2) - self.assertNotEqual(n1, n3) - self.assertEqual(n1, n4) - - # test durations with the same pitch - for x, y, match in [ - (1, 1, True), - (1, 0.5, False), - (1, 2, False), - (1, 1.5, False) - ]: - n1.quarterLength = x - n4.quarterLength = y - self.assertEqual(n1 == n4, match) # sub1 - - # test durations with different pitch - for x, y, match in [(1, 1, False), (1, 0.5, False), - (1, 2, False), (1, 1.5, False)]: - n1.quarterLength = x - n2.quarterLength = y - self.assertEqual(n1 == n2, match) # sub2 - - # same pitches different octaves - n1.quarterLength = 1.0 - n4.quarterLength = 1.0 - for x, y, match in [(4, 4, True), (3, 4, False), (2, 4, False)]: - n1.pitch.octave = x - n4.pitch.octave = y - self.assertEqual(n1 == n4, match) # sub4 - - # with and without ties - n1.pitch.octave = 4 - n4.pitch.octave = 4 - t1 = tie.Tie() - t2 = tie.Tie() - for x, y, match in [(t1, None, False), (t1, t2, True)]: - n1.tie = x - n4.tie = y - self.assertEqual(n1 == n4, match) # sub4 - - # with ties but different pitches - for n in [n1, n2, n3, n4]: - n.quarterLength = 1.0 - t1 = tie.Tie() - t2 = tie.Tie() - for a, b, match in [(n1, n2, False), (n1, n3, False), - (n2, n3, False), (n1, n4, True)]: - a.tie = t1 - b.tie = t2 - self.assertEqual(a == b, match) # sub5 - - # articulation groups - a1 = [articulations.Accent()] - a2 = [articulations.Accent(), articulations.StrongAccent()] - a3 = [articulations.StrongAccent(), articulations.Accent()] - a4 = [articulations.StrongAccent(), articulations.Accent(), - articulations.Tenuto()] - a5 = [articulations.Accent(), articulations.Tenuto(), - articulations.StrongAccent()] - - for a, b, c, d, match in [(n1, n4, a1, a1, True), - (n1, n2, a1, a1, False), (n1, n3, a1, a1, False), - # same pitch different orderings - (n1, n4, a2, a3, True), (n1, n4, a4, a5, True), - # different pitch same orderings - (n1, n2, a2, a3, False), (n1, n3, a4, a5, False), - ]: - a.articulations = c - b.articulations = d - self.assertEqual(a == b, match) # sub6 - - def testMetricalAccent(self): - from music21 import meter - from music21 import note - from music21 import stream - data = [ - ('4/4', 8, 0.5, [1.0, 0.125, 0.25, 0.125, 0.5, 0.125, 0.25, 0.125]), - ('3/4', 6, 0.5, [1.0, 0.25, 0.5, 0.25, 0.5, 0.25]), - ('6/8', 6, 0.5, [1.0, 0.25, 0.25, 0.5, 0.25, 0.25]), - - ('12/32', 12, 0.125, [1.0, 0.125, 0.125, 0.25, 0.125, 0.125, - 0.5, 0.125, 0.125, 0.25, 0.125, 0.125]), - - ('5/8', 10, 0.25, [1.0, 0.25, 0.5, 0.25, 0.5, 0.25, 0.5, 0.25, 0.5, 0.25]), - - # test notes that do not have defined accents - ('4/4', 16, 0.25, [1.0, 0.0625, 0.125, 0.0625, 0.25, 0.0625, 0.125, 0.0625, - 0.5, 0.0625, 0.125, 0.0625, 0.25, 0.0625, 0.125, 0.0625]), - ('4/4', 32, 0.125, [1.0, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625, - 0.25, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625, - 0.5, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625, - 0.25, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625]), - ] - - for tsStr, nCount, dur, match in data: - m = stream.Measure() - m.timeSignature = meter.TimeSignature(tsStr) - n = note.Note() - n.quarterLength = dur - m.repeatAppend(n, nCount) - - self.assertEqual([n.beatStrength for n in m.notesAndRests], match) - - def testTieContinue(self): - from music21 import note - from music21 import stream - - n1 = note.Note() - n1.tie = tie.Tie() - n1.tie.type = 'start' - - n2 = note.Note() - n2.tie = tie.Tie() - n2.tie.type = 'continue' - - n3 = note.Note() - n3.tie = tie.Tie() - n3.tie.type = 'stop' - - s = stream.Stream() - s.append([n1, n2, n3]) - - # need to test that this gets us a "continue" tie, but hard to test - # post musicxml processing - # s.show() - - def testVolumeA(self): - from music21 import note - v1 = volume.Volume() - - n1 = note.Note() - n2 = note.Note() - - n1.volume = v1 # can set as v1 has no client - self.assertEqual(n1.volume, v1) - self.assertEqual(n1.volume.client, n1) - - # object is created on demand - self.assertIsNot(n2.volume, v1) - self.assertIsNotNone(n2.volume) - - def testVolumeB(self): - from music21 import note - # manage deepcopying properly - n1 = note.Note() - - n1.volume.velocity = 100 - self.assertEqual(n1.volume.velocity, 100) - self.assertEqual(n1.volume.client, n1) - - n1Copy = copy.deepcopy(n1) - self.assertEqual(n1Copy.volume.velocity, 100) - self.assertEqual(n1Copy.volume.client, n1Copy) - # ------------------------------------------------------------------------------ # define presented order in documentation diff --git a/music21/pitch.py b/music21/pitch.py index 99468cd246..068f17f1d7 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -5224,7 +5224,7 @@ def getStringHarmonic(self, chordIn): ''' # Takes in a chord, finds the interval between the notes - from music21 import note + from music21 import note # pylint: disable=redefined-outer-name # only in TYPE_CHECKING from music21 import chord pitchList = chordIn.pitches diff --git a/music21/stream/core.py b/music21/stream/core.py index fc6a9326bc..3cf68d8fcc 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -38,6 +38,10 @@ from music21.stream.iterator import StreamIterator, RecursiveIterator +if t.TYPE_CHECKING: + from music21.stream import Stream + + class StreamCore(Music21Object): ''' Core aspects of a Stream's behavior. Any of these can change at any time. @@ -259,7 +263,7 @@ def coreElementsChanged( if self._derivation is not None: sdm = self._derivation.method if sdm in ('flat', 'semiflat'): - origin: stream.Stream = self._derivation.origin + origin: Stream = self._derivation.origin origin.clearCache() # may not always need to clear cache of all living sites, but may diff --git a/music21/test/__init__.py b/music21/test/__init__.py index 2385f1d51f..ab56566290 100644 --- a/music21/test/__init__.py +++ b/music21/test/__init__.py @@ -1,7 +1,11 @@ from __future__ import annotations from music21.test import test_base as base +from music21.test import test_clef as clef +from music21.test import test_expressions as expressions +from music21.test import test_interval as interval from music21.test import test_metadata as metadata +from music21.test import test_note as note from music21.test import test_pitch as pitch from music21.test import test_repeat as repeat diff --git a/music21/test/test_clef.py b/music21/test/test_clef.py new file mode 100644 index 0000000000..5f5ba15352 --- /dev/null +++ b/music21/test/test_clef.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from xml.etree.ElementTree import fromstring as El +import unittest + +from music21 import clef +from music21 import meter +from music21.musicxml.xmlToM21 import MeasureParser +from music21 import note +from music21 import stream + + +class Test(unittest.TestCase): + def testConversionClassMatch(self): + # need to get music21.clef.X, not X, because + # we are comparing the result to a translation outside + # clef.py + src = [ + [('G', 1, 0), clef.FrenchViolinClef], + [('G', 2, 0), clef.TrebleClef], + [('G', 2, -1), clef.Treble8vbClef], + [('G', 2, 1), clef.Treble8vaClef], + [('G', 3, 0), clef.GSopranoClef], + [('C', 1, 0), clef.SopranoClef], + [('C', 2, 0), clef.MezzoSopranoClef], + [('C', 3, 0), clef.AltoClef], + [('C', 4, 0), clef.TenorClef], + [('C', 5, 0), clef.CBaritoneClef], + [('F', 3, 0), clef.FBaritoneClef], + [('F', 4, 0), clef.BassClef], + [('F', 4, 1), clef.Bass8vaClef], + [('F', 4, -1), clef.Bass8vbClef], + [('F', 5, 0), clef.SubBassClef], + [('TAB', 5, 0), clef.TabClef] + ] + + MP = MeasureParser() + + for params, className in src: + sign, line, octaveChange = params + mxClef = El(r'' + + sign + '' + + str(line) + '' + + '' + + str(octaveChange) + + '') + c = MP.xmlToClef(mxClef) + + # environLocal.printDebug([type(c).__name__]) + + self.assertEqual(c.sign, params[0]) + self.assertEqual(c.line, params[1]) + self.assertEqual(c.octaveChange, params[2]) + self.assertIsInstance(c, className, + f'Failed Conversion of classes: {c} is not a {className}') + + def testContexts(self): + n1 = note.Note('C') + n1.offset = 10 + c1 = clef.AltoClef() + c1.offset = 0 + s1 = stream.Stream([c1, n1]) + + self.assertIs(s1.recurse().notes[0].getContextByClass(clef.Clef), c1) + # equally good: getContextsByClass(Clef)[0] + + del s1 + + n2 = note.Note('D') + n2.duration.type = 'whole' + n3 = note.Note('E') + n3.duration.type = 'whole' + ts1 = meter.TimeSignature('4/4') + s2 = stream.Stream() + s2.append(c1) + s2.append(ts1) + s2.append(n2) + s2.append(n3) + s2.makeMeasures() + self.assertIs(n2.getContextByClass(clef.Clef), c1) + + del s2 + + n4 = note.Note('F') + n4.duration.type = 'half' + n5 = note.Note('G') + n5.duration.type = 'half' + n6 = note.Note('A') + n6.duration.type = 'whole' + + ts2 = meter.TimeSignature('4/4') + bc1 = clef.BassClef() + tc1 = clef.TrebleClef() + + s3 = stream.Stream() + s3.append(bc1) + s3.append(ts2) + s3.append(n4) + s3.append(tc1) + s3.append(n5) + s3.append(n6) + s3.makeMeasures() + + self.assertIs(n4.getContextByClass(stream.Measure), n5.getContextByClass(stream.Measure)) + self.assertIs(n4.getContextByClass(clef.Clef), bc1) + self.assertIs(n5.getContextByClass(clef.Clef), tc1) + self.assertIs(n6.getContextByClass(clef.Clef), tc1) + + def testTabClefBeamDirections(self): + m = stream.Measure() + + n1 = note.Note(64, quarterLength=0.25) + n2 = note.Note(67, quarterLength=0.25) + + m.append(clef.TabClef()) + m.append(meter.TimeSignature('4/4')) + m.append(n1) + m.append(n2) + m.makeBeams(inPlace=True) + + self.assertEqual(m.notes[0].stemDirection, 'down') + + +if __name__ == '__main__': + import music21 + music21.mainTest(Test) diff --git a/music21/test/test_expressions.py b/music21/test/test_expressions.py new file mode 100644 index 0000000000..ca89b0420a --- /dev/null +++ b/music21/test/test_expressions.py @@ -0,0 +1,161 @@ + +from __future__ import annotations + +import unittest + +from music21 import chord +from music21 import clef +from music21 import expressions +from music21 import key +from music21 import meter +from music21.musicxml import m21ToXml +from music21 import note +from music21 import stream + + +class Test(unittest.TestCase): + + def testRealize(self): + n1 = note.Note('D4') + n1.quarterLength = 4 + n1.expressions.append(expressions.WholeStepMordent()) + expList = expressions.realizeOrnaments(n1) + st1 = stream.Stream() + st1.append(expList) + st1n = st1.notes + self.assertEqual(st1n[0].name, 'D') + self.assertEqual(st1n[0].quarterLength, 0.125) + self.assertEqual(st1n[1].name, 'C') + self.assertEqual(st1n[1].quarterLength, 0.125) + self.assertEqual(st1n[2].name, 'D') + self.assertEqual(st1n[2].quarterLength, 3.75) + + def testGetRepeatExpression(self): + te = expressions.TextExpression('lightly') + # no repeat expression is possible + self.assertEqual(te.getRepeatExpression(), None) + + te = expressions.TextExpression('d.c.') + self.assertEqual(str(te.getRepeatExpression()), + "") + re = te.getRepeatExpression() + self.assertEqual(re.getTextExpression().content, 'd.c.') + + te = expressions.TextExpression('DC al coda') + self.assertEqual(str(te.getRepeatExpression()), + "") + re = te.getRepeatExpression() + self.assertEqual(re.getTextExpression().content, 'DC al coda') + + te = expressions.TextExpression('DC al fine') + self.assertEqual(str(te.getRepeatExpression()), + "") + re = te.getRepeatExpression() + self.assertEqual(re.getTextExpression().content, 'DC al fine') + + te = expressions.TextExpression('ds al coda') + self.assertEqual(str(te.getRepeatExpression()), + "") + re = te.getRepeatExpression() + self.assertEqual(re.getTextExpression().content, 'ds al coda') + + te = expressions.TextExpression('d.s. al fine') + self.assertEqual(str(te.getRepeatExpression()), + "") + re = te.getRepeatExpression() + self.assertEqual(re.getTextExpression().content, 'd.s. al fine') + + def testExpandTurns(self): + p1 = stream.Part() + m1 = stream.Measure() + m2 = stream.Measure() + p1.append(clef.TrebleClef()) + p1.append(key.Key('F', 'major')) + p1.append(meter.TimeSignature('2/4')) + n1 = note.Note('C5', type='half') + turn0 = expressions.Turn() + n1.expressions.append(turn0) + n2 = note.Note('B4', type='quarter') + n2.duration.dots = 1 + + n2.expressions.append(expressions.InvertedTurn()) + m1.append(n1) + m2.append(key.KeySignature(5)) + m2.append(n2) + m2.append(note.Rest('eighth')) + p1.append(m1) + p1.append(m2) + realized1 = expressions.realizeOrnaments(n1) + realized2 = expressions.realizeOrnaments(n2) + self.assertEqual('C5 D5 C5 B-4 C5', ' '.join(n.pitch.nameWithOctave for n in realized1)) + self.assertEqual('B4 A#4 B4 C#5 B4', ' '.join(n.pitch.nameWithOctave for n in realized2)) + self.assertEqual(realized1[0].quarterLength, 1.0) + self.assertEqual(realized1[1].quarterLength, 0.25) + self.assertEqual(realized2[0].quarterLength, 0.5) + self.assertEqual(realized2[1].quarterLength, 0.25) + + turn0.quarterLength = 0.125 + realized1b = expressions.realizeOrnaments(n1) + self.assertEqual(realized1b[0].quarterLength, 1.5) + self.assertEqual(realized1b[1].quarterLength, 0.125) + + + def testExpandTrills(self): + p1 = stream.Part() + m1 = stream.Measure() + p1.append(clef.TrebleClef()) + p1.append(key.Key('D', 'major')) + p1.append(meter.TimeSignature('1/4')) + n1 = note.Note('E4', type='eighth') + n1.expressions.append(expressions.Trill()) + m1.append(n1) + p1.append(m1) + realized = expressions.realizeOrnaments(n1) + self.assertIsInstance(realized, list) + self.assertEqual(len(realized), 4) + self.assertIsInstance(realized[0], note.Note) + self.assertEqual(realized[0].quarterLength, 0.125) + self.assertEqual('E4 F#4 E4 F#4', ' '.join(n.pitch.nameWithOctave for n in realized)) + + + def testTrillExtensionA(self): + '''Test basic wave line creation and output, as well as passing + objects through make measure calls. + ''' + s = stream.Stream() + s.repeatAppend(note.Note(), 12) + n1 = s.notes[0] + n2 = s.notes[-1] + sp1 = expressions.TrillExtension(n1, n2) + s.append(sp1) + raw = m21ToXml.GeneralObjectExporter().parse(s) + self.assertEqual(raw.count(b'wavy-line'), 2) + + s = stream.Stream() + s.repeatAppend(chord.Chord(['c-3', 'g4']), 12) + n1 = s.notes[0] + n2 = s.notes[-1] + sp1 = expressions.TrillExtension(n1, n2) + s.append(sp1) + raw = m21ToXml.GeneralObjectExporter().parse(s) + # s.show() + self.assertEqual(raw.count(b'wavy-line'), 2) + + def testUnpitchedUnsupported(self): + unp = note.Unpitched() + mord = expressions.Mordent() + with self.assertRaises(TypeError): + mord.realize(unp) # type: ignore + + +# class TestExternal(unittest.TestCase): +# def testCPEBachRealizeOrnaments(self): +# from music21 import corpus +# cpe = corpus.parse('cpebach/h186').parts[0].measures(1, 4) +# cpe2 = cpe.realizeOrnaments() +# cpe2.show() + + +if __name__ == '__main__': + import music21 + music21.mainTest(Test) diff --git a/music21/test/test_interval.py b/music21/test/test_interval.py new file mode 100644 index 0000000000..d222181f77 --- /dev/null +++ b/music21/test/test_interval.py @@ -0,0 +1,453 @@ +from __future__ import annotations + +import unittest + +from music21 import corpus +from music21 import interval +from music21 import note +from music21 import pitch + + +class Test(unittest.TestCase): + + def testConstructorPitches(self): + p1 = pitch.Pitch('A4') + p2 = pitch.Pitch('E5') + intv = interval.Interval(p1, p2) + self.assertEqual(intv.name, 'P5') + self.assertIs(intv.pitchStart, p1) + self.assertIs(intv.pitchEnd, p2) + + # same with keywords, but reversed + intv = interval.Interval(pitchStart=p2, pitchEnd=p1) + self.assertEqual(intv.name, 'P5') + self.assertEqual(intv.directedName, 'P-5') + self.assertIs(intv.pitchStart, p2) + self.assertIs(intv.pitchEnd, p1) + + def testFirst(self): + n1 = note.Note() + n2 = note.Note() + + n1.step = 'C' + n1.octave = 4 + + n2.step = 'B' + n2.octave = 5 + n2.pitch.accidental = pitch.Accidental('-') + + int0 = interval.Interval(noteStart=n1, noteEnd=n2) + dInt0 = int0.diatonic + gInt0 = dInt0.generic + + self.assertFalse(gInt0.isDiatonicStep) + self.assertTrue(gInt0.isSkip) + + n1.pitch.accidental = pitch.Accidental('#') + int1 = interval.Interval(noteStart=n1, noteEnd=n2) + + # returns music21.interval.ChromaticInterval object + cInt1 = interval.notesToChromatic(n1, n2) + self.assertIsInstance(cInt1, interval.ChromaticInterval) + cInt2 = int1.chromatic # returns same as cInt1 -- a different way of thinking of things + self.assertEqual(cInt1.semitones, cInt2.semitones) + + self.assertEqual(int1.simpleNiceName, 'Diminished Seventh') + + self.assertEqual(int1.directedSimpleNiceName, 'Ascending Diminished Seventh') + self.assertEqual(int1.name, 'd14') + self.assertEqual(int1.specifier, interval.Specifier.DIMINISHED) + + # returns same as gInt1 -- just a different way of thinking of things + dInt1 = int1.diatonic + gInt1 = dInt1.generic + + self.assertEqual(gInt1.directed, 14) + self.assertEqual(gInt1.undirected, 14) + self.assertEqual(gInt1.simpleDirected, 7) + self.assertEqual(gInt1.simpleUndirected, 7) + + self.assertEqual(cInt1.semitones, 21) + self.assertEqual(cInt1.undirected, 21) + self.assertEqual(cInt1.mod12, 9) + self.assertEqual(cInt1.intervalClass, 3) + + n4 = note.Note() + n4.step = 'D' + n4.octave = 3 + n4.pitch.accidental = '-' + + # n3 = interval.transposePitch(n4, 'AA8') + # if n3.pitch.accidental is not None: + # print(n3.step, n3.pitch.accidental.name, n3.octave) + # else: + # print(n3.step, n3.octave) + # print(n3.name) + # print() + + cI = interval.ChromaticInterval(-14) + self.assertEqual(cI.semitones, -14) + self.assertEqual(cI.cents, -1400) + self.assertEqual(cI.undirected, 14) + self.assertEqual(cI.mod12, 10) + self.assertEqual(cI.intervalClass, 2) + + lowB = note.Note() + lowB.name = 'B' + highBb = note.Note() + highBb.name = 'B-' + highBb.octave = 5 + dimOct = interval.Interval(lowB, highBb) + self.assertEqual(dimOct.niceName, 'Diminished Octave') + + noteA1 = note.Note() + noteA1.name = 'E-' + noteA1.octave = 4 + noteA2 = note.Note() + noteA2.name = 'F#' + noteA2.octave = 5 + intervalA1 = interval.Interval(noteA1, noteA2) + + noteA3 = note.Note() + noteA3.name = 'D' + noteA3.octave = 1 + + noteA4 = interval.transposeNote(noteA3, intervalA1) + self.assertEqual(noteA4.name, 'E#') + self.assertEqual(noteA4.octave, 2) + + interval1 = interval.Interval('-P5') + + n5 = interval.transposeNote(n4, interval1) + n6 = interval.transposeNote(n4, 'P-5') + self.assertEqual(n5.name, 'G-') + self.assertEqual(n6.name, n5.name) + + # same thing using newer syntax: + + interval1 = interval.Interval('P-5') + + n5 = interval.transposeNote(n4, interval1) + self.assertEqual(n5.name, 'G-') + n7 = note.Note() + n8 = interval.transposeNote(n7, 'P8') + self.assertEqual(n8.name, 'C') + self.assertEqual(n8.octave, 5) + + n9 = interval.transposeNote(n7, 'm7') # should be B- + self.assertEqual(n9.name, 'B-') + self.assertEqual(n9.octave, 4) + n10 = interval.transposeNote(n7, 'dd-2') # should be B## + self.assertEqual(n10.name, 'B##') + self.assertEqual(n10.octave, 3) + + # test getWrittenHigherNote functions + nE = note.Note('E') + nESharp = note.Note('E#') + nFFlat = note.Note('F-') + nF1 = note.Note('F') + nF2 = note.Note('F') + + higher1 = interval.getWrittenHigherNote(nE, nESharp) + higher2 = interval.getWrittenHigherNote(nESharp, nFFlat) + higher3 = interval.getWrittenHigherNote(nF1, nF2) + + self.assertEqual(higher1, nESharp) + self.assertEqual(higher2, nFFlat) + self.assertEqual(higher3, nF1) # in case of ties, first is returned + + higher4 = interval.getAbsoluteHigherNote(nE, nESharp) + higher5 = interval.getAbsoluteHigherNote(nESharp, nFFlat) + higher6 = interval.getAbsoluteHigherNote(nESharp, nF1) + higher7 = interval.getAbsoluteHigherNote(nF1, nESharp) + + self.assertEqual(higher4, nESharp) + self.assertEqual(higher5, nESharp) + self.assertEqual(higher6, nESharp) + self.assertEqual(higher7, nF1) + + lower1 = interval.getWrittenLowerNote(nESharp, nE) + lower2 = interval.getWrittenLowerNote(nFFlat, nESharp) + lower3 = interval.getWrittenLowerNote(nF1, nF2) + + self.assertEqual(lower1, nE) + self.assertEqual(lower2, nESharp) + self.assertEqual(lower3, nF1) # still returns first. + + lower4 = interval.getAbsoluteLowerNote(nESharp, nE) + lower5 = interval.getAbsoluteLowerNote(nFFlat, nESharp) + lower6 = interval.getAbsoluteLowerNote(nESharp, nF1) + + self.assertEqual(lower4, nE) + self.assertEqual(lower5, nFFlat) + self.assertEqual(lower6, nESharp) + + middleC = note.Note() + lowerC = note.Note() + lowerC.octave = 3 + descendingOctave = interval.Interval(middleC, lowerC) + self.assertEqual(descendingOctave.generic.simpleDirected, 1) + # no descending unisons ever + self.assertEqual(descendingOctave.generic.semiSimpleDirected, -8) + # no descending unisons ever + self.assertEqual(descendingOctave.directedName, 'P-8') + self.assertEqual(descendingOctave.directedSimpleName, 'P1') + + lowerG = note.Note() + lowerG.name = 'G' + lowerG.octave = 3 + descendingFourth = interval.Interval(middleC, lowerG) + self.assertEqual(descendingFourth.diatonic.directedNiceName, + 'Descending Perfect Fourth') + self.assertEqual(descendingFourth.diatonic.directedSimpleName, 'P-4') + self.assertEqual(descendingFourth.diatonic.simpleName, 'P4') + self.assertEqual(descendingFourth.diatonic.mod7, 'P5') + + perfectFifth = descendingFourth.complement + self.assertEqual(perfectFifth.niceName, 'Perfect Fifth') + self.assertEqual(perfectFifth.diatonic.simpleName, 'P5') + self.assertEqual(perfectFifth.diatonic.mod7, 'P5') + self.assertEqual(perfectFifth.complement.niceName, 'Perfect Fourth') + + def testCreateIntervalFromPitch(self): + p1 = pitch.Pitch('c') + p2 = pitch.Pitch('g') + i = interval.Interval(p1, p2) + self.assertEqual(i.intervalClass, 5) + + def testTransposeImported(self): + def collectAccidentalDisplayStatus(s_inner): + post = [] + for e in s_inner.flatten().notes: + if e.pitch.accidental is not None: + post.append(e.pitch.accidental.displayStatus) + else: # mark as not having an accidental + post.append('x') + return post + + s = corpus.parse('bach/bwv66.6') + # this has accidentals in measures 2 and 6 + sSub = s.parts[3].measures(2, 6) + + self.assertEqual(collectAccidentalDisplayStatus(sSub), + ['x', False, 'x', 'x', True, False, 'x', False, False, False, + False, False, False, 'x', 'x', 'x', False, False, False, + 'x', 'x', 'x', 'x', True, False]) + + sTransposed = sSub.flatten().transpose('p5') + # sTransposed.show() + + self.assertEqual(collectAccidentalDisplayStatus(sTransposed), + ['x', None, 'x', 'x', None, None, None, None, None, + None, None, None, None, 'x', None, None, None, None, + None, 'x', 'x', 'x', None, None, None]) + + def testIntervalMicrotonesA(self): + i = interval.Interval('m3') + self.assertEqual(i.chromatic.cents, 300) + self.assertEqual(i.cents, 300.0) + + i = interval.Interval('p5') + self.assertEqual(i.chromatic.cents, 700) + self.assertEqual(i.cents, 700.0) + + i = interval.Interval(8) + self.assertEqual(i.chromatic.cents, 800) + self.assertEqual(i.cents, 800.0) + + i = interval.Interval(8.5) + self.assertEqual(i.chromatic.cents, 850.0) + self.assertEqual(i.cents, 850.0) + + i = interval.Interval(5.25) # a sharp p4 + self.assertEqual(i.cents, 525.0) + # we can subtract the two to get an offset + self.assertEqual(i.cents, 525.0) + self.assertEqual(str(i), '') + self.assertEqual(i._diatonicIntervalCentShift(), 25) + + i = interval.Interval(4.75) # a flat p4 + self.assertEqual(str(i), '') + self.assertEqual(i._diatonicIntervalCentShift(), -25) + + i = interval.Interval(4.48) # a sharp M3 + self.assertEqual(str(i), '') + self.assertAlmostEqual(i._diatonicIntervalCentShift(), 48.0) + + i = interval.Interval(4.5) # a sharp M3 + self.assertEqual(str(i), '') + self.assertAlmostEqual(i._diatonicIntervalCentShift(), 50.0) + + i = interval.Interval(5.25) # a sharp p4 + p1 = pitch.Pitch('c4') + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'F4(+25c)') + + i = interval.Interval(5.80) # a sharp p4 + p1 = pitch.Pitch('c4') + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'F#4(-20c)') + + i = interval.Interval(6.00) # an exact Tritone + p1 = pitch.Pitch('c4') + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'F#4') + + i = interval.Interval(5) # a chromatic p4 + p1 = pitch.Pitch('c4') + p1.microtone = 10 # c+20 + self.assertEqual(str(p1), 'C4(+10c)') + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'F4(+10c)') + + i = interval.Interval(7.20) # a sharp P5 + p1 = pitch.Pitch('c4') + p1.microtone = -20 # c+20 + self.assertEqual(str(p1), 'C4(-20c)') + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'G4') + + i = interval.Interval(7.20) # a sharp P5 + p1 = pitch.Pitch('c4') + p1.microtone = 80 # c+20 + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'G#4') + + i = interval.Interval(0.20) # a sharp unison + p1 = pitch.Pitch('e4') + p1.microtone = 10 + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'E~4(-20c)') + + i = interval.Interval(0.05) # a tiny bit sharp unison + p1 = pitch.Pitch('e4') + p1.microtone = 5 + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'E4(+10c)') + + i = interval.Interval(12.05) # a tiny bit sharp octave + p1 = pitch.Pitch('e4') + p1.microtone = 5 + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'E5(+10c)') + + i = interval.Interval(11.85) # a flat octave + p1 = pitch.Pitch('e4') + p1.microtone = 5 + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'E5(-10c)') + + i = interval.Interval(11.85) # a flat octave + p1 = pitch.Pitch('e4') + p1.microtone = -20 + p2 = i.transposePitch(p1) + self.assertEqual(str(p2), 'E`5(+15c)') + + def testIntervalMicrotonesB(self): + i = interval.Interval(note.Note('c4'), note.Note('c#4')) + self.assertEqual(str(i), '') + + i = interval.Interval(note.Note('c4'), note.Note('c~4')) + self.assertEqual(str(i), '') + + def testDescendingAugmentedUnison(self): + ns = note.Note('C4') + ne = note.Note('C-4') + i = interval.Interval(noteStart=ns, noteEnd=ne) + directedNiceName = i.directedNiceName + self.assertEqual(directedNiceName, 'Descending Diminished Unison') + + def testTransposeWithChromaticInterval(self): + ns = note.Note('C4') + i = interval.ChromaticInterval(5) + n2 = ns.transpose(i) + self.assertEqual(n2.nameWithOctave, 'F4') + + ns = note.Note('B#3') + i = interval.ChromaticInterval(5) + n2 = ns.transpose(i) + self.assertEqual(n2.nameWithOctave, 'F4') + + def testTransposeImplicit(self): + p = pitch.Pitch('D#4') + i = interval.Interval(0) + self.assertTrue(i.implicitDiatonic) + p2 = i.transposePitch(p) + self.assertEqual(p.ps, p2.ps) + self.assertNotEqual(p.name, p2.name) + + def testRepeatedTransposePitch(self): + p = pitch.Pitch('C1') + intv = interval.Interval('P5') + out = [] + for _ in range(33): + out.append(p.nameWithOctave) + intv.transposePitch(p, maxAccidental=None, inPlace=True) + self.assertEqual( + out, + ['C1', 'G1', 'D2', 'A2', 'E3', 'B3', 'F#4', + 'C#5', 'G#5', 'D#6', 'A#6', 'E#7', 'B#7', 'F##8', + 'C##9', 'G##9', 'D##10', 'A##10', 'E##11', 'B##11', 'F###12', + 'C###13', 'G###13', 'D###14', 'A###14', 'E###15', 'B###15', 'F####16', + 'C####17', 'G####17', 'D####18', 'A####18', 'E####19', + ] + ) + with self.assertRaisesRegex(pitch.AccidentalException, + '5.0 is not a supported accidental type'): + intv.transposePitch(p, maxAccidental=None) + p2 = intv.transposePitch(p) + self.assertEqual(p2.nameWithOctave, 'B-20') + + + def testIntervalWithOneNoteGiven(self): + noteC = note.Note('C4') + with self.assertRaises(ValueError): + i = interval.Interval(name='P4', noteStart=noteC) + i = interval.Interval(name='P4') + i.noteStart = noteC + self.assertEqual(i.noteEnd.nameWithOctave, 'F4') + noteF = i.noteEnd + + # giving noteStart and noteEnd and a name where the name does not match + # the notes is an exception. + with self.assertRaises(ValueError): + interval.Interval(name='d5', noteStart=noteC, noteEnd=noteF) + + # same with chromatic only intervals + with self.assertRaises(ValueError): + interval.Interval(chromatic=interval.ChromaticInterval(6), + noteStart=noteC, + noteEnd=noteF) + + # but these should work + i2 = interval.Interval(name='P4', noteStart=noteC, noteEnd=noteF) + self.assertIs(i2.noteStart, noteC) + self.assertIs(i2.noteEnd, noteF) + self.assertEqual(i2.name, 'P4') + self.assertEqual(i2.semitones, 5) + + i3 = interval.Interval(chromatic=interval.ChromaticInterval(5), + noteStart=noteC, + noteEnd=noteF) + self.assertIs(i3.noteStart, noteC) + self.assertIs(i3.noteEnd, noteF) + self.assertEqual(i3.name, 'P4') + self.assertEqual(i3.semitones, 5) + + + def testEmptyIntervalProperties(self): + ''' + As of v8, an empty Interval is equal to P1 + ''' + empty = interval.DiatonicInterval() + self.assertEqual(empty.cents, 0.0) + + empty = interval.Interval() + self.assertEqual(empty.cents, 0.0) + self.assertEqual(empty.intervalClass, 0) + self.assertEqual(empty.name, 'P1') + + +if __name__ == '__main__': + import music21 + music21.mainTest(Test) diff --git a/music21/test/test_note.py b/music21/test/test_note.py new file mode 100644 index 0000000000..6b376023f2 --- /dev/null +++ b/music21/test/test_note.py @@ -0,0 +1,337 @@ +from __future__ import annotations + +import copy +import unittest + +from music21 import articulations +from music21 import corpus +from music21.duration import DurationTuple +from music21 import expressions +from music21.lily.translate import LilypondConverter +from music21 import meter +from music21 import note +from music21 import stream +from music21 import tie +from music21 import volume + +class Test(unittest.TestCase): + def testLyricRepr(self): + ly = note.Lyric() + self.assertEqual(repr(ly), '') + ly.text = 'hi' + self.assertEqual(repr(ly), "") + ly.identifier = 'verse' + self.assertEqual(repr(ly), "") + ly.text = None + self.assertEqual(repr(ly), "") + + def testComplex(self): + note1 = note.Note() + note1.duration.clear() + d1 = DurationTuple('whole', 0, 4.0) + d2 = DurationTuple('quarter', 0, 1.0) + note1.duration.addDurationTuple(d1) + note1.duration.addDurationTuple(d2) + self.assertEqual(note1.duration.quarterLength, 5.0) + self.assertEqual(note1.duration.componentIndexAtQtrPosition(2), 0) + self.assertEqual(note1.duration.componentIndexAtQtrPosition(4), 1) + self.assertEqual(note1.duration.componentIndexAtQtrPosition(4.5), 1) + note1.duration.sliceComponentAtPosition(1.0) + + matchStr = "c'4~\nc'2.~\nc'4" + conv = LilypondConverter() + conv.appendM21ObjectToContext(note1) + outStr = str(conv.context).replace(' ', '').strip() + # print(outStr) + self.assertEqual(matchStr, outStr) + i = 0 + for thisNote in note1.splitAtDurations(): + matchSub = matchStr.split('\n')[i] # pylint: disable=use-maxsplit-arg + conv = LilypondConverter() + conv.appendM21ObjectToContext(thisNote) + outStr = str(conv.context).replace(' ', '').strip() + self.assertEqual(matchSub, outStr) + i += 1 + + def testNote(self): + note2 = note.Rest() + self.assertTrue(note2.isRest) + note3 = note.Note() + note3.pitch.name = 'B-' + # not sure how to test not None + # self.assertFalse (note3.pitch.accidental, None) + self.assertEqual(note3.pitch.accidental.name, 'flat') + self.assertEqual(note3.pitch.pitchClass, 10) + + a5 = note.Note() + a5.name = 'A' + a5.octave = 5 + self.assertAlmostEqual(a5.pitch.frequency, 880.0) + self.assertEqual(a5.pitch.pitchClass, 9) + + def testCopyNote(self): + a = note.Note() + a.quarterLength = 3.5 + a.name = 'D' + b = copy.deepcopy(a) + self.assertEqual(b.name, a.name) + + def testMusicXMLFermata(self): + a = corpus.parse('bach/bwv5.7') + found = [] + for n in a.flatten().notesAndRests: + for obj in n.expressions: + if isinstance(obj, expressions.Fermata): + found.append(obj) + self.assertEqual(len(found), 24) + + def testNoteBeatProperty(self): + data = [ + ['3/4', 0.5, 6, [1.0, 1.5, 2.0, 2.5, 3.0, 3.5], + [1.0] * 6, ], + ['3/4', 0.25, 8, [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75], + [1.0] * 8], + ['3/2', 0.5, 8, [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75], + [2.0] * 8], + + ['6/8', 0.5, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], + [1.5] * 6], + ['9/8', 0.5, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], + [1.5] * 6], + ['12/8', 0.5, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], + [1.5] * 6], + + ['6/16', 0.25, 6, [1.0, 1.3333, 1.66666, 2.0, 2.3333, 2.666666], + [0.75] * 6], + + ['5/4', 1, 5, [1.0, 2.0, 3.0, 4.0, 5.0], + [1.] * 5], + + ['2/8+3/8+2/8', 0.5, 6, [1.0, 1.5, 2.0, 2.33333, 2.66666, 3.0], + [1., 1., 1.5, 1.5, 1.5, 1.]], + + ] + + # one measure case + for tsStr, nQL, nCount, matchBeat, matchBeatDur in data: + n = note.Note() # need fully qualified name + n.quarterLength = nQL + m = stream.Measure() + m.timeSignature = meter.TimeSignature(tsStr) + m.repeatAppend(n, nCount) + + self.assertEqual(len(m), nCount + 1) + + # test matching beat proportion value + post = [m.notesAndRests[i].beat for i in range(nCount)] + for i in range(len(matchBeat)): + self.assertAlmostEqual(post[i], matchBeat[i], 4) + + # test getting beat duration + post = [m.notesAndRests[i].beatDuration.quarterLength for i in range(nCount)] + + for i in range(len(matchBeat)): + self.assertAlmostEqual(post[i], matchBeatDur[i], 4) + + # two measure case + for tsStr, nQL, nCount, matchBeat, matchBeatDur in data: + p = stream.Part() + n = note.Note() + n.quarterLength = nQL + + # m1 has time signature + m1 = stream.Measure() + m1.timeSignature = meter.TimeSignature(tsStr) + p.append(m1) + + # m2 does not have time signature + m2 = stream.Measure() + m2.repeatAppend(n, nCount) + self.assertEqual(len(m2), nCount) + self.assertEqual(len(m2.notesAndRests), nCount) + + p.append(m2) + + # test matching beat proportion value + post = [m2.notesAndRests[i].beat for i in range(nCount)] + for i in range(len(matchBeat)): + self.assertAlmostEqual(post[i], matchBeat[i], 4) + # test getting beat duration + post = [m2.notesAndRests[i].beatDuration.quarterLength for i in range(nCount)] + for i in range(len(matchBeat)): + self.assertAlmostEqual(post[i], matchBeatDur[i], 4) + + def testNoteBeatPropertyCorpus(self): + data = [['bach/bwv255', [4.0, 1.0, 2.5, 3.0, 4.0, 4.5, 1.0, 1.5]], + ['bach/bwv153.9', [1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 3.0, 1.0]] + ] + + for work, match in data: + s = corpus.parse(work) + # always use tenor line + found = [] + for n in s.parts[2].flatten().notesAndRests: + n.lyric = n.beatStr + found.append(n.beat) + + for i in range(len(match)): + self.assertEqual(match[i], found[i]) + + # s.show() + + def testNoteEquality(self): + n1 = note.Note('A#') + n2 = note.Note('G') + n3 = note.Note('A-') + n4 = note.Note('A#') + + self.assertNotEqual(n1, n2) + self.assertNotEqual(n1, n3) + self.assertEqual(n1, n4) + + # test durations with the same pitch + for x, y, match in [ + (1, 1, True), + (1, 0.5, False), + (1, 2, False), + (1, 1.5, False) + ]: + n1.quarterLength = x + n4.quarterLength = y + self.assertEqual(n1 == n4, match) # sub1 + + # test durations with different pitch + for x, y, match in [(1, 1, False), (1, 0.5, False), + (1, 2, False), (1, 1.5, False)]: + n1.quarterLength = x + n2.quarterLength = y + self.assertEqual(n1 == n2, match) # sub2 + + # same pitches different octaves + n1.quarterLength = 1.0 + n4.quarterLength = 1.0 + for x, y, match in [(4, 4, True), (3, 4, False), (2, 4, False)]: + n1.pitch.octave = x + n4.pitch.octave = y + self.assertEqual(n1 == n4, match) # sub4 + + # with and without ties + n1.pitch.octave = 4 + n4.pitch.octave = 4 + t1 = tie.Tie() + t2 = tie.Tie() + for x, y, match in [(t1, None, False), (t1, t2, True)]: + n1.tie = x + n4.tie = y + self.assertEqual(n1 == n4, match) # sub4 + + # with ties but different pitches + for n in [n1, n2, n3, n4]: + n.quarterLength = 1.0 + t1 = tie.Tie() + t2 = tie.Tie() + for a, b, match in [(n1, n2, False), (n1, n3, False), + (n2, n3, False), (n1, n4, True)]: + a.tie = t1 + b.tie = t2 + self.assertEqual(a == b, match) # sub5 + + # articulation groups + a1 = [articulations.Accent()] + a2 = [articulations.Accent(), articulations.StrongAccent()] + a3 = [articulations.StrongAccent(), articulations.Accent()] + a4 = [articulations.StrongAccent(), articulations.Accent(), + articulations.Tenuto()] + a5 = [articulations.Accent(), articulations.Tenuto(), + articulations.StrongAccent()] + + for a, b, c, d, match in [(n1, n4, a1, a1, True), + (n1, n2, a1, a1, False), (n1, n3, a1, a1, False), + # same pitch different orderings + (n1, n4, a2, a3, True), (n1, n4, a4, a5, True), + # different pitch same orderings + (n1, n2, a2, a3, False), (n1, n3, a4, a5, False), + ]: + a.articulations = c + b.articulations = d + self.assertEqual(a == b, match) # sub6 + + def testMetricalAccent(self): + data = [ + ('4/4', 8, 0.5, [1.0, 0.125, 0.25, 0.125, 0.5, 0.125, 0.25, 0.125]), + ('3/4', 6, 0.5, [1.0, 0.25, 0.5, 0.25, 0.5, 0.25]), + ('6/8', 6, 0.5, [1.0, 0.25, 0.25, 0.5, 0.25, 0.25]), + + ('12/32', 12, 0.125, [1.0, 0.125, 0.125, 0.25, 0.125, 0.125, + 0.5, 0.125, 0.125, 0.25, 0.125, 0.125]), + + ('5/8', 10, 0.25, [1.0, 0.25, 0.5, 0.25, 0.5, 0.25, 0.5, 0.25, 0.5, 0.25]), + + # test notes that do not have defined accents + ('4/4', 16, 0.25, [1.0, 0.0625, 0.125, 0.0625, 0.25, 0.0625, 0.125, 0.0625, + 0.5, 0.0625, 0.125, 0.0625, 0.25, 0.0625, 0.125, 0.0625]), + ('4/4', 32, 0.125, [1.0, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625, + 0.25, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625, + 0.5, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625, + 0.25, 0.0625, 0.0625, 0.0625, 0.125, 0.0625, 0.0625, 0.0625]), + ] + + for tsStr, nCount, dur, match in data: + m = stream.Measure() + m.timeSignature = meter.TimeSignature(tsStr) + n = note.Note() + n.quarterLength = dur + m.repeatAppend(n, nCount) + + self.assertEqual([n.beatStrength for n in m.notesAndRests], match) + + def testTieContinue(self): + n1 = note.Note() + n1.tie = tie.Tie() + n1.tie.type = 'start' + + n2 = note.Note() + n2.tie = tie.Tie() + n2.tie.type = 'continue' + + n3 = note.Note() + n3.tie = tie.Tie() + n3.tie.type = 'stop' + + s = stream.Stream() + s.append([n1, n2, n3]) + + # need to test that this gets us a "continue" tie, but hard to test + # post musicxml processing + # s.show() + + def testVolumeA(self): + v1 = volume.Volume() + + n1 = note.Note() + n2 = note.Note() + + n1.volume = v1 # can set as v1 has no client + self.assertEqual(n1.volume, v1) + self.assertEqual(n1.volume.client, n1) + + # object is created on demand + self.assertIsNot(n2.volume, v1) + self.assertIsNotNone(n2.volume) + + def testVolumeB(self): + # manage deepcopying properly + n1 = note.Note() + + n1.volume.velocity = 100 + self.assertEqual(n1.volume.velocity, 100) + self.assertEqual(n1.volume.client, n1) + + n1Copy = copy.deepcopy(n1) + self.assertEqual(n1Copy.volume.velocity, 100) + self.assertEqual(n1Copy.volume.client, n1Copy) + + +if __name__ == '__main__': + import music21 + music21.mainTest(Test) diff --git a/music21/tree/fromStream.py b/music21/tree/fromStream.py index 7544d145ea..bffdeb7f3c 100644 --- a/music21/tree/fromStream.py +++ b/music21/tree/fromStream.py @@ -90,7 +90,7 @@ def listOfTreesByClass( Changed in v8: it is now a stickler that classLists must be sequences of sequences, such as tuples of tuples. ''' - from music21 import stream + from music21 import stream # pylint: disable=redefined-outer-name # only in TYPE_CHECKING if currentParentage is None: currentParentage = (inputStream,)