Skip to content
Merged
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
83 changes: 82 additions & 1 deletion documentation/source/about/migratingToV11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type checker can follow ``stream.Stream[note.Note]()`` all the way to
Octaves are always integers
---------------------------

**In one line:** ``octave`` is always an ``int``, and ``octaveIsImplicit`` says whether you gave one.
**Summary:** ``octave`` is always an ``int``, and ``octaveIsImplicit`` says whether you gave one.

Before v11, ``pitch.Pitch('F#').octave`` was ``None``: a lovely idea (an
F-sharp in *any* octave) that crashed the moment someone wrote
Expand Down Expand Up @@ -122,6 +122,81 @@ got truer ones: ``opensIncorrectly()`` is ``not modalOpening()`` and
``closesIncorrectly()`` is ``not clausulaVera()``.


Meter parts cannot be changed
-----------------------------

**Summary:** :class:`~music21.meter.core.MeterTerminal` and
:class:`~music21.meter.core.MeterSequence` cannot be changed once made, so keep
what their methods return.

If you only make TimeSignatures and ask them about beats, nothing changes but
the speed. The second ``TimeSignature('4/4')`` is about 100 times faster than
before, and a deepcopy about 9 times.

If you customize beams, beats, or accents, assign the result back:

>>> ts = meter.TimeSignature('6/8')
>>> ts.beatSequence = ts.beatSequence.partitioned(6)
>>> ts.beatCount
6

.. list-table::
:header-rows: 1
:widths: 50 50

* - Before v11
- v11
* - ``ms.partition(2)``
- ``ms = ms.partitioned(2)``
* - ``ms.partitionByCount(2)``, ``ms.partitionByList([1, 1])``,
``ms.partitionByOtherMeterSequence(other)``
- ``ms = ms.partitionedByCount(2)``, ``ms = ms.partitionedByList([1, 1])``,
``ms = ms.partitionedByOtherMeterSequence(other)``
* - ``ms.subdividePartitionsEqual(2)``, ``ms.subdivideNestedHierarchy(3)``
- ``ms = ms.subdividedPartitionsEqual(2)``,
``ms = ms.subdividedNestedHierarchy(3)``
* - ``ms.setLevelWeight([1.0, 0.5])``
- ``ms = ms.withLevelWeight([1.0, 0.5])``
* - ``ms[0] = ms[0].subdivide(2)``
- ``ms = ms.setIndex(0, ms[0].subdivide(2))``
* - ``ms.weight = 1.0``
- ``ms = ms.withWeight(1.0)``
* - ``ms = MeterSequence(); ms.load('4/4')``
- ``ms = MeterSequence('4/4')``
* - ``ms.parenthesis = True``
- ``ms = ms.modify(parenthesis=True)``, also for ``summedNumerator``
* - ``mt.numerator = 2``
- ``mt = mt.modify(numerator=2)``, also for ``denominator`` and ``weight``

Every form on the left now raises an exception, and most name the replacement,
so nothing silently stops working. A deepcopy of either class is the same
object, two with the same contents are equal, and a MeterSequence is no longer a
kind of MeterTerminal, so ``isinstance(ms, meter.MeterTerminal)`` is False.

``meter.core.getMeterTerminal()`` and ``meter.core.getMeterSequence()`` reuse an
earlier object with the same values when there is one.

The durations that come from a meter are now shared
:class:`~music21.duration.FrozenDuration` objects: ``.duration`` on a
MeterTerminal or MeterSequence, ``barDuration``, ``beatDuration``,
``beatDivisionDurations``, and ``getBeatDuration()``. Changing one raises an exception, and so does changing
the length of a note that was given one. Call ``unfreeze()`` for a copy that can
be changed:

>>> beat = meter.TimeSignature('6/8').beatDuration
>>> beat
<music21.duration.FrozenDuration 1.5>
>>> changeable = beat.unfreeze()
>>> changeable.dots = 0
>>> changeable.quarterLength
1.0

