diff --git a/.agents/skills/writing-docs/SKILL.md b/.agents/skills/writing-docs/SKILL.md index 0e393cfcc8..8950cd7ec8 100644 --- a/.agents/skills/writing-docs/SKILL.md +++ b/.agents/skills/writing-docs/SKILL.md @@ -47,8 +47,13 @@ what a reader must act on; drop the before-picture. * Changed in v11: `stringOutput()` always returns a `str`, never None. * Changed in v11: emits `\tuplet`; the arguments are now actual, normal. * New in v11. +* Deprecated in v11: use `.octave`, which is now always an int. ``` +A deprecated function or method also gets `@common.deprecated(...)`. A property +gets only the marker, since IDEs read every property while inspecting an object; +leave `# Add real deprecation message here in vX` in its body instead. + A plain bug fix — code now does what it always claimed — gets no marker and no doctest. It goes in the commit message. diff --git a/AGENTS.md b/AGENTS.md index 6b9738c6f2..2e7640dcab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,10 @@ `environLocal.printDebug(...)`, or `environLocal.warn(...)` when the user should hear about it every time. `test/toggleDebug.py` switches debug output on and off. - New exceptions subclass `exceptions21.Music21Exception`. +- Deprecate a function or method with `@common.deprecated(...)`. Never put it on a + property: IDEs read every property while inspecting an object, so the warning would + fire on people who never used it. For a property, write `* Deprecated in vX: use Y.` + in the docstring and leave `# Add real deprecation message here in vY` in the body. - Return named things — a small class, a `namedtuple`, a dict — never a positional tuple of more than 2 (maybe 3) elements whose elements each mean something different and unrelated (x, y, z is okay for instance). Nobody should write `returned[3][0][7]`. - Don't reuse a variable name once the type of what it holds changes @@ -128,6 +132,12 @@ after addressing the problem. (A blind close or close with "not accepted" etc. generally means that the issue/PR has too many problems to easily solve and has become a burden for the maintainer). - Do not include a "Tests run" section unless the testing procedure was unusual (like it affects part of the system without standard tests, like the testing system itself.) +- While someone is reviewing a PR or a pushed branch, "do X" is not "commit and push X": + make the change and leave it unstaged. When the list looks finished (or you hear "done!" + or "push it"), offer to commit, or to commit and push. Batch a round's small fixes into + one commit; no micro-commit trains. Prefer new commits to amend + force-push, since the + reviewer may have pulled the branch; if asked to fold a fix into the commit it changes, + amend, force-push with `--force-with-lease` against an explicit SHA, and say so. # Writing style diff --git a/documentation/source/about/index.rst b/documentation/source/about/index.rst index ee319489b2..d0be870496 100644 --- a/documentation/source/about/index.rst +++ b/documentation/source/about/index.rst @@ -8,4 +8,5 @@ About `music21` about applications faq + migratingToV11 referenceCorpus diff --git a/documentation/source/about/migratingToV11.rst b/documentation/source/about/migratingToV11.rst new file mode 100644 index 0000000000..acc455dfde --- /dev/null +++ b/documentation/source/about/migratingToV11.rst @@ -0,0 +1,268 @@ +.. _migratingToV11: + +Migrating to music21 v11 +======================== + +*(This is an in-progress guide that was automatically generated by Myke's AI Agent.)* + +What changed between the v10 line and v11, and what to use instead. The big, code-breaking items come first, then smaller +improvements by area, then one table of everything removed or deprecated. + + +Python 3.12 or newer +-------------------- + +Python 3.11 support is gone; 3.12, 3.13, and 3.14 are tested. The payoff is +real generics: ``Stream`` and its iterators use the PEP 695 bracket form, so a +type checker can follow ``stream.Stream[note.Note]()`` all the way to +``for n in s: n.pitch``. + + +Octaves are always integers +--------------------------- + +**In one line:** ``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 +``p.octave + 1``. The pitch knew all along which octave it would use for +MIDI or a staff. It kept that in ``implicitOctave``, a property that nobody +remembered. Now ``octave`` does that job itself: + +>>> from music21 import * +>>> anyFSharp = pitch.Pitch('F#') +>>> anyFSharp.octave +4 +>>> anyFSharp.octaveIsImplicit +True + +Nothing else about an octave-less pitch changes. It still prints without a +number, transposes without one, and is not equal to an explicit F#4: + +>>> anyFSharp + +>>> anyFSharp.transpose('P8') + +>>> anyFSharp == pitch.Pitch('F#4') +False + +Give it an octave and the flag flips: + +>>> anyFSharp.octave = 5 +>>> anyFSharp.octaveIsImplicit +False +>>> anyFSharp + + +Notes follow along: ``note.Note('B-').octave`` is ``4``, and the flag lives +on the note's pitch, ``n.pitch.octaveIsImplicit``. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Before v11 + - v11 + * - ``if p.octave is None:`` + - ``if p.octaveIsImplicit:`` + * - ``p.octave = None`` + - ``p.octaveIsImplicit = True`` (the old spelling is deprecated; removed in v13) + * - ``p.implicitOctave`` + - ``p.octave`` + * - ``if p.octave is None: p.octave = p.implicitOctave`` + - ``p.octaveIsImplicit = False`` + * - ``octave: int | None`` + - ``octave: int`` + +``p.octave + 1`` and ``f'{p.name}{p.octave}'`` no longer crash. The few +methods that need a pitch's *own* octave, such as +:meth:`~music21.pitch.Pitch.transposeAboveTarget`, still raise for an +implicit one; set ``.octave`` first. ``implicitOctave`` stays as a synonym +so nothing breaks today, but it is deprecated and starts warning in v12. + + +KeySignature.sharps is always an int +------------------------------------ + +Similar idea to Octave. ``KeySignature.sharps`` (and ``Key.sharps``) is an +``int`` you can add to and subtract from. A non-traditional signature is a +flag plus a list of pitches. ``sharps=None`` still works but warns: + +>>> unusual = key.KeySignature() +>>> unusual.isNonTraditional = True +>>> unusual.alteredPitches = ['E-', 'G#4'] +>>> unusual + + + +Chord indexing gets simpler +--------------------------- + +``c[1]``, ``c['G4']`` and ``c[somePitch]`` still return the chord's +:class:`~music21.note.Note`; everything fancier is gone. Attribute paths +such as ``c['2.tie']`` and ``c['D-4.style.color']`` no longer work, a Note is +no longer accepted as a key (use its pitch), and the per-note getters and +setters (``getTie``, ``setColor``, ``getNotehead``, and their siblings) are +deprecated. Index, then use the Note: + +>>> c = chord.Chord('C4 E4 G4') +>>> c['E4'].tie = tie.Tie('start') +>>> c[1].tie + + + +VoiceLeadingQuartet needs its four notes +---------------------------------------- + +``voiceLeading.VoiceLeadingQuartet(v1n1, v1n2, v2n1, v2n2)`` requires all +four notes up front, the first Music21Object ever to insist on arguments. In +return they are always Notes, never ``None``. ``analyticKey`` is now +``key``, ``vIntervals`` and ``hIntervals`` are tuples, and two dogmatic names +got truer ones: ``opensIncorrectly()`` is ``not modalOpening()`` and +``closesIncorrectly()`` is ``not clausulaVera()``. + + +Roman numerals +-------------- + +* ``romanNumeralFromChord`` keeps the sharp on major-quality chords built on + the raised sixth and seventh degrees in minor: ``#VI``, not ``VI#63``. + ``correctRNAlterationForMinor`` gained a ``chordHasMajorThird`` keyword for + the same reason. +* Figures containing ``x``, ``y`` or ``z`` raise. ``RomanNumeral('IIIx')`` + used to be a III13 chord by accident. +* ``FigureTuple`` and ``PitchFigureTuple`` are typed NamedTuples, and + ``FigureTuple.fromPitchAndReference()`` replaces ``figureTupleSolo``. + + +Durations and sorting +--------------------- + +* Two Durations with ``expressionIsInferred`` True are equal when their + quarterLengths match; type, dots and tuplets are free to be re-expressed. +* ``sorting.SortTuple`` is a modern NamedTuple. ``priority`` and + ``classSortOrder`` may be floats, and ``modify()`` with a bad field name + raises ``ValueError``. +* tinyNotation's undocumented ``0`` duration (a whole bar plus a fermata) is + deprecated. + + +Streams +------- + +* New ``Stream.isAtEnd(el)`` says whether ``storeAtEnd`` put ``el`` there. +* ``Stream.hasElement(el)`` is removed: write ``el in s``. + ``hasElementOfClass`` is deprecated: write ``if s.getElementsByClass(X):``. + + +Pitches +------- + +``pitch.simplifyMultipleEnharmonics`` takes ``criterion`` and ``keyContext`` +as keyword-only arguments. + + +File formats +------------ + +* **ABC**: ``w:`` lyric lines are imported, with hyphenation and ``*`` skips. + ``abcToStreamOpus`` always returns an Opus. +* **Humdrum**: grace notes keep their written duration instead of becoming + eighths; duration parsing lives in a new ``hdStringToDuration``; a token + with no duration warns when a quarter is assumed; ``GlobalReference`` is no + longer a Music21Object. +* **LilyPond**: output targets current LilyPond (``\tuplet``, modern barline + names, ``\markuplist``), and every ``stringOutput()`` returns a ``str``. +* **MIDI**: ``midiEventsToInstrument`` is removed; use + ``midiEventToInstrument``. +* **Vexflow**: the ``music21.vexflow`` module is gone. It had not worked in + a decade; music21j is the way to draw in a browser. +* **configure**: choosing MuseScore as the MusicXML reader also sets + ``musescoreDirectPNGPath``. + + +figuredBass, features, tree +--------------------------- + +* figuredBass: ``hiddenFifth`` and ``hiddenOctave`` are now ``hiddenFifths`` + and ``hiddenOctaves``, matching ``parallelFifths``. ``FiguredBassScale``'s + first argument is ``scaleTonic``, not ``scaleValue``. Rests inside a + possibility are typed sentinels rather than the string ``'RT'``. +* features: ``Feature.vector`` starts as ``[]`` rather than ``None``, and + ``FeatureExtractor.dimensions`` defaults to 1. +* tree: ``ElementTimespan`` and ``PitchedTimespan`` take their arguments in + the same order as ``Timespan``; ``offset``, ``endTime`` and ``element`` are + required. + + +For developers +-------------- + +* Type annotations across nearly the whole library, with ``t.cast()`` for + narrowing and ``@property`` decorators throughout. +* ``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. +* 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 + ``AGENTS.md`` and ``.agents/``. + + +Removed and deprecated +---------------------- + +.. list-table:: Removed in v11 + :header-rows: 1 + :widths: 50 50 + + * - Gone + - Use instead + * - Python 3.11 + - Python 3.12 or newer + * - ``music21.vexflow`` + - music21j + * - ``midi.translate.midiEventsToInstrument`` + - ``midiEventToInstrument`` + * - ``Stream.hasElement(el)`` + - ``el in s`` + * - ``musicxml.xmlToM21.MusicXMLImporter.identificationToMetadata`` + - ``addIdentificationToMetadata`` + * - a string for ``musicxml.xmlToM21.MeasureParser.getStaffNumber`` + - an ``int`` + * - ``VoiceLeadingQuartet.unison`` / ``.fifth`` / ``.octave`` + - ``interval.Interval('P1')`` and friends + * - ``c['2.tie']`` attribute paths into a Chord + - ``c[2].tie`` + * - a Note as a Chord index, ``c[someNote]`` + - ``c[someNote.pitch]`` + +.. list-table:: Deprecated in v11 (removed in v12 unless noted) + :header-rows: 1 + :widths: 50 50 + + * - Deprecated + - Use instead + * - ``Stream.hasElementOfClass(X)`` + - ``if s.getElementsByClass(X):`` + * - ``Chord.getTie``, ``setTie``, ``getColor``, ``setColor``, + ``getNotehead``, ``setNotehead``, ``getNoteheadFill``, + ``setNoteheadFill``, ``getStemDirection``, ``setStemDirection`` + - ``c[i].tie``, ``c[i].style.color``, ``c[i].notehead``, + ``c[i].noteheadFill``, ``c[i].stemDirection`` + * - ``roman.figureTupleSolo`` + - ``FigureTuple.fromPitchAndReference()`` + * - ``VoiceLeadingQuartet.opensIncorrectly()`` / ``closesIncorrectly()`` + - ``not modalOpening()`` / ``not clausulaVera()`` + * - ``KeySignature(sharps=None)`` + - ``ks.isNonTraditional = True`` + * - ``variant.addVariant(replacementDuration=...)`` + - ``replacementQuarterLength=...`` + * - ``common.defaultlist`` + - a ``list`` or a ``dict`` + * - ``common.enums.ContainsEnum`` + - not needed: ``enum.StrEnum`` suffices, or ``HexEnum`` for hex values such as MIDI + * - tinyNotation ``0`` duration + - the duration plus an ``expressions.Fermata`` + * - ``Pitch.implicitOctave`` + - ``Pitch.octave`` (warns from v12, removed later) diff --git a/documentation/source/usersGuide/usersGuide_03_pitches.ipynb b/documentation/source/usersGuide/usersGuide_03_pitches.ipynb index 2026ffef60..40c4f43f81 100644 --- a/documentation/source/usersGuide/usersGuide_03_pitches.ipynb +++ b/documentation/source/usersGuide/usersGuide_03_pitches.ipynb @@ -54,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -94,7 +94,7 @@ "4" ] }, - "execution_count": 3, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -105,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -114,7 +114,7 @@ "10" ] }, - "execution_count": 4, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -125,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -134,7 +134,7 @@ "'B-'" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -145,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -154,7 +154,7 @@ "-1.0" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -177,7 +177,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -186,7 +186,7 @@ "'B-4'" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -197,7 +197,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -206,7 +206,7 @@ "70" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -215,6 +215,91 @@ "p1.midi" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Leave the octave off and the `Pitch` means \"a B-flat in *any* octave.\"\n", + "It prints without a number:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "anyBFlat = pitch.Pitch('B-')\n", + "anyBFlat" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But `music21` often needs an octave anyhow, to play the pitch or put it\n", + "on a staff, so `.octave` still answers with the default, `4` (the octave\n", + "of middle C), and `.octaveIsImplicit` remembers that you didn't give one:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "anyBFlat.octave" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "anyBFlat.octaveIsImplicit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set `.octave` yourself and `.octaveIsImplicit` becomes `False`." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -233,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -242,7 +327,7 @@ "'D#3'" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -263,7 +348,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -272,7 +357,7 @@ "" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -306,7 +391,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -315,7 +400,7 @@ "'C#'" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -327,7 +412,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -336,7 +421,7 @@ "'C#'" ] }, - "execution_count": 12, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -347,7 +432,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -356,7 +441,7 @@ "4" ] }, - "execution_count": 13, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -367,7 +452,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -376,7 +461,7 @@ "4" ] }, - "execution_count": 14, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -394,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -403,7 +488,7 @@ "'do sostenido'" ] }, - "execution_count": 15, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -421,7 +506,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "metadata": { "tags": [ "nbval-raises-exception" @@ -453,7 +538,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -477,7 +562,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -550,7 +635,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -583,7 +668,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "metadata": {}, "outputs": [], "source": [ @@ -604,7 +689,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "metadata": {}, "outputs": [ { @@ -613,7 +698,7 @@ "1.5" ] }, - "execution_count": 21, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -624,7 +709,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -633,7 +718,7 @@ "2.0" ] }, - "execution_count": 22, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -652,7 +737,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -661,7 +746,7 @@ "'half'" ] }, - "execution_count": 23, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -672,7 +757,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -681,7 +766,7 @@ "'quarter'" ] }, - "execution_count": 24, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -703,7 +788,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -712,7 +797,7 @@ "0" ] }, - "execution_count": 25, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -723,7 +808,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -732,7 +817,7 @@ "1" ] }, - "execution_count": 26, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -754,7 +839,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 29, "metadata": {}, "outputs": [ { @@ -763,7 +848,7 @@ "1.75" ] }, - "execution_count": 27, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -775,7 +860,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 30, "metadata": {}, "outputs": [ { @@ -784,7 +869,7 @@ "1.875" ] }, - "execution_count": 28, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -796,7 +881,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -805,7 +890,7 @@ "1.9375" ] }, - "execution_count": 29, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -825,7 +910,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -834,7 +919,7 @@ "'16th'" ] }, - "execution_count": 30, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -846,7 +931,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "metadata": {}, "outputs": [ { @@ -855,7 +940,7 @@ "0" ] }, - "execution_count": 31, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -913,7 +998,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -922,7 +1007,7 @@ "" ] }, - "execution_count": 47, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -934,7 +1019,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 35, "metadata": {}, "outputs": [ { @@ -943,7 +1028,7 @@ "" ] }, - "execution_count": 48, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -961,7 +1046,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 36, "metadata": {}, "outputs": [], "source": [ @@ -978,7 +1063,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 37, "metadata": {}, "outputs": [ { @@ -987,7 +1072,7 @@ "'half'" ] }, - "execution_count": 50, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -998,7 +1083,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 38, "metadata": {}, "outputs": [ { @@ -1007,7 +1092,7 @@ "1" ] }, - "execution_count": 51, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -1018,7 +1103,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 39, "metadata": {}, "outputs": [ { @@ -1027,7 +1112,7 @@ "'E-'" ] }, - "execution_count": 52, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } @@ -1038,7 +1123,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 40, "metadata": {}, "outputs": [ { @@ -1047,7 +1132,7 @@ "" ] }, - "execution_count": 53, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -1058,7 +1143,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 41, "metadata": {}, "outputs": [ { @@ -1067,7 +1152,7 @@ "5" ] }, - "execution_count": 54, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } @@ -1087,7 +1172,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 42, "metadata": {}, "outputs": [ { @@ -1096,7 +1181,7 @@ "'E-'" ] }, - "execution_count": 55, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -1107,7 +1192,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 43, "metadata": {}, "outputs": [ { @@ -1116,7 +1201,7 @@ "3.0" ] }, - "execution_count": 56, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -1134,7 +1219,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 44, "metadata": {}, "outputs": [], "source": [ @@ -1153,7 +1238,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 45, "metadata": {}, "outputs": [], "source": [ @@ -1173,7 +1258,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 46, "metadata": {}, "outputs": [], "source": [ @@ -1191,7 +1276,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 47, "metadata": {}, "outputs": [], "source": [ @@ -1215,7 +1300,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 48, "metadata": {}, "outputs": [ { @@ -1250,7 +1335,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 49, "metadata": {}, "outputs": [ { diff --git a/music21/capella/fromCapellaXML.py b/music21/capella/fromCapellaXML.py index 820cf6e967..8730568e7e 100644 --- a/music21/capella/fromCapellaXML.py +++ b/music21/capella/fromCapellaXML.py @@ -539,7 +539,7 @@ def noteFromHead(self, headElement: ET.Element) -> note.Note: noteNameWithOctave = headElement.attrib['pitch'] n = note.Note() n.nameWithOctave = noteNameWithOctave - n.octave = n.pitch.implicitOctave - 1 # capella octaves are 1 off + n.octave = n.pitch.octave - 1 # capella octaves are 1 off alters = headElement.findall('alter') if len(alters) > 1: diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 3c5a16330c..a0d6789f68 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -1544,9 +1544,6 @@ def closedPosition( pBass = returnObj.bass() # returns a reference, not a copy if forceOctave is not None: pBassOctave = pBass.octave - if pBassOctave is None: - pBassOctave = pBass.implicitOctave - if pBassOctave > forceOctave: dif = -1 elif pBassOctave < forceOctave: @@ -1557,16 +1554,13 @@ def closedPosition( while pBass.octave != forceOctave: # shift octave of all pitches for p in returnObj.pitches: - if p.octave is None: - p.octave = p.implicitOctave p.octave += dif # can change these pitches in place for p in returnObj.pitches: # bring each pitch down octaves until pitch space is # within an octave - if p.octave is None: - p.octave = p.implicitOctave + p.octaveIsImplicit = False while p.ps >= pBass.ps + 12: p.octave -= 1 # check for a bass of C4 and the note B#7 added to it, should be B#4 not B#3... @@ -2394,10 +2388,7 @@ def _setInversion( currentMaxMidi = max(self.pitches).ps tempBassPitch = self.bass() while tempBassPitch.ps < currentMaxMidi: - if tempBassPitch.octave is not None: - tempBassPitch.octave += 1 - else: - tempBassPitch.octave = tempBassPitch.implicitOctave + 1 + tempBassPitch.octave += 1 # housekeeping for next loop tests self.clearCache() @@ -4012,7 +4003,7 @@ def semiClosedPosition( if p.step not in usedSteps: usedSteps.append(p.step) else: - p.octave = p.implicitOctave + 1 + p.octave += 1 newRemainingPitches.append(p) remainingPitches = newRemainingPitches diff --git a/music21/expressions.py b/music21/expressions.py index fbecb96bf7..bcbf71a59d 100644 --- a/music21/expressions.py +++ b/music21/expressions.py @@ -760,8 +760,7 @@ def getSize( ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch) ornamentalPitch.accidental = None - if ornamentalPitch.octave is None: - ornamentalPitch.octave = ornamentalPitch.implicitOctave + ornamentalPitch.octaveIsImplicit = False if self._direction == 'up': ornamentalPitch.transpose(interval.GenericInterval(2), inPlace=True) @@ -828,8 +827,7 @@ def resolveOrnamentalPitches( transposeInterval: interval.IntervalBase = self.getSize(srcObj, keySig=keySig) ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch) - if ornamentalPitch.octave is None: - ornamentalPitch.octave = ornamentalPitch.implicitOctave + ornamentalPitch.octaveIsImplicit = False ornamentalPitch.transpose(transposeInterval, inPlace=True) # if there are microtones, see if they can be converted to quarter tones. if ornamentalPitch.microtone.cents != 0: @@ -1375,8 +1373,7 @@ def getSize( ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch) ornamentalPitch.accidental = None - if ornamentalPitch.octave is None: - ornamentalPitch.octave = ornamentalPitch.implicitOctave + ornamentalPitch.octaveIsImplicit = False if self._direction == 'up': ornamentalPitch.transpose(interval.GenericInterval(2), inPlace=True) @@ -1424,8 +1421,7 @@ def resolveOrnamentalPitches( transposeInterval: interval.IntervalBase = self.getSize(srcObj, keySig=keySig) ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch) - if ornamentalPitch.octave is None: - ornamentalPitch.octave = ornamentalPitch.implicitOctave + ornamentalPitch.octaveIsImplicit = False ornamentalPitch.transpose(transposeInterval, inPlace=True) # if there are microtones, see if they can be converted to quarter tones. if ornamentalPitch.microtone.cents != 0: @@ -2043,8 +2039,7 @@ def getSize( ornamentalPitch: pitch.Pitch = copy.deepcopy(srcPitch) ornamentalPitch.accidental = None - if ornamentalPitch.octave is None: - ornamentalPitch.octave = ornamentalPitch.implicitOctave + ornamentalPitch.octaveIsImplicit = False accidental: pitch.Accidental|None = None if which == 'upper': @@ -2119,16 +2114,14 @@ def resolveOrnamentalPitches( srcObj, 'lower', keySig=keySig) upperPitch: pitch.Pitch = copy.deepcopy(srcPitch) - if upperPitch.octave is None: - upperPitch.octave = upperPitch.implicitOctave + upperPitch.octaveIsImplicit = False upperPitch.transpose(transposeIntervalUp, inPlace=True) # if there are microtones, see if they can be converted to quarter tones. if upperPitch.microtone.cents != 0: upperPitch.convertMicrotonesToQuarterTones(inPlace=True) lowerPitch: pitch.Pitch = copy.deepcopy(srcPitch) - if lowerPitch.octave is None: - lowerPitch.octave = lowerPitch.implicitOctave + lowerPitch.octaveIsImplicit = False lowerPitch.transpose(transposeIntervalDown, inPlace=True) # if there are microtones, see if they can be converted to quarter tones. if lowerPitch.microtone.cents != 0: diff --git a/music21/figuredBass/realizerScale.py b/music21/figuredBass/realizerScale.py index 3f3345fc19..a1b6575dcd 100644 --- a/music21/figuredBass/realizerScale.py +++ b/music21/figuredBass/realizerScale.py @@ -203,7 +203,7 @@ def getPitches(self, bassPitch = convertToPitch(bassPitch) maxPitch = convertToPitch(maxPitch) pitchNames = self.getPitchNames(bassPitch, notationString) - maxOctave = maxPitch.implicitOctave + maxOctave = maxPitch.octave iter1 = itertools.product(pitchNames, range(maxOctave + 1)) iter2 = map(lambda x: pitch.Pitch(x[0] + str(x[1])), iter1) iter3 = itertools.filterfalse(lambda samplePitch: bassPitch > samplePitch, iter2) diff --git a/music21/figuredBass/segment.py b/music21/figuredBass/segment.py index 5bf60d4818..466ccca6d0 100644 --- a/music21/figuredBass/segment.py +++ b/music21/figuredBass/segment.py @@ -930,15 +930,15 @@ def getPitches(pitchNames: Iterable[str] = ('C', 'E', 'G'), >>> segment.getPitches(maxPitch=pitch.Pitch('E')) Traceback (most recent call last): - ValueError: maxPitch must be given an octave + ValueError: maxPitch must not have an implicit octave ''' if isinstance(bassPitch, str): bassPitch = pitch.Pitch(bassPitch) if isinstance(maxPitch, str): maxPitch = pitch.Pitch(maxPitch) - if maxPitch.octave is None: - raise ValueError('maxPitch must be given an octave') + if maxPitch.octaveIsImplicit: + raise ValueError('maxPitch must not have an implicit octave') iter1 = itertools.product(pitchNames, range(maxPitch.octave + 1)) iter2 = map(lambda x: pitch.Pitch(x[0] + str(x[1])), iter1) iter3 = itertools.filterfalse(lambda samplePitch: bassPitch > samplePitch, iter2) diff --git a/music21/harmony.py b/music21/harmony.py index f9d152f2c7..1210fc6ad8 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -2676,10 +2676,6 @@ def testHarmonyPreservesInversionAndBass(self): self.assertEqual(explicitFm6.root(find=False).name, 'F') fm6bassOctave = explicitFm6.bass(find=False).octave fm6rootOctave = explicitFm6.root(find=False).octave - self.assertIsNotNone(fm6bassOctave) - self.assertIsNotNone(fm6rootOctave) - assert fm6bassOctave is not None - assert fm6rootOctave is not None self.assertLess(fm6bassOctave, fm6rootOctave) def testClassSortOrderHarmony(self): diff --git a/music21/interval.py b/music21/interval.py index 2390cdaa1b..01cac6ca4f 100644 --- a/music21/interval.py +++ b/music21/interval.py @@ -1409,10 +1409,7 @@ def transposePitch(self, p: pitch.Pitch, *, inPlace=False): >>> gSharp ''' - if p.octave is None: - useImplicitOctave = True - else: - useImplicitOctave = False + useImplicitOctave = p.octaveIsImplicit pitchDNN = p.diatonicNoteNum if inPlace: @@ -1422,7 +1419,7 @@ def transposePitch(self, p: pitch.Pitch, *, inPlace=False): newPitch.diatonicNoteNum = pitchDNN + self.staffDistance if useImplicitOctave: - newPitch.octave = None + newPitch.octaveIsImplicit = True if not inPlace: return newPitch @@ -2500,10 +2497,7 @@ def transposePitch(self, p: pitch.Pitch, *, inPlace=False): * Changed in v6: added inPlace. ''' - if p.octave is None: - useImplicitOctave = True - else: - useImplicitOctave = False + useImplicitOctave = p.octaveIsImplicit pps = p.ps if not inPlace: @@ -2512,8 +2506,8 @@ def transposePitch(self, p: pitch.Pitch, *, inPlace=False): newPitch = p newPitch.ps = pps + self.semitones - if useImplicitOctave is True: - newPitch.octave = None + if useImplicitOctave: + newPitch.octaveIsImplicit = True if not inPlace: return newPitch @@ -3426,8 +3420,8 @@ def transposePitch(self, maxAccidental=maxAccidental, ) - if p.fundamental.octave is None: - pOut.fundamental.octave = None + if p.fundamental.octaveIsImplicit: + pOut.fundamental.octaveIsImplicit = True if not inPlace: return pOut @@ -3450,10 +3444,7 @@ def _diatonicTransposePitch(self, # true unison and any multiple of true octave inheritAccidentalDisplayStatus = True - if p.octave is None: - useImplicitOctave = True - else: - useImplicitOctave = False + useImplicitOctave = p.octaveIsImplicit pitch1 = p pitch2 = copy.deepcopy(pitch1) @@ -3543,7 +3534,7 @@ def _diatonicTransposePitch(self, pitch2.microtone = pitch2.microtone.cents + centsOrigin if useImplicitOctave: - pitch2.octave = None + pitch2.octaveIsImplicit = True if not inPlace: return pitch2 diff --git a/music21/key.py b/music21/key.py index ffc80d7ca4..631a49a13c 100644 --- a/music21/key.py +++ b/music21/key.py @@ -127,7 +127,7 @@ def sharpsToPitch(sharpCount: int) -> pitch.Pitch: return copy.deepcopy(_sharpsToPitchCache[sharpCount]) pitchInit = pitch.Pitch('C') - pitchInit.octave = None + pitchInit.octaveIsImplicit = True # keyPc = (self.sharps * 7) % 12 if sharpCount > 0: intervalStr = 'P5' @@ -139,7 +139,7 @@ def sharpsToPitch(sharpCount: int) -> pitch.Pitch: intervalObj = interval.Interval(intervalStr) for i in range(abs(sharpCount)): pitchInit = intervalObj.transposePitch(pitchInit) - pitchInit.octave = None + pitchInit.octaveIsImplicit = True _sharpsToPitchCache[sharpCount] = pitchInit return pitchInit @@ -518,7 +518,7 @@ def alteredPitches(self) -> list[pitch.Pitch]: for i in range(self.sharps): pKeep.transpose('P5', inPlace=True) p = copy.deepcopy(pKeep) - p.octave = None + p.octaveIsImplicit = True post.append(p) elif self.sharps < 0: @@ -526,7 +526,7 @@ def alteredPitches(self) -> list[pitch.Pitch]: for i in range(abs(self.sharps)): pKeep.transpose('P4', inPlace=True) p = copy.deepcopy(pKeep) - p.octave = None + p.octaveIsImplicit = True post.append(p) return post @@ -780,7 +780,7 @@ def transposePitchFromC(self, p: pitch.Pitch, *, inPlace=False) -> pitch.Pitch|N for i in range(transTimes): transInterval.transposePitch(p, inPlace=True) - if originalOctave is not None: + if not p.octaveIsImplicit: p.octave = originalOctave if not inPlace: diff --git a/music21/lily/translate.py b/music21/lily/translate.py index c573246baa..335c9728e4 100644 --- a/music21/lily/translate.py +++ b/music21/lily/translate.py @@ -1514,12 +1514,12 @@ def octaveCharactersFromPitch(self, pitchObj: pitch.Pitch) -> str: returns a string of single-quotes or commas or '' representing the octave of a :class:`~music21.pitch.Pitch` object ''' - implicitOctave = pitchObj.implicitOctave - if implicitOctave < 3: - correctedOctave = 3 - implicitOctave + octave = pitchObj.octave + if octave < 3: + correctedOctave = 3 - octave octaveModChars = ',' * correctedOctave # C2 = c, C1 = c,, else: - correctedOctave = implicitOctave - 3 + correctedOctave = octave - 3 octaveModChars = "'" * correctedOctave # C4 = c', C5 = c'' etc. return octaveModChars diff --git a/music21/musedata/base40.py b/music21/musedata/base40.py index 073537e524..3a4d6c5417 100644 --- a/music21/musedata/base40.py +++ b/music21/musedata/base40.py @@ -233,7 +233,7 @@ def base40ToPitch(base40Num: int) -> pitch.Pitch: ''' p = pitch.Pitch() p.octave = ((base40Num - 1) / 40) + 1 - tableNum = base40Num - 40 * (p.implicitOctave - 1) + tableNum = base40Num - 40 * (p.octave - 1) pitchName = base40Equivalent.get(tableNum, '') if pitchName: p.name = pitchName @@ -266,7 +266,7 @@ def pitchToBase40(pitchToConvert: str|pitch.Pitch) -> int: pitchObj = pitchToConvert if pitchObj.name in base40Representation: tableNum = base40Representation[pitchObj.name] - base40Num = (40 * (pitchObj.implicitOctave - 1)) + tableNum + base40Num = (40 * (pitchObj.octave - 1)) + tableNum return base40Num raise Base40Exception('Base40 cannot handle this pitch ' + pitchObj.nameWithOctave) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 605810d917..e54b522059 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4580,7 +4580,7 @@ def pitchToXml(self, p: pitch.Pitch): if p.accidental is not None: mxAlter = SubElement(mxPitch, 'alter') mxAlter.text = str(common.numToIntOrFloat(p.accidental.alter)) - _setTagTextFromAttribute(p, mxPitch, 'octave', 'implicitOctave') + _setTagTextFromAttribute(p, mxPitch, 'octave') return mxPitch def unpitchedToXml(self, @@ -7397,7 +7397,7 @@ def keySignatureToXml(self, keyOrKeySignature: key.KeySignature) -> Element: # TODO: key-accidental for i, p in enumerate(keyOrKeySignature.alteredPitches): - if p.octave is not None: + if not p.octaveIsImplicit: mxKeyOctave = SubElement(mxKey, 'key-octave') mxKeyOctave.text = str(p.octave) mxKeyOctave.set('number', str(i + 1)) diff --git a/music21/note.py b/music21/note.py index 9c0ff2a282..1d69923fce 100644 --- a/music21/note.py +++ b/music21/note.py @@ -5,7 +5,7 @@ # Authors: Michael Scott Asato Cuthbert # Christopher Ariza # -# Copyright: Copyright © 2006-2024 Michael Scott Asato Cuthbert +# Copyright: Copyright © 2006-2026 Michael Scott Asato Cuthbert # License: BSD, see license.txt # ------------------------------------------------------------------------------ ''' @@ -1446,10 +1446,10 @@ class Note(NotRest): >>> n = note.Note('B-') >>> n.name 'B-' - >>> n.octave is None - True - >>> n.pitch.implicitOctave + >>> n.octave 4 + >>> n.pitch.octaveIsImplicit + True >>> n = note.Note(name='D#') >>> n.name @@ -1657,15 +1657,17 @@ def step(self, value: StepName): self.pitch.step = value @property - def octave(self) -> int|None: + def octave(self) -> int: ''' Return or set the octave value from the :class:`~music21.pitch.Pitch` object. See :attr:`~music21.pitch.Pitch.octave`. + + * Changed in v11: always an int; see :attr:`~music21.pitch.Pitch.octaveIsImplicit`. ''' return self.pitch.octave @octave.setter - def octave(self, value: int|None): + def octave(self, value: int|float|None): self.pitch.octave = value @property @@ -1886,7 +1888,7 @@ def __init__( if displayName: display_pitch = Pitch(displayName) self.displayStep = display_pitch.step - self.displayOctave = display_pitch.implicitOctave + self.displayOctave = display_pitch.octave def _reprInternal(self): if not self.storedInstrument: diff --git a/music21/pitch.py b/music21/pitch.py index feb281a4da..dba07baa3e 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -5,7 +5,7 @@ # Authors: Michael Scott Asato Cuthbert # Christopher Ariza # -# Copyright: Copyright © 2008-2019 Michael Scott Asato Cuthbert +# Copyright: Copyright © 2008-2026 Michael Scott Asato Cuthbert # License: BSD, see license.txt # ------------------------------------------------------------------------------ ''' @@ -579,7 +579,7 @@ def _dissonanceScore(pitches: list[Pitch], intervals = [] for p1, p2 in itertools.combinations(pitches, 2): p2 = copy.deepcopy(p2) - p2.octave = None + p2.octaveIsImplicit = True this_interval = interval.Interval(noteStart=p1, noteEnd=p2) intervals.append(this_interval) except interval.IntervalException: @@ -1681,31 +1681,29 @@ class Pitch(prebase.ProtoM21Object): >>> alters [1.0, -1.0] - If a `Pitch` doesn't have an associated octave, then its - `.octave` value is None. This means that it represents - any G#, regardless of octave. Transposing this note up - an octave doesn't change anything. + A `Pitch` created without an octave represents any G#, regardless + of octave: it prints without an octave number, and transposing it + up an octave changes nothing. >>> anyGSharp = pitch.Pitch('G#') - >>> anyGSharp.octave is None - True + >>> anyGSharp + >>> print(anyGSharp.transpose('P8')) G# - Sometimes we need an octave for a `Pitch` even if it's not - specified. For instance, we can't play an octave-less `Pitch` - in MIDI or display it on a staff. So there is an `.implicitOctave` - tag to deal with these situations; by default it's always 4 (unless - defaults.pitchOctave is changed) + Yet an octave is often needed anyhow, to play the `Pitch` in MIDI or + put it on a staff, so `.octave` is always an integer: the default octave, + 4 (`defaults.pitchOctave`), when none was given. `.octaveIsImplicit` + tells the two cases apart. - >>> anyGSharp.implicitOctave + >>> anyGSharp.octave 4 - - If a `Pitch` has its `.octave` explicitly set, then `.implicitOctave` - always equals `.octave`. - - >>> highEflat.implicitOctave + >>> anyGSharp.octaveIsImplicit + True + >>> highEflat.octave 6 + >>> highEflat.octaveIsImplicit + False If an integer or float >= 12 is passed to the constructor then it is used as the `.ps` attribute, which is for most common piano notes, the @@ -1724,7 +1722,7 @@ class Pitch(prebase.ProtoM21Object): >>> p2 = pitch.Pitch(3) >>> p2 - >>> p2.octave is None + >>> p2.octaveIsImplicit True Since in instantiating pitches from numbers, @@ -1846,8 +1844,8 @@ class Pitch(prebase.ProtoM21Object): and cannot be put into Streams ''' # define order for presenting names in documentation; use strings - _DOC_ORDER = ['name', 'nameWithOctave', 'step', 'pitchClass', 'octave', 'midi', 'german', - 'french', 'spanish', 'italian', 'dutch'] + _DOC_ORDER = ['name', 'nameWithOctave', 'step', 'pitchClass', 'octave', 'octaveIsImplicit', + 'midi', 'german', 'french', 'spanish', 'italian', 'dutch'] # documentation for all attributes (not properties or methods) # _DOC_ATTR: dict[str, str] = { # } @@ -1934,12 +1932,7 @@ def __init__(self, # 5% of pitch creation time; it'll be created in a sec anyhow self._microtone: Microtone|None = None - # # CA, Q: should this remain an attribute or only refer to value in defaults? - # # MSC A: no, it's a useful attribute for cases such as scales where if there are - # # no octaves we give a defaultOctave higher than the previous - # # MSC 12 years later: maybe Chris was right! - # self.defaultOctave: int = defaults.pitchOctave - # # MSC: even later: Chris Ariza was right + # None means implicit: .octave then reports defaults.pitchOctave self._octave: int|None = None # if True, accidental is not known; is determined algorithmically @@ -2046,7 +2039,7 @@ def __eq__(self, other: object) -> bool: ''' if not isinstance(other, Pitch): return NotImplemented - if (self.octave == other.octave + if (self._octave == other._octave and self.step == other.step and self.accidental == other.accidental and self.microtone == other.microtone): @@ -2085,7 +2078,7 @@ def __hash__(self) -> int: self.fundamental, self.spellingIsInferred, self.microtone, - self.octave, + self._octave, self.step, type(self), ) @@ -2592,15 +2585,15 @@ def ps(self) -> float: >>> print(f'{p.ps:.1f}') 60.2 - Octaveless pitches use their .implicitOctave attributes: + A pitch without an octave of its own uses the default octave, 4: >>> d = pitch.Pitch('D#') - >>> d.octave is None + >>> d.octaveIsImplicit True - >>> d.implicitOctave - 4 >>> d.ps 63.0 + >>> d.ps == pitch.Pitch('D#4').ps + True >>> d.octave = 5 >>> d.ps @@ -2628,7 +2621,7 @@ def ps(self) -> float: or self.accidental are changed. ''' step = self._step - ps = float(((self.implicitOctave + 1) * 12) + STEPREF[step]) + ps = float(((self.octave + 1) * 12) + STEPREF[step]) if self.accidental is not None: ps = ps + self.accidental.alter if self._microtone is not None: @@ -2892,9 +2885,9 @@ def nameWithOctave(self) -> str: Traceback (most recent call last): music21.pitch.PitchException: Cannot set a nameWithOctave with 'C#' - Set octave to None explicitly instead. + Set `.octaveIsImplicit = True` instead. ''' - if self.octave is None: + if self.octaveIsImplicit: return self.name else: return self.name + str(self.octave) @@ -2922,7 +2915,7 @@ def unicodeNameWithOctave(self) -> str: >>> p.unicodeNameWithOctave 'C♯4' ''' - if self.octave is None: + if self.octaveIsImplicit: return self.unicodeName else: return self.unicodeName + str(self.octave) @@ -2948,7 +2941,7 @@ def fullName(self) -> str: if self.accidental is not None: name += f'-{self.accidental.fullName}' - if self.octave is not None: + if not self.octaveIsImplicit: name += f' in octave {self.octave}' if self._microtone is not None and self.microtone.cents != 0: @@ -3166,31 +3159,42 @@ def pitchClassString(self, v: int|PitchClassString) -> None: @property - def octave(self) -> int|None: + def octave(self) -> int: ''' - Returns or sets the octave of the note. - Setting the octave updates the pitchSpace attribute. + Returns or sets the octave of the Pitch. - >>> a = pitch.Pitch('g') - >>> a.octave is None - True - >>> a.implicitOctave + >>> b = pitch.Pitch('B5') + >>> b.octave + 5 + + Always an int: a Pitch created without an octave reports the default + octave, 4 (`defaults.pitchOctave`), and has `.octaveIsImplicit` True. + + >>> g = pitch.Pitch('g') + >>> g.octave 4 - >>> a.ps ## will use implicitOctave + >>> g.octaveIsImplicit + True + >>> g.ps 67.0 - >>> a.name - 'G' - >>> a.octave = 14 - >>> a.octave - 14 - >>> a.implicitOctave + Setting the octave updates `.ps` and makes the octave explicit: + + >>> g.octave = 14 + >>> g.octave 14 - >>> a.name - 'G' - >>> a.ps + >>> g.octaveIsImplicit + False + >>> g.ps 187.0 + + To make the pitch octaveless, set `.octaveIsImplicit = True`. Setting + `.octave = None` is deprecated and will be removed in v13. + + * Changed in v11: always an int; `.octaveIsImplicit` says whether it was given. ''' + if self._octave is None: + return defaults.pitchOctave return self._octave @octave.setter @@ -3198,28 +3202,64 @@ def octave(self, value: int|float|None) -> None: if value is not None: self._octave = int(value) else: + # None is deprecated, removed in v13. Add real deprecation message here in v12 self._octave = None self.informClient() + @property + def octaveIsImplicit(self) -> bool: + ''' + True if this Pitch was never given an octave, so it stands for its + pitch class in any octave: it prints without an octave number, and + `.octave` reports the default, 4. + + >>> anyFSharp = pitch.Pitch('F#') + >>> anyFSharp.octaveIsImplicit + True + >>> anyFSharp + + >>> anyFSharp.octave + 4 + + Setting `.octave` makes the octave explicit: + + >>> anyFSharp.octave = 5 + >>> anyFSharp.octaveIsImplicit + False + >>> anyFSharp + + + Set it back to True to make the pitch octaveless again: + + >>> anyFSharp.octaveIsImplicit = True + >>> anyFSharp + + + * New in v11. + ''' + return self._octave is None + + @octaveIsImplicit.setter + def octaveIsImplicit(self, value: bool) -> None: + if bool(value) == self.octaveIsImplicit: + return + self._octave = None if value else defaults.pitchOctave + self.informClient() + @property def implicitOctave(self) -> int: ''' - Returns the octave of the Pitch, or defaultOctave if - octave was never set. To set an octave, use .octave. - Default octave is usually 4. + Synonym for `.octave`. >>> p = pitch.Pitch('C#') - >>> p.octave is None - True >>> p.implicitOctave 4 - Cannot be set. Instead, just change the `.octave` of the pitch + * Deprecated in v11: use `.octave`, which is now always an int. + A warning arrives in v12 and the property goes away later. ''' - if self.octave is None: - return defaults.pitchOctave - else: - return self.octave + # Add real deprecation message here in v12 + return self.octave # noinspection SpellCheckingInspection,GrazieInspection @property @@ -4006,7 +4046,7 @@ def isEnharmonic(self, other: Pitch) -> bool: >>> pD4.isEnharmonic(pEbb4) and pD4.step == pEbb4.step False ''' - if other.octave is None or self.octave is None: + if other.octaveIsImplicit or self.octaveIsImplicit: return (other.ps - self.ps) % 12 == 0 else: # if pitch spaces are equal, these are enharmonics @@ -4040,19 +4080,19 @@ def _getEnharmonicHelper(self, if intervalString not in self._transpositionIntervals: self._transpositionIntervals[intervalString] = interval.Interval(intervalString) intervalObj = self._transpositionIntervals[intervalString] - octaveStored = self.octave # may be None + octaveWasImplicit = self.octaveIsImplicit p = intervalObj.transposePitch(self, maxAccidental=None) if not inPlace: - if octaveStored is None: - p.octave = None + if octaveWasImplicit: + p.octaveIsImplicit = True return p else: self.step = p.step self.accidental = p.accidental if p.microtone is not None: self.microtone = p.microtone - if octaveStored is None: - self.octave = None + if octaveWasImplicit: + self.octaveIsImplicit = True else: self.octave = p.octave return None @@ -4231,10 +4271,10 @@ def simplifyEnharmonic( else: # by resetting the pitch space value, we will get a simpler # enharmonic spelling - saveOctave = self.octave + octaveWasImplicit = self.octaveIsImplicit returnObj.ps = self.ps - if saveOctave is None: - returnObj.octave = None + if octaveWasImplicit: + returnObj.octaveIsImplicit = True if mostCommon: if returnObj.name == 'D#': @@ -4466,7 +4506,7 @@ def diatonicNoteNum(self) -> int: >>> b.diatonicNoteNum 0 - An `implicitOctave` of 4 is used if octave is not set: + The default octave, 4, is used as a basis if the pitch had no octave: >>> c = pitch.Pitch('C') >>> c.diatonicNoteNum @@ -4496,7 +4536,7 @@ def diatonicNoteNum(self) -> int: >>> lowLowLowD.diatonicNoteNum -19 ''' - return STEP_TO_DNN_OFFSET[self.step] + 1 + (7 * self.implicitOctave) + return STEP_TO_DNN_OFFSET[self.step] + 1 + (7 * self.octave) @diatonicNoteNum.setter def diatonicNoteNum(self, newNum: int) -> None: @@ -4641,7 +4681,7 @@ def transpose( # pitch attributes # NOTE: in some cases this may not return exactly the proper config self.name = p.name - if self.octave is not None: + if not self.octaveIsImplicit: self.octave = p.octave # manually copy accidental object self.accidental = p.accidental @@ -4725,14 +4765,13 @@ def transposeBelowTarget( * Changed in v3: default for inPlace=False. ''' - if self.octave is None: + if self.octaveIsImplicit: raise PitchException('Cannot call transposeBelowTarget with an octaveless Pitch.') if inPlace: src = self else: src = copy.deepcopy(self) - assert src.octave is not None while True: # ref 20, min 10, lower ref. @@ -4810,14 +4849,13 @@ def transposeAboveTarget(self, * Changed in v3: default for inPlace=False. ''' - if self.octave is None: + if self.octaveIsImplicit: raise PitchException('Cannot call transposeAboveTarget with an octaveless Pitch.') if inPlace: src = self else: src = copy.deepcopy(self) - assert src.octave is not None # case where self is below target while True: diff --git a/music21/roman.py b/music21/roman.py index 52d5df146e..d9de8dc1f4 100644 --- a/music21/roman.py +++ b/music21/roman.py @@ -21,7 +21,6 @@ from music21 import chord from music21 import common -from music21 import defaults from music21 import environment from music21 import exceptions21 from music21.figuredBass import notation as fbNotation @@ -3416,12 +3415,9 @@ def _updatePitches(self) -> None: thisScaleDegree, direction=scale.Direction.ASCENDING)) pitchName = self.figuresNotationObj.modifiers[i].modifyPitchName(newPitch.name) newNewPitch = pitch.Pitch(pitchName) - if newPitch.octave is not None: - newNewPitch.octave = newPitch.octave - else: - newNewPitch.octave = defaults.pitchOctave + newNewPitch.octave = newPitch.octave if newNewPitch.ps < lastPitch.ps: - newNewPitch.octave += 1 # type: ignore + newNewPitch.octave += 1 pitches.append(newNewPitch) lastPitch = newNewPitch @@ -3480,13 +3476,13 @@ def _updatePitches(self) -> None: addedPitch.accidental = pitch.Accidental(alteration) while addedPitch.ps < bassPitch.ps: - addedPitch.octave = addedPitch.implicitOctave + 1 + addedPitch.octave += 1 if (addedPitch.ps == bassPitch.ps and addedPitch.diatonicNoteNum < bassPitch.diatonicNoteNum): # RN('IV[add#7]', 'C') would otherwise result # in E#4 as bass, not E#5 as highest note. - addedPitch.octave = addedPitch.implicitOctave + 1 + addedPitch.octave += 1 if addedPitch not in self.pitches: self.add(addedPitch) diff --git a/music21/scale/__init__.py b/music21/scale/__init__.py index 20ead29ffa..b48563c639 100644 --- a/music21/scale/__init__.py +++ b/music21/scale/__init__.py @@ -98,7 +98,6 @@ # ------------------------- from music21 import base from music21 import common -from music21 import defaults from music21 import environment from music21 import exceptions21 from music21 import note @@ -250,8 +249,7 @@ def extractPitchList( seen.add(hashValue) post.append(p) for p in post: - if p.octave is None: - p.octave = defaults.pitchOctave + p.octaveIsImplicit = False return post @@ -388,8 +386,7 @@ def buildNetworkFromPitches( self.octaveDuplicating = False else: p = copy.deepcopy(pitchListProcessed[0]) - if p.octave is None: - p.octave = p.implicitOctave + p.octaveIsImplicit = False if pitchListProcessed[-1] > pitchListProcessed[0]: # ascending while p.ps < pitchListProcessed[-1].ps: p.octave += 1 @@ -426,10 +423,10 @@ def fixDefaultOctaveForPitchList(pitchList: list[pitch.Pitch]) -> list[pitch.Pit >>> pitchListStrs = 'a b c d e f g a'.split() >>> pitchList = [pitch.Pitch(p) for p in pitchListStrs] - Here's the problem, between `pitchList[1]` and `pitchList[2]` the `.implicitOctave` - stays the same, so the `.ps` drops: + Here's the problem, between `pitchList[1]` and `pitchList[2]` the `.octave` + stays the same (since both pitches have .octaveIsImplicit == True), so the `.ps` drops: - >>> (pitchList[1].implicitOctave, pitchList[2].implicitOctave) + >>> (pitchList[1].octave, pitchList[2].octave) (4, 4) >>> (pitchList[1].ps, pitchList[2].ps) (71.0, 60.0) @@ -438,7 +435,7 @@ def fixDefaultOctaveForPitchList(pitchList: list[pitch.Pitch]) -> list[pitch.Pit one has a .ps above the previous: >>> pl2 = scale.AbstractScale.fixDefaultOctaveForPitchList(pitchList) - >>> (pl2[1].implicitOctave, pl2[2].implicitOctave, pl2[3].implicitOctave) + >>> (pl2[1].octave, pl2[2].octave, pl2[3].octave) (4, 5, 5) >>> (pl2[1].ps, pl2[2].ps) (71.0, 72.0) @@ -452,9 +449,9 @@ def fixDefaultOctaveForPitchList(pitchList: list[pitch.Pitch]) -> list[pitch.Pit ''' # fix defaultOctave for pitchList lastPs: float = 0 - lastOctave = pitchList[0].implicitOctave + lastOctave = pitchList[0].octave for p in pitchList: - if p.octave is None: + if p.octaveIsImplicit: if lastPs > p.ps: p.octave = lastOctave while lastPs > p.ps: @@ -462,7 +459,7 @@ def fixDefaultOctaveForPitchList(pitchList: list[pitch.Pitch]) -> list[pitch.Pit p.octave = lastOctave lastPs = p.ps - lastOctave = p.implicitOctave + lastOctave = p.octave return pitchList diff --git a/music21/scale/intervalNetwork.py b/music21/scale/intervalNetwork.py index 384b181a55..4b7e5eaecc 100644 --- a/music21/scale/intervalNetwork.py +++ b/music21/scale/intervalNetwork.py @@ -1370,13 +1370,14 @@ def nextPitch( if alteredDegrees and degree in alteredDegrees: alterSemitones = alteredDegrees[degree]['interval'].semitones alterSemitonesInt = t.cast('int', alterSemitones) - if ((usedNeighbor and getNeighbor == Direction.DESCENDING) - or (not usedNeighbor and direction == Direction.ASCENDING)): - while p.octave is not None and p.transpose(alterSemitonesInt) > pitchOriginObj: - p.octave -= 1 - else: - while p.octave is not None and p.transpose(alterSemitonesInt) < pitchOriginObj: - p.octave += 1 + if not p.octaveIsImplicit: + if ((usedNeighbor and getNeighbor == Direction.DESCENDING) + or (not usedNeighbor and direction == Direction.ASCENDING)): + while p.transpose(alterSemitonesInt) > pitchOriginObj: + p.octave -= 1 + else: + while p.transpose(alterSemitonesInt) < pitchOriginObj: + p.octave += 1 # pitchObj = p n = self.nodes[foundNodeId] @@ -1487,8 +1488,7 @@ def realizeAscending( nodeObj = t.cast(list[Node], self.nodeNameToNodes(nodeId))[0] # must set an octave for pitch reference, even if not given - if pitchReference.octave is None: - pitchReference.octave = pitchReference.implicitOctave + pitchReference.octaveIsImplicit = False if isinstance(minPitch, str): minPitch = pitch.Pitch(minPitch) @@ -1675,8 +1675,7 @@ def realizeDescending( pitchRef = copy.deepcopy(pitchReference) # must set an octave for pitch reference, even if not given - if pitchRef.octave is None: - pitchRef.octave = 4 + pitchRef.octaveIsImplicit = False # get first node if no node is provided if isinstance(nodeId, Node): @@ -1861,8 +1860,7 @@ def realize(self, pitchRef = copy.deepcopy(pitchReference) # must set an octave for pitch reference, even if not given - if pitchRef.octave is None: - pitchRef.octave = pitchRef.implicitOctave + pitchRef.octaveIsImplicit = False minPitchObj: pitch.Pitch|None if isinstance(minPitch, str): @@ -2383,9 +2381,8 @@ def getRelativeNodeId( else: pitchTargetObj = pitchTarget - saveOctave = pitchTargetObj.octave - if saveOctave is None: - pitchTargetObj.octave = pitchTargetObj.implicitOctave + octaveWasImplicit = pitchTargetObj.octaveIsImplicit + pitchTargetObj.octaveIsImplicit = False # try an octave spread first # if a scale degree is larger than an octave this will fail @@ -2413,8 +2410,8 @@ def getRelativeNodeId( if realizedNode not in post: # may be more than one match post.append(realizedNode) - if saveOctave is None: - pitchTargetObj.octave = None + if octaveWasImplicit: + pitchTargetObj.octaveIsImplicit = True if not post: return None @@ -2459,10 +2456,9 @@ def getNeighborNodeIds( else: pitchTargetObj = pitchTarget - savedOctave = pitchTargetObj.octave - if savedOctave is None: - # don't alter permanently, in case a Pitch object was passed in. - pitchTargetObj.octave = pitchTargetObj.implicitOctave + # don't alter permanently, in case a Pitch object was passed in. + octaveWasImplicit = pitchTargetObj.octaveIsImplicit + pitchTargetObj.octaveIsImplicit = False # try an octave spread first # if a scale degree is larger than an octave this will fail minPitch = pitchTargetObj.transpose(-12, inPlace=False) @@ -2484,8 +2480,8 @@ def getNeighborNodeIds( return lowNeighbor, highNeighbor lowNeighbor = realizedNode - if savedOctave is None: - pitchTargetObj.octave = savedOctave + if octaveWasImplicit: + pitchTargetObj.octaveIsImplicit = True return None def getRelativeNodeDegree( diff --git a/music21/serial.py b/music21/serial.py index b98c6f7da4..c081368943 100644 --- a/music21/serial.py +++ b/music21/serial.py @@ -304,7 +304,7 @@ def __init__(self, row=None, **keywords): else: n = pc - n.pitch.octave = None + n.pitch.octaveIsImplicit = True self.append(n) def _reprInternal(self): @@ -399,7 +399,7 @@ def makeTwelveToneRow(self): n = note.Note() n.duration.quarterLength = 0.0 n.pitch.pitchClass = thisPc - n.pitch.octave = None + n.pitch.octaveIsImplicit = True a.append(n) return a @@ -700,7 +700,7 @@ def matrix(self): n = note.Note() n.duration.quarterLength = 0.0 n.pitch.pitchClass = p - n.pitch.octave = None + n.pitch.octaveIsImplicit = True rowObject.append(n) matrixObj.insert(0, rowObject) @@ -1286,7 +1286,7 @@ def pcToToneRow(pcSet): for thisPc in pcSet: n = note.Note() n.pitch.pitchClass = thisPc - n.pitch.octave = None + n.pitch.octaveIsImplicit = True a.append(n) return a diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 6e49f36179..ef92fb2ff3 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -2909,9 +2909,9 @@ def testMakeAccidentalsOnChord(self): # Repeat the test without octaves and reset state low, high = augmented_octave.pitches - low.octave = None + low.octaveIsImplicit = True low.accidental = None - high.octave = None + high.octaveIsImplicit = True high.accidental.displayStatus = None s2.makeAccidentals(inPlace=True) diff --git a/music21/test/test_pitch.py b/music21/test/test_pitch.py index fbd4379bb8..3716d8e5b5 100644 --- a/music21/test/test_pitch.py +++ b/music21/test/test_pitch.py @@ -5,7 +5,7 @@ # Authors: Michael Scott Asato Cuthbert # Christopher Ariza # -# Copyright: Copyright © 2008-2024 Michael Scott Asato Cuthbert +# Copyright: Copyright © 2008-2026 Michael Scott Asato Cuthbert # License: BSD, see license.txt # ------------------------------------------------------------------------------ from __future__ import annotations @@ -16,6 +16,7 @@ from music21 import common from music21 import converter from music21 import corpus +from music21 import defaults from music21 import key from music21 import note from music21 import pitch @@ -41,6 +42,79 @@ def testOctave(self): b = Pitch('B#3') self.assertEqual(b.octave, 3) + def testOctaveIsImplicit(self): + anyFSharp = Pitch('F#') + self.assertTrue(anyFSharp.octaveIsImplicit) + self.assertEqual(anyFSharp.octave, 4) + self.assertEqual(anyFSharp.implicitOctave, 4) + self.assertEqual(anyFSharp.nameWithOctave, 'F#') + self.assertEqual(anyFSharp.ps, 66.0) + + anyFSharp.octave = 5 + self.assertFalse(anyFSharp.octaveIsImplicit) + self.assertEqual(anyFSharp.octave, 5) + self.assertEqual(anyFSharp.implicitOctave, 5) + self.assertEqual(anyFSharp.nameWithOctave, 'F#5') + + anyFSharp.octaveIsImplicit = True + self.assertEqual(anyFSharp.octave, 4) + self.assertEqual(anyFSharp.nameWithOctave, 'F#') + self.assertEqual(anyFSharp.ps, 66.0) + + # False gives the default octave explicitly + anyFSharp.octaveIsImplicit = False + self.assertEqual(anyFSharp.nameWithOctave, 'F#4') + + # octave = None still forgets the octave + anyFSharp.octave = None + self.assertTrue(anyFSharp.octaveIsImplicit) + self.assertEqual(anyFSharp.octave, 4) + + # Notes proxy the int + self.assertEqual(note.Note('B-').octave, 4) + self.assertEqual(note.Note('B-3').octave, 3) + + # the default is read live from defaults.pitchOctave + savedDefaultOctave = defaults.pitchOctave + try: + defaults.pitchOctave = 3 + self.assertEqual(Pitch('C').octave, 3) + self.assertEqual(Pitch('C').ps, 48.0) + self.assertEqual(Pitch('C5').octave, 5) + finally: + defaults.pitchOctave = savedDefaultOctave + + # creation paths + self.assertTrue(Pitch().octaveIsImplicit) + self.assertTrue(Pitch(3).octaveIsImplicit) # pitch class + self.assertFalse(Pitch(65).octaveIsImplicit) # midi + self.assertFalse(Pitch('C4').octaveIsImplicit) + self.assertFalse(Pitch('C', octave=4).octaveIsImplicit) + self.assertTrue(Pitch(step='D', accidental='#').octaveIsImplicit) + self.assertTrue(note.Note('B-').pitch.octaveIsImplicit) + self.assertFalse(note.Note().pitch.octaveIsImplicit) + + # implicitness survives copying and transposition + anyD = Pitch('D') + self.assertTrue(copy.deepcopy(anyD).octaveIsImplicit) + self.assertTrue(anyD.transpose('M2').octaveIsImplicit) + self.assertTrue(anyD.transpose(3).octaveIsImplicit) + self.assertTrue(anyD.getEnharmonic().octaveIsImplicit) + self.assertFalse(Pitch('D4').transpose('M2').octaveIsImplicit) + + # an implicit octave is not the same as an explicit default octave + self.assertNotEqual(Pitch('C'), Pitch('C4')) + self.assertEqual(Pitch('C'), Pitch('C')) + self.assertNotEqual(hash(Pitch('C')), hash(Pitch('C4'))) + + # the setter informs a Note client only when something changed + n = note.Note('C4') + n._cache['junk'] = 1 + n.pitch.octaveIsImplicit = False + self.assertEqual(n._cache, {'junk': 1}) + n.pitch.octaveIsImplicit = True + self.assertEqual(n._cache, {}) + def testNameSetting(self): with self.assertRaisesRegex(ValueError, r"Cannot have octave given before pitch name in '8D-4'\."):