Note Names / Pitch Classes

picking up from this thread Class Library Developer Group - #42 by jordan, this thread is to discuss proposals to add Note Names / Pitch Classes to the class library.

The goal is to be able to use note names for pitch rather than midinotes.

Some have pointed to CTK’s PitchClass as a good implementation of the traditional western system - (for example transposition of B3 by a major third gives D#4 rather than it’s enharmonic equivalent - a pain point in many less well fleshed out implementations. )

…but PitchClass, along with @prko’s initial proposal involving specifically Scientific Pitch Notation - (C5 for example as opposed to Helmholz notation which would be c’‘’ or some such) is currently limited to heptatonic names and IIRC 24EDO tuning - many would like to see a more general, non-culturally-specific (and extensible!) solution that might be able to support arbitrary scales and numbers of accidentals.

3 Likes

I mentioned this in the previous thread, but it seems like this one might be a more suitable location for our discussion. Thank you.

1 Like

I love @smoge’s idea that subtracting pitches from each other would return an interval.

So to think more about that perhaps we need PitchClasses (no octave) and AbsolutePitches (with octave).

In that case I think we would need IntervalClasses and AbsoluteIntervals (or there could be an “absolute” flag?)

so IntervalClass(4, \perfect) == IntervalClass(5, \perfect)

(in this little sketch already different naming conventions would need to be accommodated! in the classical context the ninth interval can be major, minor, augmented or diminished - in jazz the terms “flat ninth” and “sharp ninth” are preferred - to be extensible we would need to be able to subclass AbstractInterval with different Quality names - as well as PitchClass itself etc etc)

AbsoluteIntervals either need to be signed (to indicate up or down) or have a direction flag:

AbsPitch(PitchClass('a'),octave:3) - AbsPitch(PitchClass('b'), \sharp, 2) 
// AbsInterval(-7,\diminished) 

AbsoluteIntervals could also have an octaves key perhaps ?

So this is a partial table of addition and subtraction across the classes:

AbsPitch + AbsInterval → AbsPitch
AbsPitch + Interval → PitchClass
AbsPitch - AbsPitch → AbsInterval
PitchClass - PitchClass → Interval
Interval + Interval → Interval
AbsInterval + AbsInterval → AbsInterval

Many nice possibilities:

AbsPitch('a', 3) - AbsPitch('c', 4) + AbsPitch('d',\sharp,3) // AbsPitch('b',\sharp,2)

now what happens if we add two Pitches or PitchClasses?

One idea is that the addition could be used to build sonorities - a3 + c3 + e3 could yield a simultaneous triad. in that case ++ could perhaps be used for concatenation so a3 ++ c3 ++ e3 would be a little arpeggio ?

chords might be constructed in that case

~majorTriad = {|absPitch| 
   absPitch + [ AbsInterval.unison, AbsInterval(3, \major), AbsInterval(5,\perfect)]
}

These operations could perhaps yield prefix tree datastructures…

1 Like

IMHO…
I don’t think we should do this!
Actually, I’d be against all ‘pitch classes’ as this assumes the intervals can be inverted about the tritone (a la Allen Forte) which is a HUGE musical assumption relevant only to (cheifly Austrian, although later American) 20th century music post Webern - it does not hold for almost every other style, e.g., Italian renaissance polyphony where the 5th is consonant and the fourth dissonant.

If Intervals are instead signed (perfect fourth up, dim fifth down, etc…) this negates the issue. I also don’t think you need an AbsInterval as when transposing Pitch these can just wrap around the octave,
meaning you don’t need the AbsPitch either, you can just extract the name bit and ignore the octave.

One down sound is that the return type alternates depending on keep track. In typed languages this isn’t an issue because the tooling/type annotations tells you what you have - we don’t have that.

Perhaps this could be made explicit instead.

Pitch + Pitch -> Error 
Pitch - Pitch -> Interval

Pitch.transpose( Interval ) → Pitch

Interval + Interval -> Interval
Interval - Interval -> Interval
Interval * Number -> Interval
Interval / Number -> Interval

Interval.smallestForm -> Interval 
(does the Forte thing, turns 4ths to 5ths, )

Another question that comes up here all the time is how to get patterns to make sequences of pitches vs polyphony, this might cause similar confusion.

3 Likes

Point taken re: requiring intervals to be signed.

my thought re “pitch classes” wasn’t to favor serial techniques - simply that we might wish to reason about note name collections in the abstract!

A pitch can have a name, and an octave… or maybe better a name and an octave and an accidental… The names could be Symbols sure, but should they not be instances of a separate class? Same questions for collections of accidentals.

From the point of view of western notation there are 7 places to put a note in each octave corresponding to 7 note names. These can be modified by accidentals. Intervals have a degree and a quality - the degree expresses the relation between the note names only. B (or H) to F is (some kind of) fifth. B# to F-double-sharp is a perfect fifth while B# to G-natural is a diminished 6th. This goes to readability - every second is the same vertical distance as every other regardless of quality, as well as expressing harmonic intent (if using).

So there is structure to the collection of note names - obviously this could all live as data and methods inside one big class but are there not any scenarios where it would be better to have these separate?

1 Like

By separating note names and accidentals, we can create a system where we can even change which accidentals are available (or even note names), and the system still ends up working for a situation. Since note names and accidentals always have the same value, I doesn’t mean either we can’t redefine them if necessary (most of the time, we don’t need that, of course).

I just thought about equal temperament systems, but I believe this is also true for other systems.

There is the issue of enharmony. But this cannot be solved with a general rule: each user must choose which direction he wants to use.

In the simplest case, the user can choose “preferFlats” or “preferSharps”.

Or, “I prefer the closest note to a note without accidentals.” EXAMPLE with 72ET:

Let’s say we want to work with a wide range of micro-tonal intervals (72ET); the base for the alterations will be the same seven notes. For example"

[Flat,Natural,Sharp,QuarterFlat,QuarterSharp,ThreeQuartersFlat,ThreeQuartersSharp,DoubleSharp,DoubleFlat,ThirdSharp,ThirdFlat,SixthSharp,SixthFlat,TwelfthSharp,TwelfthFlat,EighthSharp,EighthFlat,FiveTwelfthsSharp,FiveTwelfthsFlat,SevenTwelfthsSharp,SevenTwelfthsFlat,FiveSixthsSharp,FiveSixthsFlat,ElevenTwelfthsSharp,ElevenTwelfthsFlat,TwoThirdsSharp,TwoThirdsFlat,ThreeEighthsSharp,ThreeEighthsFlat]

With a list comprehensions, we can create all possible pitchclasses:

allPC = [createPitchClass n a | n <- [C .. B], a <- accidentals]

Two notes match the PitchClass (10/3) E Third Flat and F Five Sixths Flat, because (4 + (-2)%3) == (5 + (-5)%3)

>>> filterNotesWithPitchClassVal allPC (10%3)
[PitchClass {_noteName = E, _accidental = Accidental {_accName = ThirdFlat, _accAbbreviation = "rf", _accArrow = Nothing, _accSemitones = (-2) % 3}},PitchClass {_noteName = F, _accidental = Accidental {_accName = FiveSixthsFlat, _accAbbreviation = "fxf", _accArrow = Nothing, _accSemitones = (-5) % 3}}]

In that case, the note closest to a note without accidentals can be chosen. Or not, we can think of other rules appropriate to a context.

It’s hard to think of all the possibilities, but the less we assume, the better.

I can even imagine a case where one would need extra note names, like J in Bohlen–Pierce scale.

hello,

I’ve been kind of exploring a method to address a previous discussion on enharmonic relations between pitch classes on this discussion on music notation etc etc.

We touched that a few times if I’m not mistaken. Well, I’ve been tinkering around with an idea that goes beyond the usual ‘preferFlats’ or ‘preferSharps’ approach. It’s pretty flexible, letting you set your own rules for how you want pitch classes to be spelled out.

The idea is to allow users to define their own rules, although a “default” is provided. The aim is to maintain a flexible implementation. The algorithm/idea is easily transferable to SuperCollider, and the idea is very simple. I’m not sure it covers all use cases, but it’s more flexible than average.

Also, it works with quarter-tones, or any other accidental (72ET, 48ET, etc). If your rules include the accidental, there is no difference between a “normal” accidental or a sixth-tone-sharp, or any other. (It kind of shows why I suggested separating accidental, having their own classes/types etc)

enharmonicMapping :: [Rational] -> EnharmonicMapping
enharmonicMapping = map (\r -> (r, snd <$> enharmonicPCEquivs' r))

{- | This generates a list of all enharmonic representations
 for a given 'PitchClass'. 

If no enharmonic equivalents are found, it returns a 
list containing the original 'PitchClass'.

For instance, for a 'PitchClass' corresponding to C Sharp:
 
>>> enharmonics (PitchClass C sharp)
 [C Sharp,D Flat,B DoubleSharp] 
-}

enharmonics :: PitchClass -> [PitchClass]
enharmonics pc = fromMaybe [pc] (lookup (pitchClassVal pc) out)
  where
    out = enharmonicMapping [pitchClassVal pc]

-- | A list of 'NoteName's
-- Useful to pick special rules for certain notenames (quite common case)
-- for example if you don't want any B sharps or F flats:
preferNaturalFor :: [NoteName]
preferNaturalFor = [E, B] 

{- | predefined rules  for determining the suitability of its 
enharmonic representation. 
The higher the score, the more preferred the 'PitchClass'.
The function can be further customized to handle more 
intricate rules or new accidentals.-}

scoreEnharmonic :: PitchClass -> Int
scoreEnharmonic pc@(PitchClass noteName accidental)
    | accidental == natural = 6
    | accidental == sharp &&  noteName `notElem` preferNaturalFor = 5
    | accidental == flat &&  noteName `notElem` preferNaturalFor = 4
    | accidental == quarterFlat = 3
    | accidental == quarterSharp = 2
    | accidental == threeQuartersSharp = 1
    | accidental == threeQuartersFlat = 0
    | otherwise = 0

-- | Selects the "best" (more suitable) 'PitchClass' from a 
-- list based on the score number (ranking) from 'scoreEnharmonic':
pickBest :: [PitchClass] -> PitchClass
pickBest = maximumBy (compare `on` scoreEnharmonic)

Finally, this provides the best-fitting enharmonic representation for a given ‘PitchClass’ based onthe rules provided by the user (as in the examples above ). Now, the function enharmonics identifies all possible enharmonic variations of the ‘PitchClass’ Then, it picks the variation with the highest score according to ‘scoreEnharmonic’ (where the rules are defined)

Example:


pickBestEnharmonic :: PitchClass -> PitchClass
pickBestEnharmonic = pickBest . enharmonics

{-
>>> pickBestEnharmonic (PitchClass C doubleSharp) 
D Natural 
-}

It only takes into account the pitch in question, it does not consider what comes before or after. (Which could be an idea too, but which I don’t see as a high priority at the moment)

It can be quite simple if the interval is just a number. But can be more tricky if we consider the interval “quality” (a perfect fifth etc). But, in general, makes a lot of sense to work with intervals and chords structures (a “major triad” is formed by intervals, not specific pitches etc).

Here’s a first stab at a little PtInterval class with a semitones method. It first sets up an array of “rawIntervals” that are whole for perfect intervals and fractional for imperfect - then two dictionaries for the qualities.

PtInterval(5,\perfect).semitones //7
Ptnterval(3,\major).semitones //5

PtInterval {
	classvar rawIntervals = #[0, 1.5, 3.5, 5, 7, 8.5, 10.5, 12];
	classvar perfectIntervalQualities, imperfectIntervalQualities;
	var <degree, <quality;
	*initClass {
		perfectIntervalQualities = (diminished:-1, perfect:0 ,augmented:1);
		imperfectIntervalQualities = (diminished:-1.5, minor:-0.5, major:0.5, augmented:1.5);
	}
	*new{ |degree quality|
		^super.newCopyArgs(degree, quality)
	}
	semitones {
		^( rawIntervals[ degree - 1 ] + 
			rawIntervals[ degree  - 1].isInteger.if{
				perfectIntervalQualities.at(quality)
			}{
				imperfectIntervalQualities.at(quality)
			}
		)
	}
}

Cool. Does it work with both “diatonic” and “chromatic” intervals? It’s probably possible to combine both in the same class. Even if you’re writing non-tonal music, 7 semitones will be called ‘perfect fifth’ anyway. Even quarter-tones have this, you can call a “neutral third” for the interval between the major and minor 3rd. etc.

Operations like these would make sense:


\m3 + \M3
 // output:  \P5

\d5 + \M6 
 // output: \m10

// separate octaves + interval from a large chromatic interval in semitones:
[2, \m3]

or something like

in case (noteNameDifference, semitoneDifference) of
        (0, 0) -> Interval Perfect Unison
        (0, 1) -> Interval Augmented Unison
        
        (1, 1) -> Interval Minor Second
        (1, 2) -> Interval Major Second
        
        (2, 3) -> Interval Minor Third
        (2, 4) -> Interval Major Third
        
        (3, 4) -> Interval Perfect Fourth
        (3, 6) -> Interval Augmented Fourth
        
        (4, 6) -> Interval Diminished Fifth
        (4, 7) -> Interval Perfect Fifth
        
        (5, 8) -> Interval Minor Sixth
        (5, 9) -> Interval Major Sixth
        
        (6, 10) -> Interval Minor Seventh
        (6, 11) -> Interval Major Seventh
        
        (7, 12) -> Interval Perfect Octave
        

need to test this to see if it works

Let’s say a Perfect Fourth above F FLAT. Regardless of accidentals, just to calculate from note name F, a P4 “would be” a B♭. However, since we started from F♭, we should adjust the result to B♭♭ (or B double flat) to maintain the correct interval relationship.

If we use the same logic of calculating the interval from BOTH semitones and note name steps, it would also work:

something like this

addInterval :: PitchClass ->Interval -> PitchClass
addInterval pc interval =
    let totalSemitones = (pitchClassToSemitones pc + intervalToSemitones interval) `mod` 12
        newSize = case snd interval of
            Unison -> 0
            Second -> 1
            Third -> 2
            Fourth -> 3
            Fifth -> 4
            Sixth -> 5
            Seventh -> 6
            Octave -> 7
        newNoteValue = (fromEnum (_noteName pc) + newSize) `mod` 7
        newNote = toEnum newNoteValue
        newAccidental = toEnum (totalSemitones - fromEnum newNote)
    in PitchClass newNote newAccidental

Or I’m totally wrong?

I think this is right in principal - let me try to build in sclang using PtInterval object above…

Maybe something like this:

(That’s an attempt to simulate the haskell code (addInterval) above to a minimal sc snippet)

(

~noteNames = [ \C, \D, \E, \F, \G, \A, \B ];

~accidentals = [ \doubleFlat, \flat, \natural, \sharp, \doubleSharp ];

// Returns semitones value of just the note (ignoring accidental)
~noteToSemitones = { |noteName|
    switch(noteName)
    {\C} { 0 }
    {\D }{ 2 }
    {\E} { 4 }
    {\F} { 5 }
    {\G} { 7 }
    {\A} { 9 }
    {\B} { 11 };
};

~pitchClassToSemitones = { |note|
    var base;
    var adjust;
  
    base = ~noteToSemitones.(note[0]);

    adjust = switch (note[1])
    {\doubleFlat} { -2 }
    {\flat} { -1 }
    {\natural} { 0 }
    {\sharp} { 1 }
    {\doubleSharp} { 2 };
    
    base + adjust;
};

~intervalToSemitones = { |interval|
    var base;
    var adjust;
    
    base = switch(interval[0], 
        \unison, { 0 },
        \second, { 2 },
        \third, { 4 },
        \fourth, { 5 },
        \fifth, { 7 },
        \sixth, { 9 },
        \seventh, { 11 },
        \octave, { 12 },
        { 0 }
    );
    
    adjust = switch(interval[1], 
        \perfect, { 0 },
        \major, { 0 },
        \minor, { -1 },
        \augmented, { 1 },
        \diminished, { -1 },
        { 0 }
    );
    
    base + adjust;
};

~addInterval = { |pitchClass, interval|
    var totalSemitones;
    var newSize;
    var newNoteValue;
    var newNote;
    var newAccidental; var diff ;
    
    var noteNames = [ \C, \D, \E, \F, \G, \A, \B ];
    
    totalSemitones = (~pitchClassToSemitones.([pitchClass[0], pitchClass[1]]) + ~intervalToSemitones.(interval)).mod(12);
    
    newSize = switch(interval[0]) 
    {\unison} { 0 }
    {\second} { 1 }
    {\third} { 2 }
    {\fourth} { 3 }
    {\fifth} { 4 }
    {\sixth} { 5 }
    {\seventh} { 6 }
    {\octave} { 7 };
    
    newNoteValue = (noteNames.indexOf(pitchClass[0]) + newSize).mod(7);
    newNote = noteNames[newNoteValue];

    diff = totalSemitones - ~noteToSemitones.(newNote);
    
    newAccidental = switch(diff)
    { -2 } { \doubleFlat }
    { -1 } { \flat }
    { 0 } { \natural }
    { 1 } { \sharp }
    { 2 } { \doubleSharp };

    [newNote, newAccidental];
};

)

Example:

~c = [\C, \natural];
~perfectFourth = [\fourth, \perfect];

~resultingPitch = ~addInterval.(~c, ~perfectFourth);
// -> [ F, natural ]

~addInterval.([\F, \sharp], [\third, \major]);
// -> [ A, sharp ]


~addInterval.([\B, \sharp], [\third, \minor]);
// -> [ D, sharp ]

~addInterval.([\F, \flat], [\sixth, \major]);
-> [ D, flat ]

Seems to work (bugs in edge cases still, another pair of eyes will be able to catch it)

(

~testAddInterval = {
    var tests = [
        // Format: [starting pitch class, interval, expected result]
        [[\C, \natural], [\fourth, \perfect], [\F, \natural]],
        [[\F, \sharp], [\third, \major], [\A, \sharp]],
        [[\C, \sharp], [\seventh, \minor], [\B, \natural]],
        [[\B, \flat], [\second, \major], [\C, \natural]],
        [[\A, \natural], [\sixth, \minor], [\F, \natural]],
        [[\G, \sharp], [\third, \minor], [\B, \natural]],
        [[\F, \flat], [\fourth, \augmented], [\B, \flat]]
    ];

    tests.do { |test|
        var result = ~addInterval.(test[0], test[1]);
        if (result == test[2]) {
            "Test passed: % + % = %".format(test[0], test[1], result).postln;
        }  {
            "Test failed: % + % = %, expected %".format(test[0], test[1], result, test[2]).postln;
        }
    };
};

)


~testAddInterval.();
Test passed: [ C, natural ] + [ fourth, perfect ] = [ F, natural ]
Test passed: [ F, sharp ] + [ third, major ] = [ A, sharp ]
Test passed: [ C, sharp ] + [ seventh, minor ] = [ B, natural ]
Test passed: [ B, flat ] + [ second, major ] = [ C, natural ]
Test passed: [ A, natural ] + [ sixth, minor ] = [ F, natural ]
Test passed: [ G, sharp ] + [ third, minor ] = [ B, natural ]
Test passed: [ F, flat ] + [ fourth, augmented ] = [ B, flat ]

Have a look at this one - one advantage is that all he machinery - the note list and the raw intervals and the dictionary of qualities should be straightforward to subclass ?

PtInterval(3, \minor) + PtInteval(4, \perfect) // PtInterval(7, doubly-diminished)

[Removed as was not working - corrected version down thread!]

1 Like

How do you use it? It’s not clear to me


a = PtInterval(3, \major);
b = a + a
//6 major (??)
a.semitones //4 
b.semitones // 9

I made an error = 3rd +3rd should be 5th not 6th let me fix!

a = PtInterval(3, \major);
b = a + a

a.semitones //4 
b.semitones // 9

[a.degree, a.quality] //-> [ 3, major ]
[b.degree, b.quality] //  -> [ 6, major ]

Should be 6th minor, have some bugs still

yes stupid error let me repair