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

Scales #3

Closed battermann closed 6 years ago

battermann commented 6 years ago

A module Scale.elm with types and functions for working with scales.

It should be distinguished between scale definitions (agnostic to the tonality or root) and actual scales with a fixed root pitch class.

How do we deal with scales that have different pitches based on the direction, such as melodic minor scale?

We need to have a good representation both for tonality related scales (with a fixed enharmonic spelling) and other scales like chromatic or half-tone whole-tone (with no fixed enharmonic spelling).

duncanmalashock commented 6 years ago

Here are a few thoughts on this:

I like your terminology of a more abstract ScaleDefinition versus a more concrete Scale.

I noticed that in both of our projects, we both defined scales in terms of the intervals from the root to a scale degree. For example, the major scale had the following intervals from the root:

[ majorSecond
, majorThird
, perfectFourth
, perfectFifth
, majorSixth
, majorSeventh
]

I think this makes sense for diatonic scales, but not for non-diatonic scales. We think of diatonic scales of having, for example, M3 and m7, but we probably wouldn't say the same thing about the whole-tone or diminished scales. Would you say that the whole-half diminished scale has these intervals?

[ majorSecond
, minorThird
, perfectFourth
, diminishedFifth
, minorSixth
, diminishedSeventh
, diminishedOctave
]

I probably wouldn't, because non-diatonic scales don't have numbered scale degrees in the same way and, as you point out, they don't have fixed enharmonic spellings.

But diatonic and non-diatonic scales do both have a familiar representation in terms of the intervals between their degrees. The formula for building a major scale is WWHWWWH, or:

[ majorSecond
, majorSecond
, minorSecond
, majorSecond
, majorSecond
, majorSecond
, minorSecond
]

Similarly, the formula for building the whole-half diminished scale would be WHWHWHWH, or:

[ majorSecond
, minorSecond
, majorSecond
, minorSecond
, majorSecond
, minorSecond
, majorSecond
, minorSecond
]

I think, with some enharmonic respelling applied afterward, this type of definition might be more consistent and easier to think about given the differences between diatonic and non-diatonic scales.

That's interesting about differing ascending vs. descending versions of scales. I didn't think about that at all. Melodic minor is a very good example. Here's one way we could model its definition:

melodicMinor : ScaleDefinition
melodicMinor =
  { ascending = [ majorSecond, minorSecond, majorSecond, majorSecond, minorSecond, augmentedSecond, minorSecond ]
  , descending = [ majorSecond, majorSecond, minorSecond, majorSecond, majorSecond, minorSecond, majorSecond ]
  }

Where ascending contains the stepwise intervals starting from the root going upward, and descending contains the stepwise intervals starting from the root going downward. In most scales, ascending == List.reverse descending

So a Scale could look like this:

type alias Scale =
  { root: PitchClass // this could be omitted, and the root is simply the first item in ascending
  , ascending : List PitchClass
  , descending : List PitchClass
  }

To traverse an actual scale, we would need to keep track of where we were in these lists at all times. I think the Zipper data type might come in handy for this. A ScaleTraverser could be modeled as:

type alias ScaleTraverser =
  { root : PitchClass
  , currentOctave : Octave
  , currentDirection : TraversalDirection
  , ascending : Zipper PitchClass
  , descending : Zipper PitchClass
  }
battermann commented 6 years ago

Just a few thoughts:

  1. Maybe instead of ScaleDefinition we should use the name ScaleClass to be consistent with PitchClass e.g.

  2. I find the definition of a scale class as stacked intervals not as intuitive as intervals from root. But this could just be because it's less familiar. Maybe it works just as nicely.

  3. I think that consistency is less important than a comprehensive, coherent model. I think it might be ok to define a scale as a union type with a ctor for diatonic (or better name) scales and a ctor for non-diatonic scales. Where the scale class models for these two types could be different, like one being intervals from the root and the other one could be stacked intervals or semitones. Also this provides a marker for the type of scale as well.

  4. I like your suggestion on designing scale class with different ascending and descending forms. But I'm wondering if this might make things a little too clumsy? I would like it more if it wasn't integrated too deeply into the scale class definition, more like an add on. It's something that the consumers could take care of and define themselves. Of course the lib could provides useful helpers for that.

  5. And finally but related to 4. I'm wondering if ascending and descending definitions might be an edge case that is marginal and that we could ignore? I'm thinking of the package design guide (https://package.elm-lang.org/help/design-guidelines) "Avoid gratuitous abstraction". Even though this is more related to FP abstractions, we have to think about if it's beneficial to make the API as general as possible to cover all potential use cases, or if this might make it less useable for the average use cases?

battermann commented 6 years ago

I talked to a piano player today about enharmonic spelling of non-diatonic scales. He said that it depends on the context. The spelling varies with the underlying chord. If it's context-free then there is no other rule than using sharps when the line leads up and using flats when the line leads down.

I still have no good model in mind for this. Maybe for spelling we could have an API like this:

spelling : Maybe Chord -> Scale -> List (Letter, Accidental)

But I feel I need to do some more researching before I get a good understanding of how this could be implemented. I mean this heavily depends on a good chord model as well. And even though I can do this easily based on examples I still struggle to think of a general model that works for arbitrary inputs.

duncanmalashock commented 6 years ago
  1. Yeah, I like ScaleClass— that's nice.

  2. We could definitely do both constructors for stacked intervals and intervals from root, or just as intervals from root. I was just unsure how to represent non-diatonic scales correctly as intervals from root, because I've never seen them discussed that way and couldn't find information about it.

  3. Yeah, an explicit distinction between diatonic and non-diatonic scales could be helpful.

  4. (and 5.) I had the same thought— are there any other cases besides the melodic minor? But in any case, I think, even with an ascending/descending model, those internals could be completely hidden from client code. Maybe I can try to write up an example usage, because you're right that it should be as simple as possible for the client code.

duncanmalashock commented 6 years ago

You're right that, in general, we don't need to complicate our implementations when it doesn't help the client. Maybe that's a reason to start writing examples, so we get a better sense of our use cases.

duncanmalashock commented 6 years ago

Resolved by #24