Skip to content

Find the next pitch of a cyclical scale where it is - #2057

Open
float3 wants to merge 3 commits into
cuthbertLab:masterfrom
float3:next-pitch-on-cycles
Open

float3 wants to merge 3 commits into
cuthbertLab:masterfrom
float3:next-pitch-on-cycles

Conversation

@float3

@float3 float3 commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

nextPitch on a cyclical scale could start below the origin:

>>> sc = scale.CyclicalScale('d3', ['M3'])
>>> sc.nextPitch('c4')
<music21.pitch.Pitch F#3>

Non-octave-duplicating networks now find the node's pitch near the origin (D4 here). About 15% slower for cyclical scales; others unchanged.

AI-assisted (Claude).

nextPitch spelled the found node's degree from the scale's own tonic and
moved it into the origin's octave. That holds when the network repeats
at the octave, but in a cyclical one a degree stands for more than one
pitch class: every note of a cycle of major thirds is the same node. So
the walk began on the wrong pitch.

    >>> sc = scale.CyclicalScale('d3', ['M3'])
    >>> sc.nextPitch('g4')
    <music21.pitch.Pitch F#4>
    >>> sc.nextPitch('c4')
    <music21.pitch.Pitch F#3>

For a network that is not octave-duplicating, take the node's pitch from
a realization around the origin instead. Octave-duplicating networks are
unchanged.
@coveralls

coveralls commented Oct 4, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 93.368% (+0.002%) from 93.366% — float3:next-pitch-on-cycles into cuthbertLab:master

@mscuthbert

Copy link
Copy Markdown
Member

Is that example a before or after? If after it doesn't seem right.

@float3

float3 commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

that's before.
after is this:

>>> sc = scale.CyclicalScale('d3', ['M3'])
>>> sc.nextPitch('c4')
<music21.pitch.Pitch D4>

@mscuthbert

Copy link
Copy Markdown
Member

Oh good!

@mscuthbert mscuthbert left a comment

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.

Some requests for clarification etc. thanks!

Comment thread music21/scale/intervalNetwork.py Outdated
Comment on lines +1438 to +1440
Read off a realization around the origin, since in a network that does
not repeat at the octave one node can stand for several pitch classes:
every note of a cycle of major thirds is the same node.

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.

This comment doesn't seem right "every note of a cycle of major thirds is the same node" -- say we start on D3 shouldn't all Ds be the same node, and all F#s be the same, but not that all Ds are the same as all F#s?

Comment on lines -1341 to -1347
# realize the pitch from the found node degree
# we may be getting an altered
# tone, and we need to transpose an unaltered tone, thus
# leave out altered nodes argument
p = self.getPitchFromNodeDegree(
pitchReference=pitchReference,
nodeName=nodeName,

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.

In the future, it would be a big help to my review if you put GH comments like "moved into the else: below" so I'm not wondering why all this is gone. Thanks!

Comment thread music21/scale/intervalNetwork.py Outdated
Comment on lines +1359 to +1360
# we may be getting an altered
# tone, and we need to transpose an unaltered tone, thus

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.

again -- this is a great argument for removing alteredDegrees altogether. --we might be able to use _nodePitchNearOrigin for all.

Comment thread music21/scale/intervalNetwork.py Outdated
or (not usedNeighbor and direction == Direction.ASCENDING))
p: pitch.Pitch|None
if not self.octaveDuplicating:
p = self._nodePitchNearOrigin(pitchReference,

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.

preexisting error, but seems odd that CyclicalScale(..., ['M3']) doesn't create an octaveDuplicating network. 'P4' I get...

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.

If this is true, but a note here that this path also works for octaveDuplicating scales but it is slower. I'm wondering why we don't simplify to one pathway and then optimize the hell out of that instead? :-)


fifths = scale.CyclicalScale('c4', ['P5'])
self.assertEqual(str(fifths.nextPitch('g4')), 'D5')
self.assertEqual(str(fifths.nextPitch('g4', Direction.DESCENDING)), 'C4')

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.

Thanks for doing this test too -- important to see non-octave repeated here.

Can we also add a test on multiple intervals that don't add up to an octave, like scale.CyclicalScale('d3', ['M3', 'm3']) so we can be sure that we're respecting the different intervals.

And also nextPitch tests on enharmonics (both higher DNN and lower DNN) still go to the correct pitches. thirds nextPitch on C##4 goes to F#4 not D4, etc.

Thanks!

float3 added 2 commits October 5, 2026 11:11
nextPitch now takes the node's pitch from a realization around the origin
for octave-duplicating networks too, which removes the second path. It
also corrects enharmonics across an octave boundary there:
MajorScale('c4').nextPitch('b#3') was D3 and is D4.

Tests for a cycle of two intervals and for enharmonic origins, and a
clearer docstring for _nodePitchNearOrigin.
realizeAscending cached a realization under its exact range, so every
new origin given to nextPitch walked its two octaves again. The walk
depends only on its node, its starting pitch and the altered degrees,
so it is now kept under those and extended as far as a range needs,
and each range is filtered from it. Networks pickled before the walks
existed make theirs on first use.

nextPitch on a fresh scale over 36 origins, against master: MajorScale
1045 -> 393 us, HarmonicMinorScale 1141 -> 419 us, CyclicalScale M3
558 -> 348 us. A single getPitches is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants