Beefy Boxes and Bandwidth Generously Provided by pair Networks
laziness, impatience, and hubris
 
PerlMonks  

Chord root

by benn (Vicar)
on Aug 09, 2003 at 00:19 UTC ( [id://282345]=CUFP: print w/replies, xml ) Need Help??

Wow. This is simple, but oh so beautiful. From the keyboard of the mighty Ernst Terhardt comes this lovely little algorithm to calculate the root of a chord, given a bunch of notes.

What amazed me was that the root note doesn't have to be included! - as evidenced by the 'Tristan' example. I'll be playing with this for days! :)

Update - Now improved! Uses the full subharmonic range, and picks out all possible answers - eg. "a c e g" can be either C(6) or A(m7).

my %nums = qw(A 0 A# 1 BB 1 B 2 C 3 C# 4 DB 4 D 5 D# 6 EB 6 E 7 F 8 F# + 9 GB 9 G 10 G# 11 AB 11); my %notes = qw(0 A 1 A# 2 B 3 C 4 C# 5 D 6 D# 7 E 8 F 9 F# 10 G 11 G#) +; use List::Util qw(max); sub find_root { my @f; foreach my $n (map {$nums{uc($_)}} @_) { ++$f[($n+$_)%12] foreach (0,0,0,2,5,5,8,10); } my $max = max(@f); map {$notes{$_}} grep {$f[$_] == $max} (0..11); } print find_root(qw(c f a)),"\n"; # F - 2nd inversion print find_root(qw(e g a# c)),"\n"; # C - 1st inversion 7th) print find_root(qw(f b d# g#)),"\n";# C#(!)/G# - 'Tristan' chord, C#9( +no bass) *or* G#m6

Replies are listed 'Best First'.
Re: Chord root
by YuckFoo (Abbot) on Aug 09, 2003 at 21:05 UTC
    benn

    Thanks for your algorithm and the pointer to the original. Very interesting stuff. A couple of comments.

    First, I think it was a poor choice to reorder your interval list from (0,0,5,0,8,5,10,0,2) to (0,0,0,2,5,5,8,10) (really the original interval list is (0,0,5,0,8,5,2,0,10)). Two reasons for this. The ordering of the harmonics useful information. It aids in understanding the problem. It might also be useful for processing. One way of improving the algorithm is to give more weight to the more important harmonics, those at the front of the list. When you reordered, you lost this. Also when you reordered, you dropped a '0', there are four '0's in the first eight harmonics, not three.

    That's the little stuff. More importantly, I think you didn't implement a key aspect of the original algorithm. It is not simply a count of the most common note to occur in the table. It is important that the root note be found in each column of that table.

    Regarding the example Tristan chord f b d# g#, Ernst's conclusion that the root is c# is based on c# occurring in each column, that is, f b d# and g# are all harmonics of c#.

    When you just count notes in the table, you came to an erroneous conclusion that g# is an alternative root, at least with respect to the original algorithm. f and b do not occur as harmonics of g#, at least not in the harmonics you are considering.

    You found that the algorithm has a problem with minor chords. Of course it does. The minor third interval that characterizes the chord does not occur in the root note's harmonic series, at least in the first 17 items!

    To make the algorithm *seem* to work for minor chords, you added back the redundancy that Ernst removed, counting the '0' three times and the '5' two times. Really all this did was put more weight on the fifth, which might be justified, but is also led you to pick g# in the Tristan chord because it now is counted more.

    By the way, I can not claim to do any better with this harmonic series approach. I tried many variations without much improvement. So that makes this just some ramblings of a critic. Again, I enjoyed the effort!

    YuckRoot

      YuckFoo - Thanks for the critique - lots of good points. To take them in order....

      OK - guilty as charged. My 're-ordering' of the array could be confusing to anyone wanting to understand this algorithm...it was sheerly a side effect of experimentation ("I wonder what happens if I stick an extra '0' in there..."), but I'm sorry and I won't do it again :) (Oh, and the missing '0' too was simply the result of experimentation.)

      Originally, I did implement the algorithm exactly as stated, checking for existence in all columns. And indeed, it worked beautifully for major chords - but not for minor ones, for exactly the reason you describe. So, being a hacker by nature, I played around with it until it worked for both.

      You say it "seems" to work. I'm not pretending that I understand why sticking the 'redundancy' back in and switching from a column-based count to a simple frequency count works, but it does - certainly for all the examples I've played around with. The original algorithm, given "a c e" produces a root of 'D', which is...well, I hesitate to say "plain wrong", because I suppose it could be considered a D9 with no root or 3rd, but I prefer to spell that 'A minor'. This version produces 'A', which is, in my eyes, better. The 'Tristan' example gives *both* the C#9 spelling *and* the G#m6 spelling - also 'correct'.

      I'd love to see a proof of why this works (or not, as the case may be) - I *only just* understood why the original worked, and this version is simply the result of a few hours idly hacking around. I may have jumped to a number of erroneous conclusions - but I'm happy with the result - while the original algorithm doesn't handle minor chords, this one does. Again, don't ask me *why* (my best guess is something to do with relative minors), but it certainly works for every 3/4 note chord I've tried, (other than a 4-note diminished, which understandably confuses the hell out of it :) ).

      Cheers,
      Ben.

Re: Chord root
by Anonymous Monk on Aug 09, 2003 at 16:04 UTC
    I like being able to run code under warnings (I am a module author), so adding this:

    for (@f) { $_ = 0 unless defined }

    is handy below the foreach in the code.

    Also, I am a fan of Sean Burke's MIDI perl modules, so I prefer "As" and "Af" instead of "A#" and "AB". If you use these, you can talk directly to the MIDI modules without any needless parsing gymnastics. Also, putting #'s in qw() arrays breaks with warnings on.

    Anyway, here are some questions:

    1. Is there a name for "@f" that might give away what it stands for and holds? Variables named things like "f" are pretty frustrating.

    2. What does ($n+$_)%12 represent?

    3. What is (0,0,0,2,5,5,8,10)?

    Maybe the Terhardt link describes this stuff somewhere, but... comments are really useful.

    : )

    -Gene Boggs <gene at ology dot net> Software Engineer and Epistemologist at-large _____________________________________________
      Hi Gene.

      Indeed - when I stick this in Music::Chord, it'll have strict and warnings plastered all over it...but I was playing golf :)

      I too am a fan of Sean's MIDI Modules, but not of 'As' and 'Af' - I've been reading / writing real music for too long now. Again, if/when I 'modulise' this, and once we've sorted out a common standard for note interchange, this will probably sort itself out. The 'parsing gymnastics' though would seems to consist of s/s$/#/ and s/f$/b/ :)

      Anyway, here are some answers.

      1. 'f' stands (in my head) for 'final values'. It's just an array that stores the note counts. Yes, I could have called it '@final_values'.

      2. ($n + $_) %12 represents adding a numerical note value ($n) to an interval ($_) and doing a modulus 12 on the result, to remain within the 0-11 note range.

      3. These are harmonics. Indeed, the link describes the algorithm, and explains what the values meant, but briefly, the idea is to take the 1st 8 *sub*-harmonics (8va,5th,8va,3rd,5th,7th etc., but *downwards*) of each note in the chord, then do a popularity count on each note - the most popular is/are the root. The original algorithm misses out all octave equivalents, (doubled root + 5th), but I found that this gives wrong results for minor chords, so added them back in. This gives us a semitone-interval list of (0, -12, -19,-24...), but I didn't bother with working out all the big negative numbers, as this is just functionally equivalent to (0,0,5,0,8,5,10,0,2), ordered how you like - in my case, sorted numerically cos it looked nicer :)

      Hope this answers the questions.

      Ben.

      for (@f) { $_ = 0 unless defined }
      Since the only false values are undef, '', 0, '0', you can safely write this more idiomatically as
      $_ ||= 0 for @f;

      Makeshifts last the longest.

Re: Chord root
by benn (Vicar) on Aug 11, 2003 at 19:19 UTC
    Richard Parncutt has got a bunch of 'C' code building on Prof. Terhardt's work, including a chord root program that examines progressions. This is the 'single chord' core of his version, which gives a level of ambiguity as well. I'm still playing with both this and the above...for single chords, the above may still be better - the now-infamous 'Tristan' example comes out as G# (second = B) - both valid, (and I would possibly argue more for the G#m6 spelling than a C#9 ), but research continues - *one* of them will make it into Music::Chord, I'm sure :)
    use List::Util qw(max sum); use strict; use warnings; sub note2num { my ($n,$m) = split(//,uc(shift())); my %nums = qw(C 0 D 2 E 4 F 5 G 7 A 9 B 11); my $num = $nums{$n}; $num++ if ($m eq 'S' || $m eq '#'); $num-- if ($m eq 'F' || $m eq 'B'); $num; } sub num2note { my $num = int(shift()) % 12; my %notes = (0,'C',1,'C#',2,'D',3,'D#',4,'E',5,'F',6,'F#',7,'G',8, +'G#',9,'A',10,'A#',11,'B'); $notes{$num}; } sub root { my @chord = @_; my @weights; my @rootWeights = (10,0,1,0,3,0,0,5,0,0,2,0); # weights of root-su +pport intervals, similar to those in Parncutt (1988) my @notes; $notes[$_] = 0 foreach (0..11); $notes[note2num($_)] = 1 foreach (@chord); foreach my $pc(0..11) { $weights[$pc] = sum(map{$notes[($pc+$_)%12]*$rootWeights[$_]} +(0..11)); } my $ambig=sqrt(sum(@weights)/max(@weights)); my @final = map {num2note($_)} sort {$weights[$b] <=> $weights[$a] +} (0..11); ($ambig,@final); } my ($ambiguity, @roots) = root('b','d#','f','g#'); foreach (1..int($ambiguity)) { print "Root $_ = $roots[$_-1]\n"; }
    Cheers,
    Ben,

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://282345]
Approved by PodMaster
Front-paged by jeffa
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having an uproarious good time at the Monastery: (4)
As of 2024-03-19 07:46 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found