Abjad / abjad

Abjad is a Python API for building LilyPond files. Use Abjad to make PDFs of music notation.
https://abjad.github.io
GNU General Public License v3.0
240 stars 40 forks source link

Questions regarding Abjad's pitch module #1165

Closed levinericzimmermann closed 4 years ago

levinericzimmermann commented 4 years ago

Hi @ all,

after using abjad within some projects and being amazed by its powerful and rich interface, I wondered again if microtonal notation that goes beyond quartertones could be implemented within abjad. For the last notation I used and tweaked this old hack from Gregory for abjad 2.21.

I could understand that within the main development branch there are no plans going beyond 24EDO, since Lilypond neither supports different divisions by default. I have in my mind implementing microtonal accidentals that are added to Lilypond by Ekmelily.

While skimming over the abjad/pitch/ files several questions popped up in my mind regarding particular parts of the code that I didn't quite understood. I would be glad if some of you could find time helping me or if one could recommend me where I could find more detailed explanations regarding my questions:

  1. Is there a particular reason why in abjad/pitch/constants.py some dictionaries have the same content in inverse order like for instance _accidental_abbreviation_to_semitones and _accidental_semitones_to_abbreviation? I mean, wouldn't it be less redundant and easier to maintain if only one of those would be 'hard coded' and the other one defined via a function call that inverts the former dictionary?

  2. I had some difficulties understanding the difference between Pitch and PitchClass. I guess a Pitch has an octave while a PitchClass hasn't? But even then there are some lines that are almost equal, for instance the static method _to_nearest_quarter_tone: again I wondered, wouldn't it be better for the sake of clarity and for an easier life while maintaining the software to try reducing redundant code here or is there any particular reason for having the same code twice?

  3. While trying to understand what actually happens while initializing a new Pitch object in abjad, I constantly had to jump around several files (Pitch, PitchClass, NumberedPitch, NumberedPitchClass, NamedPitchClass, NamedPitch). Whats the reason for dividing Pitch in NumberedPitch and NamedPitch when both Subclasses know mostly the same methods and attributes and are both possible to initialize with the same kind of arguments, e.g. if the only difference for the user is the different representation - method?

Thank you all for your fabulous work! Best,

Levin

GregoryREvans commented 4 years ago

@uummoo I can say that I'm in the middle of some work in the pitch files, but haven't finished because other things get in the way. If you want a peek at what is going on you can look here at the pitch files in the greg/dev branch of abjad and at the lilypond folder in my personal repository.

levinericzimmermann commented 4 years ago

Oh wow those are indeed good news! Is it the plan to integrate those changed pitch files - once you are finished - to the master branch of abjad? And is there anything particular where I could help?

Ps. It's amazing to see that even now it's already possible to generate Lilypond files with the twelfth-tone tuning syntax when using your abjad branch, even though lilypond will by default of course still produce an error when the ekmel*.ily files aren't manually included.

GregoryREvans commented 4 years ago

@uummoo There is a plan to someday integrate these changes. In the meantime, you can include this file and comment out the testing score at the bottom to keep lilypond from sending errors. I have defined accidentals only which can be rendered with the Emmentaler font. Other accidentals are represented by an x. Feel free to also use it in tandem with accidental substitution commands here if you like the way they look.

Screen Shot 2020-02-28 at 12 44 14 PM

Otherwise, keep using ekmel.ily for now. Also note that some mechanics in the pitch files of my branch don't work, so don't fret if errors show up.

GregoryREvans commented 4 years ago

@uummoo the greg/dev branch of abjad should be fully functional now. If you include the abjad.ily stylesheet, which is found in abjad/docs/source/_stylesheets in your score, all accidentals should render without intervention from ekmel.ily. Here is an example of a passage rendered this way:

Screen Shot 2020-03-01 at 5 14 31 PM

I am still cleaning the design of the accidentals themselves.

trevorbaca commented 4 years ago

