elm-music / music-theory

Principled library for working with intervals and pitch classes
BSD 3-Clause "New" or "Revised" License
5 stars 0 forks source link

Enharmonic spelling of pitch classes of scales #26

Closed battermann closed 5 years ago

battermann commented 6 years ago

Unambiguous spelling

C major (ionian)

C D E F G A B

C minor (aeolian)

C D Eb F G Ab Bb

Db major (ionian)

Db Eb F Gb Ab Bb C

Db minor (aeolian)

This is technically possible but does not make so much sense. The circle of fifth does not contain Db minor. C# minor would be a better way to represent this, which is the relative minor of E major.

Db Eb Fb Gb Ab Bbb Cb

Technically it would be possible to create a scale from any pitch class. E.g. Cbbb major:

Cbbb Dbbb Ebbbb ... Ugh (not sure if we can prevent this with the types, maybe it's just the users choice, if they wish to construct these kind of scals we'll let them)

C# minor

C# D# E F# G# A# B

Eb minor pentatonic

Eb Gb Ab Bb Db

C bebop scale

C D E F G Ab A B

According to Wikipedia the sixth note is Ab rather than G#. Actually this is a diatonic major scale with a passing note.

Conclusion

With all these the spelling is unambiguous if it can be derived from the circle of fifth.

Ambiguous spelling

C Half tone / whole tone

C C# D# E F# G A A# (ascending?)

or

C Db Eb E Gb G A Bb (descending?)

or

C Db D# E Gb G A Bb (e.g. a scale over C7b9, it's D# because that is the #9, not sure about F# vs. Gb)

or even more possibilities...

duncanmalashock commented 6 years ago

Thanks for putting these together. These are great examples!

About "correct" scales

As you've demonstrated, there are many scales (like Db minor) that are theoretically possible but rarely or never used in practice. I don't think it should be a goal of ours to prevent users from creating them. If our library supports an arbitrary number of accidentals, I think it must also support an arbitrary number of scales.

Scales of this kind will, I believe, often come from keys, so one way we can encourage people to use the "usual" scales is by exposing a functions like Key.all, Key.allMajor and Key.allMinor that returned lists of the relevant keys in the circle of fifths. This would encourage correct enharmonic keys being used in client applications (e.g. if it were used to populate a select menu).

If this doesn't seem like enough, we could modify the key constructor function we exposed to accept a parameter like useConventionalRoot that made sure that any key's tonic was always converted to the correct enharmonic equivalent in the circle of fifths before using it to generate the key's scale degrees. But I think this would be taking functionality away from the user that could be useful in a music theory setting. Maybe someone wants to do something in Cb minor— why should we stop them?

Enharmonic spelling in ambiguous contexts

It sounds like you're mainly concerned with correct enharmonic spelling in three example situations:

  1. Scales which are closely related to the current key (e.g. C bebop or C major pentatonic in the context of C major)
  2. Non-diatonic scales being used in a line, but without a key center (e.g. C half/whole patterns ascending and descending)
  3. Non-diatonic scales being used in a key (e.g. C half/whole over C7b9 in the context of F minor)

Here are some ideas about how to handle each of these:

Scales which are closely related to the current key

There isn't much to worry about in this case. If you create a C bebop scale and get its PitchClasses:

scale (pitchClass C Natural) (NonDiatonicClass bebop)
|> toList
|> List.map PitchClass.toString

➜ [ "C", "D", "E", "F", "G", "Ab", "A", "B" ]

(if PitchClass.toString took a PitchClass), you'd get C D E F G Ab A B, because bebop is defined:

bebop : NonDiatonicScaleClass
bebop =
    OctatonicScaleClass
        { secondDegree = Interval.majorSecond
        , thirdDegree = Interval.majorThird
        , fourthDegree = Interval.perfectFourth
        , fifthDegree = Interval.perfectFifth
        , sixthDegree = Interval.minorSixth
        , seventhDegree = Interval.majorSixth
        , eighthDegree = Interval.majorSeventh
        }

C D E F G Ab A B is what you want anyway, so there's no problem here. Same with C major pentatonic.

Non-diatonic scales being used in a line, but without a key center

Let's imagine you could create two ranges from the C half-whole diminished scale, one ascending and the other descending, and append them to each other:

ascendingLine =
    scale (pitchClass C Natural) (NonDiatonicClass diminishedHalfWhole)
        |> Scale.pitchesInRange (pitch C Natural 4) (pitch C Natural 5)

ascendingAndDescendingLine =
    ascendingLine ++ (List.reverse ascendingLine)
        |> List.map Pitch.toString

➜ [ "C4", "Db4", "D#4", "E4", "F#4", "G4", "A4", "Bb4", "C5", "C5", "Bb4", "A4", "G4", "F#4", "E4", "D#4", "Db4", "C4" ]

This is clearly wrong, since the pitches are just being generated by the intervals here:

