Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance kern grace note importing, fix durations of partially notated chords #1706

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

alexandermorgan
Copy link
Contributor

This pr is a rework of a previous pr that I accidentally closed when resyncing my fork, sorry if that's confusing. I've addressed the comments that were made on that previous pr. This enhances kern grace note imports to allow "qq" to mean an unslashed grace note, and for the unlinked duration to take that of the written note if one is provided. This makes the unlinked duration of imported kern grace notes behave the same everywhere else in music21. Also, for kern chords with durations only specified on the first note in the chord ("8C E G"), non-first notes will now reuse that duration instead of defaulting to a quarter note duration. I also added 4 tests to the spineParser.py file to cover all of this new import behavior.

This pr also adds types to a couple functions in spineParser.py. I had a bit of a hard time with the mypy types because my local mypy setup doesn't give the same exact output as the CI test does. Please be aware that I had to use the comment # type: ignore 4 times in this file so I hope that's not an issue.

Additionally, this pr separates duration processing into its own function, and decreases the overall import time for kern scores by ~12%. The speed up is primarily due to the following:

  1. Move duration parsing of notes to the beginning of hdStringToNote so that notes and rests can be instantiated with their correct duration from the beginning rather than being created and then having their duration replaced. This prevents an unused default duration from being created on each note and rest.
  2. Combine the normal duration and rational duration regexes into one. Likewise for note name and accidental searches.
  3. Place variables such tat duplicate operations are avoided.
  4. Comment out parsing that only leads to pass statements.

The previous version of this pr also included memoization and safe short-circuiting, but those were discarded in favor of simplicity. That earlier pr also replaced str.count('x') with if x in str where possible but that has already been addressed by this pr.

Looking quickly, this same basic idea of figuring out the duration part before creating a note could apply in tinyNotation, abc, and m21ToXml.py->GeneralObjectExporter.fromDuration/fromScale/fromDiatonicScale
And a similar idea, but figuring out pitch first, might apply in lots of places.

Now when reading a kern file, a grace note will have its unlinked duration
type match the note's notated notation in the file. Also, specifying a
grace note with 'qq' instead of just 'q' will omit the slash on the
accidental. Also, when reading chords where only the first note of the
chord specifies its duration, the non-first notes in the chord will
inherit this duration. Similar to how a chord object uses the same
literal duration object of the first note (not just a copy), these
non-first notes that don't have their own specified duration also reuse
the first note's duration object.
@coveralls
Copy link

Coverage Status

coverage: 93.04% (+0.007%) from 93.033%
when pulling 5027404 on alexandermorgan:master
into 3fae7a1 on cuthbertLab:master.

Copy link
Member

@mscuthbert mscuthbert left a comment

Choose a reason for hiding this comment

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

This is a lot closer than the last PR. Congrats. But I still have some questions esp. about defaultDuration (and make sure that you're working with an IDE that does indentation that follows the house style).

eventChord = chord.Chord(chordNotes, beams=chordNotes[-1].beams)
eventChord.duration = chordNotes[0].duration

eventChord = chord.Chord(chordNotes, beams=chordNotes[-1].beams, # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain the type: ignore here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to use type ignore where I couldn't reproduce the music21 type-checking locally and kept failing when pushing even though the types were right. It might have been an issue with things being set to None initially and then replaced with a different type according to some logic. I'll give this typing another try.

Comment on lines +2124 to +2125
def _hdStringToDuration(contents: str,
defaultDuration: duration.Duration|None = None) -> duration.Duration:
Copy link
Member

Choose a reason for hiding this comment

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

lint -- inconsistent indentation.

I still don't understand why a routine called XToDuration would take in a Duration object? This does not look right or make sense to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking back at my pr description, it looks like I didn't copy over my previous example. The issue is that you occasionally get a humdrum chord in one voice that only notates the duration of the first note, as in:
8C E G
Humdrum at least allows for this and the Humdrum Verovio Viewer would parse these three notes all as eighth notes. Here's a minimal example showing this behavior. music21 currently parses the C as an eighth note but the E and the G are seen as not having a duration so they get the default duration of a duration object, which is a quarter note. A quarter note is a reasonable default duration to have in music21 but not in this circumstance. In this case it is helpful to be able to say "in humdrum chords, use the first note's duration as the default duration for the other notes in the chord if they don't specify one themselves". This is just like what music21 does for the chord object itself. The chord object not only has the same duration as the first note, it uses the same literal duration object. So this pr extends that same behavior to the other notes in the chord if they don't specify their own duration. So 8C E G would be a chord of three eighth notes but 8C 2E 2G would be a chord of one eighth note and two half notes. I encountered this issue in a Joplin rag kern file, but I don't know how common it is in general. Tests are also added to check this new behavior.

if (qCount := contents.count('q')):
thisObject.getGrace(inPlace=True)
if qCount == 2:
thisObject.duration.slash = False # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

why type ignore -- this generally means that there's an error in the code logic. Please remove these.

Copy link
Member

Choose a reason for hiding this comment

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

My guess is that a GraceDuration should be created separately and then typed in order to change its slash value. (It seems like an error on music21's logic in some way -- the slash is really on the GeneralNote not on the duration itself... but that's a larger PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I tried to avoid creating a new grace note separately because that may have created an extra or even two extra duration objects. This seemed like a great application of the inPlace parameter. I'll double-check this typing issue as well.

Comment on lines +2285 to +2286
# Determine duration part first to avoid making an unused duration
thisDuration = _hdStringToDuration(contents, defaultDuration)
Copy link
Member

Choose a reason for hiding this comment

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

Is this line supposed to be something like:

if defaultDuration:
    thisDuration = defaultDuration
else:
    thisDuration = _hdStringToDuration(contents)

??? this is the part I'm most confused by.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is that the defaultDuration argument you pass will be None most of the time, but in the case of chords, you pass the duration object of the first note in the chord. Then, you check the second note's duration part of its string. If the second note has a duration part to its string, then you use that no matter what it is. It's only if the second note doesn't specify its own duration that we want to use the first note's duration. But using your if/else above, that would use the first note's duration (the defaultDuration argument) for all other notes in the chord no matter what. Maybe the problem is just a naming one, does fallbackDuration make more sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @mscuthbert does my explanation above clarify this? Would you be ok with calling it fallbackDuration? Passing the first note's duration to all other notes in that chord is key to fixing the bug explained in my "minimal example" given in a comment above. I'm on board with all your other comments, so if we can agree on this then I can update the pr.

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