Hi Levin (@uummoo). Thanks for your kind comments, and also for your detailed questions while reading through the pitch module.

The short answers to your questions are that, yes, redundant "dual" dictionaries can in fact be compressed to a single dictionary together with a one-liner that instantiates the dual from the by-hand definition of the prime; but that the situation with modeling pitches, pitch-classes, and the differences between different "types" of pitches (named and numbered, in the case of Abjad) is more complicated (and will probably stay as-is!).

Specifically:

  1. Is there a particular reason why in abjad/pitch/constants.py some dictionaries have the same content in inverse order like for instance _accidental_abbreviation_to_semitones and _accidental_semitones_to_abbreviation? I mean, wouldn't it be less redundant and easier to maintain if only one of those would be 'hard coded' and the other one defined via a function call that inverts the former dictionary?

Correct; we just haven't done a clean-up pass yet that's included this step!

  1. I had some difficulties understanding the difference between Pitch and PitchClass. I guess a Pitch has an octave while a PitchClass hasn't? But even then there are some lines that are almost equal, for instance the static method _to_nearest_quarter_tone: again I wondered, wouldn't it be better for the sake of clarity and for an easier life while maintaining the software to try reducing redundant code here or is there any particular reason for having the same code twice?

Oh, I can imagine that this would be confusing while first reading the code! However, the pitch / pitch-class distinction is being borrowed from distinctions made in (primarily 20th-century American) post-tonal theory. In the static model (with regards to object properties), it's true that the only big difference between pitch and pitch-class classes is whether octave is being modeled. This seems like a small difference. However, the implications of this difference in "modeling" (really music-theoretic "conceptualizing") turn out to be profound. To take an easy example, consider the number of different ways there are to think about pitch intervals in music theory: is the up-down direction of the interval included (ie, separating "melodic" intervals from "harmonic" intervals, or not)? What about the difference in octave (ie, are seconds and ninths equivalent, or not)? And then what about equivalence by inversion (ie, are fourths and fifths equivalent, or not)? And while we're thinking about equivalence-by-inversion, what about equivalence-by-multiplication, or equivalence by another arbitrary "operator" that we might come up with while operating on pitch?

None of these distinctions is rocket science. In fact, most of these distinctions have a way of feeling pedantic, at least when you're not currently composing with them! The minute you need to distinguish these differences in interval, however, these sorts of distinctions start to feel very much not at all pedantic and, in fact, quite essential!

So perhaps this helps animates Abjad's decision to model abjad.Pitch and abjad.PitchClass separately. The difference between C5 and C#5 is pretty clearly a minor second; but the difference between C (as a pitch-class) and C# (as a pitch-class) is something else entirely ... notionally the (infinite) set of all minor-second-like intervals. Bifurcating the pitch and pitch-classes classes in the Abjad object model allows overrides of, for example, Python's __sub__() method to generate different types of interval as appropriate to context.

  1. While trying to understand what actually happens while initializing a new Pitch object in abjad, I constantly had to jump around several files (Pitch, PitchClass, NumberedPitch, NumberedPitchClass, NamedPitchClass, NamedPitch). Whats the reason for dividing Pitch in NumberedPitch and NamedPitch when both Subclasses know mostly the same methods and attributes and are both possible to initialize with the same kind of arguments, e.g. if the only difference for the user is the different representation - method?

This is a great question. What Abjad calls "named pitches" and "numbered pitches" differ pretty profoundly not just in representation but also in meaning. Abjad's named pitches correspond to our usual intuition of musical pitches: abjad.NamedPitch("cs'") pretty much just means C#4. But Abjad's "numbered pitches" model a numeric way of thinking about pitch that derives, ultimately, from 20th-century serialism, and earlier twelve-tone music: abjad.NumberedPitch(1) is equivalent to C#4 ... and also to Db4 ... and also to B##3! In other words, this numeric way of modeling pitch temporarily suspends decisions about the (enharmonic) SPELLING of pitches. This is an incredibly useful way of working with pitch at certain moments during composition; but before numbered pitches can be notated as musical score, such pitches will have to be equipped with accidentals and spelled appropriately for Western music. Flipping back and forth between abjad.NamedPitch and abjad.NumberedPitch allows you to control these different ways of thinking about pitch while you're composing.

