Repository navigation
Conversation
The in-place copy-back took the transposed copy's microtone only when it was not None, so a microtone the transposition removed stayed: C4(+50c) up a ChromaticInterval(0.5) in place gave C#4(+50c), while the transposed copy is C#4. AI-assisted (Claude)
Faster copies of slotted objects, Accidental, and Pitch
Find the next pitch of a cyclical scale where it is
…place-microtone fix Pitch.transpose(inPlace) was keeping extra microtones
…aves Fix caching problem / remove ref to defaultOctave
_parseName is now _cacheParsedName: it only parses and remembers (step, accidental string, octave). The .name setter reads the cache after a miss too, so it sets the attributes and informs the Note once even the first time, and a name that fails to parse leaves the pitch unchanged (before, D$ changed the step to D and then raised). Names over 100 characters raise a ValueError, so the cache cannot hold huge strings. setStyleAttributes returns early for a tag with no attributes (most <accidental>, <stem>, <type> tags): <accidental>natural</accidental> 1378 -> 491 ns; about 1-3% on whole MusicXML files. AI-assisted (Claude)
Cache parsed Pitch names; Speedup MusicMXL Style parsing
romanNumeralFromChord(..., preferSecondaryDominants=True) relabelled chords that belong to the key, although the docstring says only chromatically altered chords are changed: I in major became V/IV, III in minor V/VI, and IV in minor V/VII. The substitution now only runs when the chord has a pitch outside the key's scale. In minor, the raised sixth and seventh count as diatonic. Fixes cuthbertLab#2053 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings Compare each pitch with the key signature instead of building the scale with keyObj.pitches and transposing the sixth and seventh; same results, about 100 times faster. The test now checks every chord with and without preferSecondaryDominants, including two chords whose figure changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atonic-check (AI) preferSecondaryDominants: skip chords diatonic to the key
…-octave-change (AI) Read MusicXML octave-change in transpose without diatonic
allParts() sorted the parts by their instrument's partId, which raised a TypeError when the parts had no partId (as with MIDI imports) and put P10 before P2. Use the order of the parts in the source score instead, falling back to partId for parts that are not in the source. Fixes cuthbertLab#2074
…le-keysig (AI) Braille: read a lone key signature in a keyboard segment heading
A trailing blank or tab-only line in a DCML TSV file, such as one left
by copy-pasting a table from GitHub, raised a bare IndexError (or a
ValueError from int('')) in populateFromRow. Padded cells from the same
copy-paste left every header name unmatched, so the file then failed in
key.Key(''). _importTsv now strips each cell and drops rows with no
content.
A file with no chord rows after the header crashed in prepStream at
chordList[0]; it now raises TsvException, a Music21Exception.
Fixes cuthbertLab#1541
…order (AI) Order TimespanTree.allParts() by the source score
A library should not pin its dependency tree; CI now resolves fresh from pyproject.toml (uv sync without --locked). AI-assisted (Claude)
- Pin ruff 0.17.1, mypy 2.4.0, pylint 4.1.3 so CI is stable without uv.lock. - mypy 2 renamed the old allow_redefinition behavior to allow_redefinition_old; the new flexible mode caused 28 spurious errors. - Levenshtein now ships types and ignores isjunk; pass None. - Remove placeholder tests flagged by pylint's redundant-unittest-assert. AI-assisted (Claude)
Remove uv.lock from repo
AI-assisted (Claude)
- Two doctests matched error text that 3.15 rewords; loosen with ellipses. - actions/checkout and setup-python v7 (Node 20 deprecation warnings). - Only run_tests saves the uv cache, avoiding the reserve-cache race. AI-assisted (Claude)
Add support for Python 3.15
coverage.py uses sys.monitoring by default on 3.14+, far faster than the 3.13 settrace core (3.13 coverage job took ~9 min vs ~3 for others). AI-assisted (Claude)
Use faster CI coverage library
Written by an AI agent (Chris, raisingchris2026), working alone.
…rline-segno (AI) Read segno and coda inside a MusicXML barline
The numeral was matched at the start of the figure and then every I,
V, i, v or N anywhere in it was deleted, so a numeral past seven read
as a smaller one with no error:
roman.RomanNumeral('VIII', 'C').romanNumeral # 'VII'
roman.RomanNumeral('IX', 'C').pitches # a C major triad
Only the matched numeral is removed now, and a numeral followed by
another numeral letter raises RomanNumeralException, worded like other
invalid figures.
The Monteverdi madrigal 5.1 analysis wrote VIII for the C major chord
in d minor at m. 28; it is VII, which is also how it was read.
…nstruments-2 Deduplicate instruments offset by offset
…erals-2 Reject roman numerals past VII
…asure-padding-2 # Conflicts: # music21/braille/segment.py
Like the first-staff test, it compares each measure's list of staff numbers in one assertion, so a failure shows which measure lost which staff.
A very long note or a high measure number in a small file made music21 build one measure per bar without limit, until it ran out of memory: - a 37-byte MIDI file with one note of 2**28 ticks at 1 tick per quarter (converter.parse runs makeMeasures), - MusicXML with <duration>99999999</duration> at 1 division (write() runs makeTies, which appends measures for the overflow), - RomanText "Time Signature: 4/4\nm1 C: I\nm99999999 V" (the skipped measures are filled one by one). makeMeasures, makeTies and the RomanText measure filler now raise StreamException / RomanTextTranslateException once a part would have more than defaults.maxMeasures (100,000) measures. makeMeasures and makeTies check a lower bound (remaining length over the longest bar in the meter stream) before each measure, so a single long note fails at once and no input makes more than the limit. The longest corpus piece has about 1,100 measures.
…gap-2 Join PartStaffs after a measure missing from the first staff
Stop making more than 100,000 measures from malformed input
…ure-padding-2 Keep an explicit zero quarterLength in GeneralNote
misc cleanup changes that didn't make it into the recent clef bug fix
Misc clef cleanup
Conflicts: - AGENTS.md: kept master's testing rules and the branch's note on preexisting failures. - _version.py, base.py: took master's 11.0.0b15. - duration.py Duration.__eq__: kept master's expressionIsInferred docs and linked/expressionIsInferred checks with the branch's isinstance and isGrace checks; both Changed-in markers; both new tests. - meter/core.py: kept the branch's Changed-in markers without master's dropped trailing blank line; kept the branch's removal of _subdivideNested. Annotate qPos in offsetToIndex and pos in getAccent as OffsetQL: mypy failed on them once duration became a FrozenDuration.
MeterTerminal and MeterSequence are slotted and immutable on this branch, so streams pickled by 11.0.0b15 cannot be loaded. The branch bumped b8 to b9 for the same reason before master moved past it.
Say what the code does in fewer words; drop history and comments that restate the code. Fix claims that no longer hold: weight may be an int, subdivide is not the only method that returns a new object, and getLevelWeight has no _weight to mention. Add doctests for subdivide, makeSequence and a withWeight example with unequal parts.
Shorten the cache and load comments, the barDuration note and the getBeatDepth example text. beatDuration and beatDivisionDurations name FrozenDuration in their first line, matching what they return.
Shorten the Duration.__eq__ text and the FrozenDuration unfreeze, augmentOrDiminish, splitDotGroups and __copy__ docs and comments.
Shorten the beatDuration unfreeze example text and the FrozenDuration comments in the duration setter.
Drop comments that restate the assertion below them; shorten the testAssignFrozenDurationStaysFrozen docstring.
ProtoM21Object comes before MeterCore in the MRO of both subclasses, and each subclass defines its own _reprInternal, so this one never ran.
The setter rebuilt the display sequence through makeSequence and the private _partition; modify(summedNumerator=value) does the same through the public method.
_setDefaultAccentWeights called setIndex once per accent. Each call copied and hashed the whole partition and left an intermediate sequence in _meterSequenceCache, so a meter with n accents did O(n^2) work and cached n sequences that nothing uses. Build the weighted terminals in a list and call modify(partition=...) once. Accent, beat and beam sequences for 153 meters match master (apart from the int/float default weight this branch already changes).
inspect.stack() reads the source file and line for every frame, which made each attribute set inside FrozenDuration.__init__ take milliseconds. With MeterTerminal.duration returning a FrozenDuration, the first TimeSignature of each meter took about 30 ms here (master: 1.5 ms). Following f_back finds the same __init__/__new__/__setstate__ frame without reading files: about 6 ms, and music21/meter tests run in 2 s instead of 11 s.
_subdivideNestedByPaths replaced one path at a time through setIndex, so each replacement copied and hashed every sequence above it, and cached each intermediate root. For n beats that is O(n^2) per level. Collect the replacements and rebuild only the sequences above them, once. subdivideNestedHierarchy gives the same sequences and weights for 108 cases (12 meters, depths 1-4, both normalizeDenominators settings, and a beatSequence as firstPartitionForm).
partition, partitionByCount, partitionByList, partitionByOtherMeterSequence, subdividePartitionsEqual, subdivideNestedHierarchy, and setLevelWeight changed the sequence in place before v11. Under the old names, a call such as ts.beatSequence.partition(6) now did nothing. The methods are now partitioned, partitionedByCount, partitionedByList, partitionedByOtherMeterSequence, subdividedPartitionsEqual, subdividedNestedHierarchy, and withLevelWeight; the old names raise a MeterException naming the new one. load() raises a MeterException and item assignment a TypeError that name the replacement.
getMeterTerminal and makeSequence keep the 1024 and 4096 most recently used objects with lru_cache, and TimeSignature.load keeps the defaults for the 512 most recently used meters. The music21/meter tests make 440 terminals, 2950 sequences, and 94 meters; 161 corpus scores make 116, 376, and 27.
FrozenObject.__hash__ hashed every slot, recursively, on each call. A
subclass that has a _frozenHash slot now keeps its hash there.
_getSlotsRecursive leaves _frozenHash out, so equality, pickling, and the
hash itself ignore it. hash(TimeSignature('4/4').beatSequence) goes from
8.3 us to 0.14 us.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For #1972.
Duration.__eq__(kept master'slinkedandexpressionIsInferredchecks with this branch'sisinstanceandisGracechecks), version (11.0.0b16, since pickles of the old MeterSequence no longer load), AGENTS.md, and twoChanged inlines in meter/core.py. TwoOffsetQLannotations for mypy.MeterCore._reprInternalnever ran (ProtoM21Object is first in the MRO); removed.summedNumeratorsetter usesmodify()instead ofmakeSequenceand_partition._setDefaultAccentWeightsandsubdivideNestedHierarchycalledsetIndexonce per accent or beat, which copied and hashed the whole partition each time and cached every step: O(n^2). Each now rebuilds once.FrozenObject._check_initwalksf_backinstead ofinspect.stack(), which read source files on every attribute set inFrozenDuration.__init__. The firstTimeSignatureof a meter goes from about 30 ms to 6 ms; music21/meter tests from 11 s to 1 s.AI-assisted (Claude)