diminishedHalfWhole : NonDiatonicScaleClass
diminishedHalfWhole =
    OctatonicScaleClass
        { secondDegree = Interval.minorSecond
        , thirdDegree = Interval.augmentedSecond
        , fourthDegree = Interval.majorThird
        , fifthDegree = Interval.augmentedFourth
        , sixthDegree = Interval.perfectFifth
        , seventhDegree = Interval.majorSixth
        , eighthDegree = Interval.minorSeventh
        }

If we wanted these notes to be spelled with sharps going up and flats going down, we might run them through a function that spells them according to some configuration:

ascendingAndDescendingLine =
    ascendingLine ++ (List.reverse ascendingLine)
        |> Enharmonic.spellPitchList [ Enharmonic.preferSharpsWhenAscending, Enharmonic.preferFlatsWhenDescending ]
        |> List.map Pitch.toString

➜ [ "C4", "C#4", "D#4", "E4", "F#4", "G4", "A4", "A#4", "C5", "C5", "Bb4", "A4", "G4", "Gb4", "E4", "Eb4", "Db4", "C4" ]

We would just have to implement spellPitchList, and make it behave according to those options:

spellPitchList : List Pitch -> List ConfigOption -> List Pitch

Pitches would be necessary here, since there's no guarantee that D is above C without knowing its octave. And it would be necessary to make this function take a List Pitch or two Pitches (the current one and the previous one), so that it could determine what direction the current pitch was going.

Non-diatonic scales being used in a key

I think a spellPitchList or a similar Enharmonic.spellPitchClass function that took a list of ConfigOptions could make a lot of behavior happen in this case.

We want to spell the C half-whole diminished scale over a C7b9 chord. Starting with our incorrectly spelled scale:

ascendingLine =
    scale (pitchClass C Natural) (NonDiatonicClass diminishedHalfWhole)
        |> Scale.toList
        |> List.map PitchClass.toString

➜ [ "C", "Db", "D#", "E", "F#", "G", "A", "Bb" ]

We're not sure whether these PitchClasses are right. How do we find out?

You are probably familiar with the idea of "chord scales". This means that, for any given chord, there is a list of scales it fits into.

For C7b9, for example, there are a few scales that it's compatible with that share the root of C: C diminished half-whole, and C Phrygian dominant. It depends on whether the 13 is supposed to be flatted or not.

In this case, the C diminished half-whole scale happens to be the correct choice. But we could make sure to spell it that way by running it through our imaginary spellPitchClass function with a ConfigOption that tells it what to do:

scaleOfTheMoment =
    scale (pitchClass C Natural) (NonDiatonicClass diminishedHalfWhole)

ascendingLine =
    scale (pitchClass C Natural) (NonDiatonicClass diminishedHalfWhole)
        |> Scale.toList
        |> List.map (Enharmonic.spellPitchList [ Enharmonic.currentScale scaleOfTheMoment ] )
        |> List.map PitchClass.toString

➜ [ "C", "Db", "D#", "E", "F#", "G", "A", "Bb" ]

If we were playing these notes over a Cm7b9b13 chord, we might do something like this (although it wouldn't change the spelling in this case, unless there were config options that told it how to spell the A natural):

scaleOfTheMoment =
    scale (pitchClass C Natural) (NonDiatonicClass phrygianDominant)

ascendingLine =
    scale (pitchClass C Natural) (NonDiatonicClass phrygianDominant)
        |> Scale.toList
        |> List.map (Enharmonic.spellPitchList [ Enharmonic.currentScale scaleOfTheMoment ] )
        |> List.map PitchClass.toString

➜ [ "C", "Db", "D#", "E", "F#", "G", "A", "Bb" ]
duncanmalashock commented 6 years ago

That last part about ConfigOptions defining the "scale of the moment" probably seemed complicated, and it didn't help that the example notes didn't change as a result. But I would be happy to try to illustrate with a different example. I think the approach of a configurable enharmonic speller function is a very promising way to solve these problems.

battermann commented 6 years ago

As you've demonstrated, there are many scales (like Db minor) that are theoretically possible but rarely or never used in practice. I don't think it should be a goal of ours to prevent users from creating them. If our library supports an arbitrary number of accidentals, I think it must also support an arbitrary number of scales.

I agree.

Scales of this kind will, I believe, often come from keys, so one way we can encourage people to use the "usual" scales is by exposing a functions like Key.all, Key.allMajor and Key.allMinor that returned lists of the relevant keys in the circle of fifths. This would encourage correct enharmonic keys being used in client applications (e.g. if it were used to populate a select menu).

Yes.

If this doesn't seem like enough, we could modify the key constructor function we exposed to accept a parameter like useConventionalRoot that made sure that any key's tonic was always converted to the correct enharmonic equivalent in the circle of fifths before using it to generate the key's scale degrees. But I think this would be taking functionality away from the user that could be useful in a music theory setting. Maybe someone wants to do something in Cb minor— why should we stop them?

Sounds right.

The rest looks good in general. But I'd have to look in detail carefully.

I would suggest that we start with small steps. Let's get the simplest possible version of ScaleClass right. Then Scale and so on. We don't have to get everything covered in the first version. We can always make breaking changes to the API later when we add more features.

I get the feeling that trying to come up with a perfect design up front is slowing us down big time.