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

FretBend update #1580

Draft
wants to merge 79 commits into
base: master
Choose a base branch
from
Draft

FretBend update #1580

wants to merge 79 commits into from

Conversation

adhooge
Copy link

@adhooge adhooge commented May 23, 2023

Fixes #1471

Hi! First PR so I expect a few things to be added/modified but here's a first draft.
I've updated the FretBend articulation so that it includes the musicXML properties bendAlter, preBend and release.

It goes round from musicXML to m21 back to mXML.

I've not written any tests yet because I'm not sure what to add. Let me know of what could be good to add!

as a PR newbie, I also have some irrelevant commits that I manually reverted. If that's a problem I'll try to clean that up.

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.

Well on its way! Ask around if you need help in writing tests, and for help on why music21's system of intervals is more complex than musicxml's.

(the changes to note.Note are rejected, so please remove them, thanks!)

@@ -84,6 +84,7 @@
from music21.common.classTools import tempAttribute
from music21 import environment
from music21 import style
from music21 import interval
Copy link
Member

Choose a reason for hiding this comment

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

alphabetical order (existing mistake with style before spanner can be fixed in this PR also)

Copy link
Member

Choose a reason for hiding this comment

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

and if this is added it needs to be removed from they TYPE_CHECKING import below (but I don't think it will need to be, see below)

Copy link
Author

Choose a reason for hiding this comment

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

Done with 3cf71e0

release: t.Optional[float]
withBar: t.Optional[bool]

def __init__(self, number=0, preBend=False, release=None, withBar=None, **keywords):
Copy link
Member

Choose a reason for hiding this comment

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

number is too generic a term -- and we usually reserve the term for identifying which of multiple simultaneous items (like slur number=1 vs slur number=2). Are fret-bends always chromatic intervals? it seems like you can also have a diatonic interval of a fretbend. hence why bendAlter was typed as IntervalBase not ChromaticInterval. It seems better to have bendAlter as described above interval.IntervalBase | None = None where None indicates unspecified. Having a default of 0, becoming ChromaticInterval(0) indicates that this fretBend does not alter the pitch at all, which would be a very strange fret bend. This would also make it so that interval.py does not need to be imported here.

The other keywords should be typed in the __init__ declaration in addition to the class.

We no longer use t.Optional[X] in the code base. Use X | None instead.

Copy link
Author

Choose a reason for hiding this comment

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

Changed with d63c78a

Comment on lines 593 to 600
bend indication for fretted instruments

bend in musicxml

number is the interval of the bend in number of semitones, bend-alter in musicxml
preBend indicates wether the note is prebended or not
release is the offset value for releasing the bend, if Any
withBar indicates if the bend is done using a whammy bar movement
Copy link
Member

Choose a reason for hiding this comment

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

Capitalization and use asterisks and line breaks for documentation -- run documentation/build.py and look at the difference between the docs produced here and elsewhere in the system.

preBend indicates wether the note is prebended or not -- I don't know what prebended means.

what is release measured in? Is it an offset (which is relative to the start of the Measure/stream)? That doesn't sound right. Is it a quarterLength measured from the start of the note? or from the end of the note.

withBar doc is fine.

Copy link
Author

Choose a reason for hiding this comment

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

I've tried to improve it in 03e6117
Let me know if that seems good to you

Copy link
Author

Choose a reason for hiding this comment

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

tests are on their way

Comment on lines 5526 to 5527
bendAlterSubElement = SubElement(mxTechnicalMark, 'bend-alter')
bendAlterSubElement.text = str(articulationMark.bendAlter.semitones)
Copy link
Member

Choose a reason for hiding this comment

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

this has enough ifs that it should be brought out as its own small method that allows for easier documentation and testing. articulationMark will probably need to be cast as a FretBend.

We can solve the case of GenericInterval later, which doesn't have .semitones. Perhaps it always needs to be a full Interval object or None.

Copy link
Author

Choose a reason for hiding this comment

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

New submethod added in 85cc6a8
Now I indeed have the problem that I need to retrieve the number of semitones. I could probably do something with GenericInterval.value but I don't know how to do the conversion properly

Comment on lines 5531 to 5532
releaseSubElement = SubElement(mxTechnicalMark, 'release')
releaseSubElement.set('offset', str(articulationMark.release))
Copy link
Member

Choose a reason for hiding this comment

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

musicxml's definition of offset is very different from music21's and (despite me now being editor of the musicxml spec) we shouldn't use musicxml offsets anywhere in music21 -- these need to be converted according to the current divisionsPerQuarter setting.

Copy link
Author

Choose a reason for hiding this comment

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

should work with d93af6d though I need to write the tests

@@ -3881,11 +3881,18 @@ def xmlTechnicalToArticulation(self, mxObj):
if tag in ('heel', 'toe'):
if mxObj.get('substitution') is not None:
tech.substitution = xmlObjects.yesNoToBoolean(mxObj.get('substitution'))
if tag == 'bend':
tech.bendAlter = interval.Interval(int(mxObj.find('bend-alter').text))
Copy link
Member

Choose a reason for hiding this comment

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

this has too many potentials for crashing the processing. What if there is no bend-alter or no bend-alter.text? check that before typing to assign.

(Better to call out into a separate sub-method. This method was designed just for one-to-two lines per technical

Copy link
Author

Choose a reason for hiding this comment

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

sub-method and bend-alter check in 2e1ea43

if mxObj.find('pre-bend') is not None:
tech.preBend = True
if mxObj.find('release') is not None:
tech.release = int(mxObj.find('release').get('offset'))
Copy link
Member

Choose a reason for hiding this comment

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

as above -- offset needs to be converted to music21 quarterLengths.

except clause without Try.

Copy link
Author

Choose a reason for hiding this comment

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

Both fixed with 58eeb04

tech.release = int(mxObj.find('release').get('offset'))
except TypeError:
# offset is not mandatory
tech.release = 0
Copy link
Member

Choose a reason for hiding this comment

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

release is a float. needs to be 0.0

Copy link
Author

Choose a reason for hiding this comment

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

also fixed with 58eeb04

music21/note.py Outdated
Comment on lines 40 to 43
from music21 import articulations

if t.TYPE_CHECKING:
from music21 import articulations
Copy link
Member

Choose a reason for hiding this comment

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

as above -- alphabetical and remove from type-checking if imported at top. (but won't be necessary).

music21/note.py Outdated
Comment on lines 1788 to 1789
@property
def string(self):
Copy link
Member

Choose a reason for hiding this comment

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

this new property and all properties below are rejected. Note is the most common object in music21 and users who are not working with guitar music will never use these routines but will need to figure out what they mean in the docs. (we don't have "accent" or "staccato" etc. either) -- and parallel routines would need to be added to Chord, etc.

In general for any mature open-source project, don't make additions or substantive changes to core parts of the system without a prior discussion.

Copy link
Author

Choose a reason for hiding this comment

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

oops sorry about that, they were just for my personal convenience. Removed with 0b2074f

@mscuthbert
Copy link
Member

Hello -- just as a note (from the music21list Google Group) I'm taking a sabbatical from reviewing music21 issues and PRs until at least Jan 1, 2024, so this PR will be deferred until then.

@adhooge
Copy link
Author

adhooge commented Jul 10, 2023

Hello Michael, sorry for not being able to finish this PR sooner. Enjoy your sabbatical! Hopefully my PR will be ready for approval when you come back 🤞

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.

Hi! It's again a Request Changes, but they're really minimal changes and I think this is very close to getting in. :-) CONGRATS on the amazing work so far and for putting up with a sabbatical that interrupted it!

>>> fb.release
0.5
'''
bendAlter: interval.IntervalBase | None
Copy link
Member

Choose a reason for hiding this comment

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

since we need to have a number of semitones, maybe make this interval.Interval | interval.ChromaticInterval | None so that if it's not None it definitely has a number of semitones.

I'm not a guitar player so forgive me, but is there a concept of an indefinitely sized bend upwards or downwards? where the direction is known but not the size? (this could be something to put in later)


WithBar indicates if the bend is done using a whammy bar movement. Defaults to False.

>>> fb = articulations.FretBend(bendAlter=interval.ChromaticInterval(-2), release=0.5)
Copy link
Member

Choose a reason for hiding this comment

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

add number to this to show that it appears in the fb representation below.

release: float | None
withBar: bool

def __init__(self, number=0, bendAlter=None, preBend=False, release=None, withBar=False, **keywords):
Copy link
Member

Choose a reason for hiding this comment

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

type needs to be repeated here. Also make all but number keyword only so people don't have the remember the order.

def __init__(
    self, 
    number: int = 0, 
    *,
    bendAlter: interval.Interval | interval.ChromaticInterval | None = None, 
    preBend: bool = False, 
    release: OffsetQL | None = None, 
    withBar: bool = False, 
    **keywords
):```

withBar: bool

def __init__(self, number=0, bendAlter=None, preBend=False, release=None, withBar=False, **keywords):
super().__init__(**keywords)
Copy link
Member

Choose a reason for hiding this comment

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

bug. needs to be super().__init__(number=number, **keywords)

Comment on lines +5512 to +5514
# musicxml expects a number of semitones but not sure how to get it
# from a GeneralInterval
pass
Copy link
Member

Choose a reason for hiding this comment

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

now with the change in typing above this can be possible.

# hammer-on and pull-off not implemented because handled
# in method objectAttachedSpannersToTechnicals of m21ToXml.py
# ('hammer-on', articulations.HammerOn),
# ('pull-off', articulations.PullOff),
# bend not implemented because it needs many subcomponents
# ('bend', articulations.FretBend),
('bend', articulations.FretBend),
Copy link
Member

Choose a reason for hiding this comment

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

this line appears to be added twice? (see line 70)

self.setPlacement(mxObj, tech)
return tech
else:
environLocal.printDebug(f'Cannot translate {tag} in {mxObj}.')
return None

@staticmethod
Copy link
Member

Choose a reason for hiding this comment

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

Not going to be able to be a staticmethod -- see below.

'''
bendAlter: interval.IntervalBase | None
preBend: bool
release: float | None
Copy link
Member

Choose a reason for hiding this comment

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

instead of float, type as OffsetQL which is float|Fraction Music21 represents 1/3 as Fraction(1, 3) not 0.3333333428 as an inexact float.

# divisions relative to the current note.
releaseSubElement = SubElement(mxh, 'release')
quarterLengthValue = bend.release
divisionsValue = defaults.divisionsPerQuarter * quarterLengthValue
Copy link
Member

Choose a reason for hiding this comment

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

divisionsValue = int(defaults.divisionsPerQuarter * quarterLengthValue)

Musicxml "prefers" that offset be an integer:
https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/divisions/

Comment on lines +3885 to +3886
divisions = float(mxh.find('release').get('offset'))
bend.release = divisions / defaults.divisionsPerQuarter
Copy link
Member

Choose a reason for hiding this comment

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

In writing to musicxml, music21 uses defaults.divisionsPerQuarter. But in reading from musicxml, we need to use the divisions defined earlier in the score, which is in self.divisions on the measure parser.

Write

bend.release = opFrac(divisions / defaults.divisionsPerQuarter)

opFrac will convert it to Fraction(1, 3) if it's released within a triplet, etc. and leave it alone as 0.5 if it's a regular eighth note, etc.

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.

Improve management of bends
2 participants