coltrane 0.0.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +15 -10
- data/Gemfile.lock +47 -128
- data/README.md +30 -8
- data/Rakefile +0 -4
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +12 -0
- data/bin/coltrane +25 -7
- data/bin/htmldiff +12 -0
- data/bin/kill-pry-rescue +12 -0
- data/bin/ldiff +12 -0
- data/bin/pry +12 -0
- data/bin/rake +12 -0
- data/bin/rescue +12 -0
- data/bin/rspec +12 -0
- data/bin/rubocop +12 -0
- data/bin/ruby-parse +12 -0
- data/bin/ruby-rewrite +12 -0
- data/exe/coltrane +173 -0
- data/lib/cli/bass_guitar.rb +9 -0
- data/lib/cli/chord.rb +24 -0
- data/lib/cli/errors.rb +28 -0
- data/lib/cli/guitar.rb +55 -0
- data/lib/cli/notes.rb +18 -0
- data/lib/cli/piano.rb +54 -0
- data/lib/cli/representation.rb +47 -0
- data/lib/cli/scale.rb +48 -0
- data/lib/cli/text.rb +13 -0
- data/lib/cli/ukulele.rb +11 -0
- data/lib/coltrane-cli.rb +12 -0
- data/lib/coltrane.rb +16 -31
- data/lib/coltrane/cache.rb +34 -0
- data/lib/coltrane/chord.rb +28 -41
- data/lib/coltrane/chord_quality.rb +14 -39
- data/lib/coltrane/classic_progressions.rb +37 -0
- data/lib/coltrane/classic_scales.rb +92 -118
- data/lib/coltrane/errors.rb +54 -0
- data/lib/coltrane/interval.rb +25 -17
- data/lib/coltrane/interval_sequence.rb +57 -27
- data/lib/coltrane/note.rb +44 -30
- data/lib/coltrane/note_set.rb +37 -22
- data/lib/coltrane/piano_representation.rb +0 -58
- data/lib/coltrane/pitch.rb +12 -3
- data/lib/coltrane/progression.rb +31 -0
- data/lib/coltrane/qualities.rb +115 -114
- data/lib/coltrane/roman_chord.rb +36 -0
- data/lib/coltrane/scale.rb +85 -77
- data/lib/coltrane/version.rb +1 -1
- data/lib/core_ext.rb +12 -0
- data/pkg/coltrane-0.0.2.gem +0 -0
- metadata +27 -14
- data/lib/coltrane/essential_guitar_chords.rb +0 -82
- data/lib/coltrane/fret_set.rb +0 -0
- data/lib/coltrane/guitar.rb +0 -15
- data/lib/coltrane/guitar_chord.rb +0 -50
- data/lib/coltrane/guitar_chord_finder.rb +0 -98
- data/lib/coltrane/guitar_note.rb +0 -50
- data/lib/coltrane/guitar_note_set.rb +0 -61
- data/lib/coltrane/guitar_representation.rb +0 -96
- data/lib/coltrane/guitar_string.rb +0 -52
- data/lib/coltrane/scale_cache.rb +0 -4
@@ -1,48 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
1
|
module Coltrane
|
4
2
|
# It describe the quality of a chord, like maj7 or dim.
|
5
|
-
class ChordQuality
|
3
|
+
class ChordQuality < IntervalSequence
|
6
4
|
attr_reader :name
|
7
5
|
include Qualities
|
8
6
|
|
9
|
-
def initialize(
|
10
|
-
if
|
11
|
-
|
7
|
+
def initialize(name: nil, notes: nil)
|
8
|
+
if !name.nil?
|
9
|
+
if(intervals = CHORD_QUALITIES[name])
|
10
|
+
@name = name
|
11
|
+
super(intervals: intervals)
|
12
|
+
else
|
13
|
+
raise ChordNotFoundError.new
|
14
|
+
end
|
15
|
+
elsif !notes.nil?
|
16
|
+
super(notes: notes)
|
17
|
+
@name = CHORD_QUALITIES.key(intervals_semitones)
|
18
|
+
else
|
19
|
+
raise WrongKeywords.new('[name:] || [notes:]')
|
12
20
|
end
|
13
|
-
@name = find_chord_name(interval_sequence)
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.new_from_notes(notes)
|
17
|
-
note_set = NoteSet.new(notes) unless notes.class == NoteSet
|
18
|
-
new(note_set.interval_sequence.reordered)
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.new_from_pitches(*pitches)
|
22
|
-
notes = pitches.sort_by(&:number)
|
23
|
-
.collect(&:note)
|
24
|
-
.collect(&:name)
|
25
|
-
.uniq
|
26
|
-
|
27
|
-
new_from_notes(notes)
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.new_from_string(quality_string)
|
31
|
-
new(CHORD_QUALITIES[quality_string])
|
32
|
-
end
|
33
|
-
|
34
|
-
def intervals
|
35
|
-
CHORD_QUALITIES[name]
|
36
|
-
end
|
37
|
-
|
38
|
-
protected
|
39
|
-
|
40
|
-
def find_chord_name(note_intervals, inversion = 0)
|
41
|
-
inversion >= note_intervals.all.size && return
|
42
|
-
CHORD_QUALITIES.key(note_intervals.numbers) ||
|
43
|
-
CHORD_QUALITIES.key(note_intervals.numbers.sort) ||
|
44
|
-
find_chord_name(note_intervals.next_inversion, inversion + 1) ||
|
45
|
-
nil
|
46
21
|
end
|
47
22
|
end
|
48
23
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Coltrane
|
2
|
+
module ClassicProgressions
|
3
|
+
PROGRESSIONS = {
|
4
|
+
pop: [:major, [1,5,6,4]],
|
5
|
+
fifties: [:major, [1,6,4,5]],
|
6
|
+
blues: [:major, [1,4,1,5,4,1]],
|
7
|
+
jazz: [:major, [2,5,1]],
|
8
|
+
jazz_minor: [:minor, [2,5,1]],
|
9
|
+
andalusian: [:minor, [1,7,6,5]]
|
10
|
+
}
|
11
|
+
|
12
|
+
def pop(tone)
|
13
|
+
scale, degrees = PROGRESSIONS[:pop]
|
14
|
+
Scale.public_send(scale, tone).progression(*degrees)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fifties(tone)
|
18
|
+
scale, degrees = PROGRESSIONS[:fifties]
|
19
|
+
Scale.public_send(scale, tone).progression(*degrees)
|
20
|
+
end
|
21
|
+
|
22
|
+
def blues(tone)
|
23
|
+
scale, degrees = PROGRESSIONS[:blues]
|
24
|
+
Scale.public_send(scale, tone).progression(*degrees)
|
25
|
+
end
|
26
|
+
|
27
|
+
def jazz(tone)
|
28
|
+
scale, degrees = PROGRESSIONS[:jazz]
|
29
|
+
Scale.public_send(scale, tone).progression(*degrees)
|
30
|
+
end
|
31
|
+
|
32
|
+
def andalusian(tone)
|
33
|
+
scale, degrees = PROGRESSIONS[:andalusian]
|
34
|
+
Scale.public_send(scale, tone).progression(*degrees)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,134 +1,108 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
def hungarian_minor(tone='C', mode=1)
|
30
|
-
new(*SCALES['Hungarian Minor'], tone: tone, mode: mode)
|
31
|
-
end
|
32
|
-
|
33
|
-
def pentatonic_major(tone='C', mode=1)
|
34
|
-
new(*SCALES['Pentatonic Major'], tone: tone, mode: mode)
|
35
|
-
end
|
36
|
-
|
37
|
-
def pentatonic_minor(tone='C', mode=1)
|
38
|
-
new(*SCALES['Pentatonic Minor'], tone: tone, mode: mode)
|
39
|
-
end
|
40
|
-
|
41
|
-
def blues_major(tone='C', mode=1)
|
42
|
-
new(*SCALES['Blues Major'], tone: tone, mode: mode)
|
43
|
-
end
|
44
|
-
|
45
|
-
def blues_minor(tone='C', mode=1)
|
46
|
-
new(*SCALES['Blues Minor'], tone: tone, mode: mode)
|
47
|
-
end
|
48
|
-
|
49
|
-
def whole_tone(tone='C', mode=1)
|
50
|
-
new(*SCALES['Blues Minor'], tone: tone, mode: mode)
|
51
|
-
end
|
1
|
+
module Coltrane
|
2
|
+
module ClassicScales
|
3
|
+
|
4
|
+
SCALES = {
|
5
|
+
'Major' => [2,2,1,2,2,2,1],
|
6
|
+
'Pentatonic Major' => [2,2,3,2,3],
|
7
|
+
'Blues Major' => [2,1,1,3,2,3],
|
8
|
+
'Natural Minor' => [2,1,2,2,1,2,2],
|
9
|
+
'Harmonic Minor' => [2,1,2,2,1,3,1],
|
10
|
+
'Hungarian Minor' => [2,1,2,1,1,3,1],
|
11
|
+
'Pentatonic Minor' => [3,2,2,3,2],
|
12
|
+
'Blues Minor' => [3,2,1,1,3,2],
|
13
|
+
'Whole Tone' => [2,2,2,2,2,2],
|
14
|
+
'Flamenco' => [1,3,1,2,1,2,2]
|
15
|
+
}
|
16
|
+
|
17
|
+
MODES = {
|
18
|
+
'Major' => %w[Ionian Dorian Phrygian Lydian Mixolydian Aeolian Locrian]
|
19
|
+
}
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# A little helper to build method names
|
24
|
+
# just make the code more clear
|
25
|
+
def self.methodize(string)
|
26
|
+
string.downcase.gsub(' ', '_')
|
27
|
+
end
|
52
28
|
|
53
|
-
|
54
|
-
new(*([1]*12), tone: tone, mode: mode)
|
55
|
-
end
|
29
|
+
public
|
56
30
|
|
57
|
-
|
58
|
-
|
59
|
-
|
31
|
+
# Creates factories for scales
|
32
|
+
SCALES.each do |name, distances|
|
33
|
+
define_method methodize(name) do |tone='C', mode=1|
|
34
|
+
new(*distances, tone: tone, mode: mode, name: name)
|
35
|
+
end
|
36
|
+
end
|
60
37
|
|
61
|
-
|
62
|
-
|
63
|
-
|
38
|
+
# Creates factories for Greek Modes and possibly others
|
39
|
+
MODES.each do |scale, modes|
|
40
|
+
modes.each_with_index do |mode, index|
|
41
|
+
scale_method = methodize(scale)
|
42
|
+
mode_name = mode
|
43
|
+
mode_n = index + 1
|
44
|
+
define_method methodize(mode) do |tone='C'|
|
45
|
+
new(*SCALES[scale], tone: tone, mode: mode_n, name: mode_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
64
49
|
|
65
|
-
|
66
|
-
|
67
|
-
|
50
|
+
alias_method :minor, :natural_minor
|
51
|
+
alias_method :pentatonic, :pentatonic_major
|
52
|
+
alias_method :blues, :blues_major
|
68
53
|
|
69
|
-
|
70
|
-
|
71
|
-
|
54
|
+
def known_scales
|
55
|
+
SCALES.keys
|
56
|
+
end
|
72
57
|
|
73
|
-
|
74
|
-
|
75
|
-
|
58
|
+
def fetch(name, tone=nil)
|
59
|
+
Coltrane::Scale.public_send(name, tone)
|
60
|
+
end
|
76
61
|
|
77
|
-
|
78
|
-
|
79
|
-
|
62
|
+
def from_key(key)
|
63
|
+
scale = key.delete!('m') ? :minor : :major
|
64
|
+
note = key
|
65
|
+
Scale.public_send(scale.nil? || scale == 'M' ? :major : :minor, note)
|
66
|
+
end
|
80
67
|
|
81
|
-
def locrian(tone='C')
|
82
|
-
new(*SCALES['Major'], tone: tone, mode: 7)
|
83
|
-
end
|
84
68
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
69
|
+
# Will output a OpenStruct like the following:
|
70
|
+
# {
|
71
|
+
# scales: [array of scales]
|
72
|
+
# results: {
|
73
|
+
# scale_name: {
|
74
|
+
# note_number => found_notes
|
75
|
+
# }
|
76
|
+
# }
|
77
|
+
# }
|
78
|
+
|
79
|
+
def having_notes(notes)
|
80
|
+
|
81
|
+
format = { scales: [], results: {} }
|
82
|
+
OpenStruct.new(
|
83
|
+
SCALES.each_with_object(format) do |(name, intervals), output|
|
84
|
+
Note.all.each.map do |tone|
|
85
|
+
scale = new(*intervals, tone: tone, name: scale)
|
86
|
+
output[:results][name] ||= {}
|
87
|
+
if output[:results][name].has_key?(tone.number)
|
88
|
+
next
|
89
|
+
else
|
90
|
+
output[:scales] << scale if scale.include?(notes)
|
91
|
+
output[:results][name][tone.number] = scale.notes & notes
|
92
|
+
end
|
93
|
+
end
|
102
94
|
end
|
103
|
-
|
104
|
-
end
|
105
|
-
print "\n"
|
95
|
+
)
|
106
96
|
end
|
107
|
-
end
|
108
97
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
SCALES.each_with_object([]) do |scale_obj, results|
|
114
|
-
scale_name, intervals = scale_obj
|
115
|
-
print Paint[scale_name.ljust(scale_name_size), 'yellow']
|
116
|
-
Note.all.each do |note|
|
117
|
-
found_sth = false
|
118
|
-
print ' '
|
119
|
-
scale = Scale.new(*intervals, tone: note.name)
|
120
|
-
notes_included = scale.include_notes?(*notes)
|
121
|
-
if notes_included == notes
|
122
|
-
found_sth = true
|
123
|
-
results << OpenStruct.new({
|
124
|
-
name: "#{note.name} #{scale_name}",
|
125
|
-
notes: notes_included
|
126
|
-
})
|
127
|
-
end
|
128
|
-
print Paint[note.name, found_sth ? 'yellow' : 'black']
|
129
|
-
print Paint["(#{notes_included.count || 0})", found_sth ? 'gray' : 'black']
|
98
|
+
def having_chords(*chords)
|
99
|
+
should_create = !chords.first.is_a?(Chord)
|
100
|
+
notes = chords.reduce(NoteSet[]) do |memo, c|
|
101
|
+
memo + (should_create ? Chord.new(name: c) : c).notes
|
130
102
|
end
|
131
|
-
|
103
|
+
having_notes(notes)
|
132
104
|
end
|
105
|
+
|
106
|
+
alias_method :having_chord, :having_chords
|
133
107
|
end
|
134
108
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Coltrane
|
2
|
+
class ColtraneError < StandardError
|
3
|
+
def initialize(msg)
|
4
|
+
super msg
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class BadConstructor < ColtraneError
|
9
|
+
def initialize(msg=nil)
|
10
|
+
super "Bad constructor. #{msg}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class WrongKeywords < BadConstructor
|
15
|
+
def initialize(msg)
|
16
|
+
super "Use one of the following set of keywords: #{msg}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class InvalidNote < BadConstructor
|
21
|
+
def initialize(note)
|
22
|
+
super "#{note} is not a valid note"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class InvalidNotes < BadConstructor
|
27
|
+
def initialize(notes)
|
28
|
+
super "#{notes} are not a valid set of notes"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class HasNoNotes < BadConstructor
|
33
|
+
def initialize(obj)
|
34
|
+
super "The given object (#{obj.inspect} does not respond to :notes, "\
|
35
|
+
"thereby it can't be used for this operation)"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class WrongDegree
|
40
|
+
def initialize(degree)
|
41
|
+
super "#{degree} is not a valid degree. Degrees for this scale must be between 1 and #{degrees}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ChordNotFoundError < ColtraneError
|
46
|
+
def initialize
|
47
|
+
super "The chord you provided wasn't found. "\
|
48
|
+
"If you're sure this chord exists, "\
|
49
|
+
"would you mind to suggest it's inclusion here: "\
|
50
|
+
"https://github.com/pedrozath/coltrane/issues "\
|
51
|
+
"\n\nA tip tho: always include the letter M for major"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/coltrane/interval.rb
CHANGED
@@ -1,33 +1,41 @@
|
|
1
1
|
module Coltrane
|
2
2
|
# It describes a interval between 2 pitches
|
3
3
|
class Interval
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :semitones
|
5
5
|
|
6
6
|
NAMES = [
|
7
7
|
'1P',
|
8
|
-
'2m',
|
9
|
-
'
|
10
|
-
'
|
8
|
+
'2m',
|
9
|
+
'2M',
|
10
|
+
'3m',
|
11
|
+
'3M',
|
12
|
+
'4P',
|
13
|
+
'4A',
|
11
14
|
'5P',
|
12
|
-
'6m',
|
15
|
+
'6m',
|
16
|
+
'6M',
|
13
17
|
'7m',
|
14
|
-
'7M'
|
15
|
-
'8P',
|
16
|
-
'9m', '9M',
|
17
|
-
'10m', '10M',
|
18
|
-
'11P',
|
19
|
-
'12P',
|
20
|
-
'13m', '13M',
|
21
|
-
'14m', '14M',
|
22
|
-
'15P', '15A'
|
18
|
+
'7M'
|
23
19
|
].freeze
|
24
20
|
|
25
|
-
def initialize(
|
26
|
-
@
|
21
|
+
def initialize(arg)
|
22
|
+
@semitones = (case arg
|
23
|
+
when Interval then arg.semitones
|
24
|
+
when String then NAMES.index(arg)
|
25
|
+
when Numeric then arg
|
26
|
+
end) % 12
|
27
27
|
end
|
28
28
|
|
29
29
|
def name
|
30
|
-
NAMES[
|
30
|
+
NAMES[semitones]
|
31
31
|
end
|
32
|
+
|
33
|
+
def +(x)
|
34
|
+
case x
|
35
|
+
when Numeric then Interval.new(semitones + x)
|
36
|
+
when Interval then Interval.new(semitones + x.semitones)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
32
40
|
end
|
33
41
|
end
|
@@ -1,30 +1,33 @@
|
|
1
1
|
module Coltrane
|
2
2
|
# It describes a sequence of intervals
|
3
3
|
class IntervalSequence
|
4
|
+
extend Forwardable
|
4
5
|
attr_reader :intervals
|
5
6
|
|
6
|
-
|
7
|
-
arg = [arg] if arg.class != Array
|
8
|
-
@intervals = arg.reduce([]) do |memo, arg_item|
|
9
|
-
case arg_item
|
10
|
-
when Numeric then memo << Interval.new(arg_item)
|
11
|
-
when Interval then memo << arg_item
|
12
|
-
when NoteSet then memo + intervals_from_note_set(arg_item)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
7
|
+
def_delegators :@intervals, :map, :each, :[], :size, :reduce
|
16
8
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
9
|
+
def initialize(notes: nil, intervals: nil, distances: nil)
|
10
|
+
if !notes.nil?
|
11
|
+
notes = NoteSet[*notes] if notes.is_a?(Array)
|
12
|
+
@intervals = intervals_from_notes(notes)
|
13
|
+
elsif !intervals.nil?
|
14
|
+
@intervals = intervals.map { |i| Interval.new(i) }
|
15
|
+
elsif !distances.nil?
|
16
|
+
@distances = distances
|
17
|
+
@intervals = intervals_from_distances(distances)
|
18
|
+
else
|
19
|
+
raise 'Provide: [notes:] || [intervals:] || [distances:]'
|
23
20
|
end
|
24
21
|
end
|
25
22
|
|
26
|
-
def
|
27
|
-
|
23
|
+
def distances
|
24
|
+
intervals_semitones[1..-1].each_with_index.map do |n, i|
|
25
|
+
if i == 0
|
26
|
+
n
|
27
|
+
elsif i < intervals_semitones.size
|
28
|
+
n - intervals_semitones[i]
|
29
|
+
end
|
30
|
+
end + [12 - intervals_semitones.last]
|
28
31
|
end
|
29
32
|
|
30
33
|
def all
|
@@ -36,35 +39,62 @@ module Coltrane
|
|
36
39
|
end
|
37
40
|
|
38
41
|
def shift(ammount)
|
39
|
-
IntervalSequence.new(intervals.map do |i|
|
40
|
-
(i.
|
42
|
+
IntervalSequence.new(intervals: intervals.map do |i|
|
43
|
+
(i.semitones + ammount) % 12
|
41
44
|
end)
|
42
45
|
end
|
43
46
|
|
44
47
|
def zero_it
|
45
|
-
self.shift(-intervals.first.
|
48
|
+
self.shift(-intervals.first.semitones)
|
49
|
+
end
|
50
|
+
|
51
|
+
def inversion(index)
|
52
|
+
IntervalSequence.new(intervals: intervals.rotate(index)).zero_it
|
46
53
|
end
|
47
54
|
|
48
55
|
def next_inversion
|
49
|
-
|
56
|
+
inversion(index+1)
|
50
57
|
end
|
51
58
|
|
52
59
|
def previous_inversion
|
53
|
-
|
60
|
+
inversion(index-1)
|
54
61
|
end
|
55
62
|
|
56
63
|
def inversions
|
57
64
|
Array.new(intervals.length) do |index|
|
58
|
-
|
65
|
+
inversion(index)
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
def
|
63
|
-
|
69
|
+
def quality
|
70
|
+
end
|
71
|
+
|
72
|
+
def intervals_semitones
|
73
|
+
map(&:semitones)
|
64
74
|
end
|
65
75
|
|
66
76
|
def names
|
67
|
-
|
77
|
+
map(&:name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def notes_for(root_note)
|
81
|
+
NoteSet[
|
82
|
+
*intervals.reduce([]) do |memo, interval|
|
83
|
+
memo + [root_note + interval]
|
84
|
+
end
|
85
|
+
]
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def intervals_from_distances(distances)
|
91
|
+
distances[0..-2].reduce([Interval.new(0)]) do |memo, d|
|
92
|
+
memo + [memo.last + d]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def intervals_from_notes(notes)
|
97
|
+
notes.map { |n| notes.root - n }.sort_by(&:semitones)
|
68
98
|
end
|
69
99
|
end
|
70
100
|
end
|