Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions music21/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -3460,7 +3460,14 @@ def _diatonicTransposePitch(self,
pitch1 = p
pitch2 = copy.deepcopy(pitch1)
oldDiatonicNum = pitch1.diatonicNoteNum
# centsOrigin = pitch1.microtone.cents # unused!!
# Only carry pitch1's microtone through when the interval is not itself
# microtonal: a microtonal interval already supplies the result's cents
# (e.g. scale realization), so preserving here would double-count.
intervalIsMicrotonal = self.chromatic.semitones != int(self.chromatic.semitones)
if not pitch1.isTwelveTone() and not intervalIsMicrotonal:
centsOrigin = pitch1.microtone.cents
else:
centsOrigin = 0.0
distanceToMove = self.diatonic.generic.staffDistance

newDiatonicNumber = oldDiatonicNum + distanceToMove
Expand All @@ -3473,9 +3480,15 @@ def _diatonicTransposePitch(self,
# if this is not set to None then terrible things happen
pitch2.microtone = None # type: ignore

# We have the right note name but not the right accidental
interval2 = Interval(pitch1, pitch2)
# halfStepsToFix already has any microtones
# We have the right note name but not the right accidental. Measure it
# from pitch1 without its microtone (reapplied to the result below); a
# microtonal endpoint here yields a fractional chromatic with no
# interval specifier.
pitch1ForAccidental = pitch1
if centsOrigin:
pitch1ForAccidental = copy.deepcopy(pitch1)
pitch1ForAccidental.microtone = None
interval2 = Interval(pitch1ForAccidental, pitch2)
halfStepsToFix = self.chromatic.semitones - interval2.chromatic.semitones

# environLocal.printDebug(['self', self, 'halfStepsToFix', halfStepsToFix,
Expand Down Expand Up @@ -3527,6 +3540,10 @@ def _diatonicTransposePitch(self,
and oldPitch2Accidental.name == 'natural'):
pitch2.accidental = oldPitch2Accidental

if centsOrigin:
# restore the cents peeled off halfStepsToFix above
pitch2.microtone = pitch2.microtone.cents + centsOrigin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pitch2.microtone.cents = ...


if useImplicitOctave:
pitch2.octave = None

Expand Down
45 changes: 45 additions & 0 deletions music21/test/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,51 @@ def testIntervalMicrotonesB(self):
i = interval.Interval(note.Note('c4'), note.Note('c~4'))
self.assertEqual(str(i), '<music21.interval.Interval A1 (-50c)>')

def testTransposeMicrotonePreserved(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good test

# A microtone (cents offset) must survive transposition unchanged and
# must not be turned into a spurious quarter-tone accidental. Formerly
# the source microtone leaked into the accidental computation, so e.g.
# C4(+30c) transposed by a unison became C~4(-20c), or larger microtones
# raised AccidentalException.

# Transposing by a perfect unison is the identity.
p = pitch.Pitch('C4')
p.microtone = 30
out = p.transpose(interval.Interval('P1'))
self.assertEqual(out.nameWithOctave, 'C4')
self.assertEqual(out.microtone.cents, 30)

# A formerly-crashing case: G#5(+50c) transposed by an augmented unison.
p = pitch.Pitch('G#5')
p.microtone = 50
out = p.transpose(interval.Interval('A1'))
self.assertEqual(out.nameWithOctave, 'G##5')
self.assertEqual(out.microtone.cents, 50)

# A quarter-tone accidental and a microtone both survive a unison.
p = pitch.Pitch('C~4')
p.microtone = 30
out = p.transpose(interval.Interval('P1'))
self.assertEqual(out.name, 'C~')
self.assertEqual(out.microtone.cents, 30)

# Across intervals and microtone magnitudes: the cents are preserved,
# the sounding pitch moves by exactly the interval, and no quarter-tone
# accidental is introduced on an integer-semitone interval.
for iName in ['P1', 'm2', 'M3', 'P5', 'm7', 'P8', 'A4', 'd5']:
iv = interval.Interval(iName)
for cents in (-49, -25, 26, 49):
with self.subTest(interval=iName, cents=cents):
src = pitch.Pitch('C4')
src.microtone = cents
psBefore = src.ps
out = src.transpose(iv)
self.assertAlmostEqual(out.microtone.cents, cents)
self.assertAlmostEqual(out.ps - psBefore,
iv.chromatic.semitones)
if out.accidental is not None:
self.assertEqual(out.accidental.alter % 1, 0.0)

def testDescendingAugmentedUnison(self):
ns = note.Note('C4')
ne = note.Note('C-4')
Expand Down
Loading