From: Brian Candler Date: 2010-03-04T01:34:31+09:00 Subject: Re: Music Theory (#229) Here's my version. I think it handles the "spelling" of 7-note scales correctly, but the 8-note scales don't always give a satisfactory answer, e.g. C#dim7 => C# E Fx A# (most people would use G rather than F double sharp) Regards, Brian. class Note NOTES = "ABCDEFG" SEMITONES = [0, 2, 3, 5, 7, 8, 10] # semitones above A attr_reader :note # 0-6 representing A-G attr_reader :semi # 0-11 representing A to G#/Ab ACCIDENTAL = {"bb"=>-2, "b"=>-1, ""=>0, "#"=>1, "x"=>2} # Parse a note like "C#..." # Return a Note object plus the remainder of the string def self.parse(str) raise "Invalid note" unless str =~ /\A([A-G])([b#]?)(.*)\z/ note = NOTES.index($1) semi = SEMITONES[note] + ACCIDENTAL[$2] return [new(note, semi), $3] end # Create a note. # new(0,0) => A # new(0,1) => A# # new(1,1) => Bb # new(1,2) => B # new(1,3) => B# # new(2,2) => Cb # new(2,3) => C def initialize(note, semi=SEMITONES[note]) @note, @semi = note % 7, semi % 12 end def to_s acc = (@semi - SEMITONES[@note] + 6) % 12 - 6 str = if acc < 0 "b" * -acc elsif acc == 2 "x" else "#" * acc end NOTES[@note,1] + str end # return a new note which is N degrees along and M semitones along. e.g. # fsharp(1,1) => G (one note up, one semitone up) # fsharp(1,2) => G# (one note up, two semitones up) # fsharp(2,2) => Ab (two notes up, two semitones up) def offset(degree_offset, semi_offset) self.class.new(@note + degree_offset, @semi + semi_offset) end # return an array of notes, given an array of [degree,semitone] offsets # representing a scale, and an array of indexes into that array def scale(pairs, degrees = [1,3,5,7]) res = [] degrees.each_with_index do |d,i| pair = pairs[(d-1) % pairs.size] res << offset(pair[0], pair[1]) end res end # Convert a scale into its nth mode def self.mode(pairs, mode) a = pairs.dup (mode-1).times { a.push(a.shift) } d0, s0 = a.first a.map { |d,s| [d-d0, s-s0] } end Ionian = [[0,0], [1,2], [2,4], [3,5], [4,7], [5,9], [6,11]] Dorian = mode(Ionian, 2) Phrygian = mode(Ionian, 3) Lydian = mode(Ionian, 4) Mixolydian = mode(Ionian, 5) Aeolian = mode(Ionian, 6) Locrian = mode(Ionian, 7) MelodicMinor = [[0,0], [1,2], [2,3], [3,5], [4,7], [5,9], [6,11]] PhrygianNatural6 = mode(MelodicMinor, 2) LydianAugmented = mode(MelodicMinor, 3) LydianDominant = mode(MelodicMinor, 4) MixolydianFlat6 = mode(MelodicMinor, 5) AeolianFlat5 = mode(MelodicMinor, 6) Altered = mode(MelodicMinor, 7) Diminished = [[0,0], [1,2], [2,3], [3,5], [3,6], [4,8], [5,9], [6,11]] EightNoteDominant = mode(Diminished, 2) Chords = { "" => [Ionian], "m" => [MelodicMinor], "m7" => [Dorian], "7" => [Mixolydian], "7+4" => [LydianDominant, [1,3,4,5,7]], "7alt" => [Altered, [1,3,5,7,9,11,13]], "dim7" => [Diminished, [1,3,5,7]], "7b9" => [EightNoteDominant, [1,3,5,7,2,4,6,8]], # Expand this at your leisure } def self.chord(str) root, chordsym = parse(str) chordarg = Chords[chordsym] || (raise "Unknown chord: #{chordsym.inspect}") root.scale(*chordarg) end end if __FILE__ == $0 while str = $stdin.gets str.chomp! puts Note.chord(str).join(" ") end end -- Posted via http://www.ruby-forum.com/.