To change the length of a bar, assign a new Duration, which ``barDuration`` then
returns as given: ``ts.barDuration = duration.Duration(7.0)``, not
``ts.barDuration.quarterLength = 7.0``. ``copy.deepcopy()`` of a FrozenDuration is
the same FrozenDuration, so use ``unfreeze()`` for a copy that can be changed.


Roman numerals
--------------

Expand All @@ -140,6 +215,8 @@ Durations and sorting

* Two Durations with ``expressionIsInferred`` True are equal when their
quarterLengths match; type, dots and tuplets are free to be re-expressed.
* ``FrozenDuration('half')`` equals ``Duration('half')``, and
``FrozenDuration.unfreeze()`` returns a Duration that can be changed.
* ``sorting.SortTuple`` is a modern NamedTuple. ``priority`` and
``classSortOrder`` may be floats, and ``modify()`` with a bad field name
raises ``ValueError``.
Expand Down Expand Up @@ -203,6 +280,8 @@ For developers
* ``common.enums.ContainsEnum`` is not needed: Python 3.12's ``StrEnum``
suffices, or ``HexEnum`` for hex values such as MIDI. The alias leaves in
v12. ``common.defaultlist`` is deprecated.
* ``EqualSlottedObjectMixin`` returns ``NotImplemented`` rather than False when
compared with a different type, so the other object's ``__eq__`` gets a turn.
* The test runners import modules the normal way, so a module's tests no
longer need ``from music21.key import KeySignature`` to compare types.
* AI agents get their own instructions, skills and shared memory in
Expand All @@ -220,6 +299,8 @@ Removed and deprecated
- Use instead
* - Python 3.11
- Python 3.12 or newer
* - ``MeterTerminal.subdivideByOther(other)``, also on MeterSequence
- ``meter.MeterSequence(other)``
* - ``music21.vexflow``
- music21j
* - ``midi.translate.midiEventsToInstrument``
Expand Down
8 changes: 7 additions & 1 deletion music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,12 @@ def __init__(self,
# private duration storage; managed by property
self._duration: Duration|None = duration
if duration is not None:
duration.client = self
try:
duration.client = self
except TypeError:
# a FrozenDuration has no client; if this changes, update the duration setter too
if not isinstance(duration, FrozenDuration):
raise
self._priority = 0 # default is zero

# store cached values here:
Expand Down Expand Up @@ -2807,6 +2812,7 @@ def duration(self, durationObj: Duration):
except TypeError:
# a FrozenDuration (e.g. from a TimeSignature) never changes,
# so it has nothing to tell a client
# if this changes, update __init__ too
if not isinstance(durationObj, FrozenDuration):
raise
if durationObjAlreadyExists:
Expand Down
35 changes: 27 additions & 8 deletions music21/meter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,15 +780,17 @@ def denominator(self, value):
@property
def barDuration(self) -> duration.Duration:
'''
Return a :class:`~music21.duration.Duration` object equal to the
Return a :class:`~music21.duration.FrozenDuration` object equal to the
total length of this TimeSignature.

>>> ts = meter.TimeSignature('5/16')
>>> ts.barDuration
<music21.duration.Duration 1.25>
<music21.duration.FrozenDuration 1.25>

>>> ts2 = meter.TimeSignature('3/8')
>>> d = ts2.barDuration
>>> d
<music21.duration.FrozenDuration 1.5>
>>> d.type
'quarter'
>>> d.dots
Expand All @@ -797,7 +799,7 @@ def barDuration(self) -> duration.Duration:
1.5

This can be overridden to create different representations
or to contradict the meter.
or to contradict the meter (which do not have to be FrozenDurations)

>>> d2 = duration.Duration(1.75)
>>> ts2.barDuration = d2
Expand All @@ -807,10 +809,28 @@ def barDuration(self) -> duration.Duration:
An uninitialized TimeSignature returns 4.0 for 4/4

>>> meter.TimeSignature().barDuration
<music21.duration.Duration 4.0>
<music21.duration.FrozenDuration 4.0>

To set a note's duration to fill a whole measure, but allow it to be
manipulated later, `unfreeze` the duration:

>>> ts = meter.TimeSignature('2/4')
>>> n = note.Note('G#',
... duration=ts.barDuration.unfreeze(),
... )
>>> n.duration
<music21.duration.Duration 2.0>
>>> n.duration.dots = 2
>>> n.duration
<music21.duration.Duration 3.5>

The time signature's barDuration is unchanged.

>>> ts.barDuration
<music21.duration.FrozenDuration 2.0>

Avoid changing the Duration you get here: in v12 it is expected to become a
:class:`~music21.duration.FrozenDuration`, which cannot be changed.
* Changed in v11: returns a :class:`~music21.duration.FrozenDuration`
unless one has been assigned.
'''
if self._overriddenBarDuration:
return self._overriddenBarDuration
Expand All @@ -819,8 +839,7 @@ def barDuration(self) -> duration.Duration:
if beamSequence is not None:
# could come from self.beamSequence, self.accentSequence,
# self.displaySequence, self.accentSequence
# unfreeze() to allow mutability till v12
return beamSequence.duration.unfreeze()
return beamSequence.duration

# should never happen.
return duration.Duration(0) # pragma: no cover
Expand Down
14 changes: 14 additions & 0 deletions music21/meter/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,20 @@ def testMeterSequenceModify(self):
with self.assertRaises(ValueError):
a.modify(**kw)

def testBarDurationCannotBeChanged(self):
# changing the copy that barDuration used to return was silently lost
ts = TimeSignature('3/4')
self.assertIsInstance(ts.barDuration, duration.FrozenDuration)
with self.assertRaises(TypeError):
ts.barDuration.quarterLength = 7.0
self.assertEqual(ts.barDuration.quarterLength, 3.0)
self.assertEqual(ts.barDuration, duration.Duration(3.0))

# an assigned duration is returned as given
override = duration.Duration(7.0)
ts.barDuration = override
self.assertIs(ts.barDuration, override)

def testInPlaceFormsRaise(self):
ts = TimeSignature('6/8')
before = ts.beatSequence
Expand Down
2 changes: 1 addition & 1 deletion music21/romanText/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ def fillMeasureFromPreviousRn(self, mFill: stream.Measure) -> None:
newRn = copy.deepcopy(self.previousRn)
newRn.lyric = ''
# set to entire bar duration and tie
newRn.duration = copy.deepcopy(self.tsAtTimeOfLastChord.barDuration)
newRn.quarterLength = self.tsAtTimeOfLastChord.barDuration.quarterLength
if self.previousRn.tie is None:
self.previousRn.tie = tie.Tie('start')
else:
Expand Down
4 changes: 2 additions & 2 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13398,10 +13398,10 @@ def barDuration(self):
>>> m = stream.Measure()
>>> m.timeSignature = meter.TimeSignature('3/4')
>>> m.barDuration
<music21.duration.Duration 3.0>
<music21.duration.FrozenDuration 3.0>
>>> m.repeatAppend(note.Note(type='quarter'), 5)
>>> m.barDuration
<music21.duration.Duration 3.0>
<music21.duration.FrozenDuration 3.0>
>>> m.duration
<music21.duration.Duration 5.0>

Expand Down
9 changes: 9 additions & 0 deletions music21/test/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ def testNoteCreation(self):
self.assertFalse('flute' in n.groups)
self.assertTrue('flute' in b.groups)

def testFrozenDurationInConstructor(self):
# Note(duration=ts.barDuration) raised while setting the duration's client
ts = meter.TimeSignature('2/4')
n = note.Note(duration=ts.barDuration)
self.assertIs(n.duration, ts.barDuration)
with self.assertRaises(TypeError):
n.duration.dots = 2
self.assertEqual(ts.barDuration.quarterLength, 2.0)

def testAssignFrozenDurationStaysFrozen(self):
'''
A note can be given a FrozenDuration, such as a MeterSequence's. It stays
Expand Down
Loading