levinericzimmermann commented 4 years ago

@GregoryREvans Cool, thank you so much, I'm super glad that I can use 72EDO with Abjad now!

@trevorbaca Hi Trevor, first of all thank you for your long and detailed answer! This helped me to comprehend Abjad way better. I could even imagine, that including parts of this reply within Abjads official documentation could even help more people than me when beginning to work with the software (for instance here or even in the core concepts explanation, even though I could imagine that there must be a lot more that waits to be explained and making documentation is of course always super time consuming). I especially love the concept to distinguish between abjad.NumberedPitch and abjad.NamedPitch!

While I get the distinction between abjad.Pitch and abjad.PitchClass (and find it quite reasonable), I still wonder if there couldn't be a chance of reducing some of the repetitive code between both, for instance during initialization both share those lines of code:

match = constants._comprehensive_pitch_name_regex.match(
if not match:
    match = constants._comprehensive_pitch_class_name_regex.match(argument)
if not match:
    message = "can not instantiate {} from {!r}."
    message = message.format(type(self).__name__, argument)
    raise ValueError(message)
group_dict = match.groupdict()
_dpc_name = group_dict["diatonic_pc_name"].lower()
_dpc_number = constants._diatonic_pc_name_to_diatonic_pc_number[_dpc_name]
_alteration = abjad.Accidental(
    group_dict["comprehensive_accidental"]
).semitones

where abjad.Pitch will search for an octave afterwards. That's also true for some more lines during initialization (and also for the _to_nearest_quarter_tone method - or _to_nearest_twelfth_tone method in case of 72 EDO - that are almost equal between both classes and that could be generalized I guess). Wouldn't it be possible to have a more broad AbstractPitch superclass from which both - abjad.Pitch and abjad.PitchClass - would inherit or is it according to you better to have some double code than to mix those two classes in any way?

trevorbaca commented 4 years ago

@levinericzimmermann Hi Levin, Yes a huge amount of information about pitch modeling and pitch manipulation can eventually go into the docs; like you're guessing it's just a question of time. (And also scoping: the core concepts the Abjad pitch classes model are treated in books and articles common in American post-tonal music theory, of the type beloved of universities and conservatories. So deciding what to write up on the Abjad site and what to refer to external sources is interesting).

As far as re-wiring abjad.Pitch and abjad.PitchClass to inherit from an abstract base class ... definitely don't do this! ;) Years ago -- maybe 8 or 9 years ago -- I had this exact structure in place. But trust me on this: it's more confusion than it's worth. A lot more. It's precisely because the interfaces (ie, properties + methods) of abjad.Pitch and abjad.PitchClass look so similar that this becomes a problem. Which, I realize, is exactly counter-intuitive to what we would normally think: similar interface means combine implementations. The challenge is that notions of what it means to, say, add or subtract pitches (versus pitch-classes) is subtle-ly but crucially different; the result is that it keeps things cleaner NOT to abstract into a baseclass in this case.

Also, as you look at the implementation of other classes in Abjad, you'll notice other cases where a baseclass would seem like the expected thing to do. A simple example are all the indicator classes that carry a "direction" property. In previous years those classes did, in fact, inherit from a baseclass that implemented a "direction" property. But this proved less clean over the long run. (And, now, in fact, there have been meaning years of undoing that type of inheritance relation in Abjad.) If you listen to leaders in the Python community, they'll frequently advise against "using inheritance for everything." And I never really believed that (or saw the purpose). But slowly, over time, I've come to agree with this principle. Though I admit that it's really difficult to articulate the principle of exactly when not to use inheritance to factor out code common to multiple classes.