coltrane 2.1.5 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +17 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -2
- data/Gemfile.lock +9 -21
- data/README.md +4 -2
- data/bin/console +1 -1
- data/coltrane.gemspec +1 -1
- data/lib/cli.rb +1 -1
- data/lib/cli/chord.rb +1 -1
- data/lib/cli/config.rb +4 -3
- data/lib/cli/guitar_chords.rb +31 -31
- data/lib/cli/scale.rb +1 -1
- data/lib/coltrane.rb +4 -3
- data/lib/coltrane/chord_quality.rb +4 -4
- data/lib/coltrane/circle_of_fifths.rb +4 -2
- data/lib/coltrane/classic_scales.rb +2 -2
- data/lib/coltrane/diatonic_scale.rb +3 -1
- data/lib/coltrane/frequency.rb +1 -1
- data/lib/coltrane/frequency_interval.rb +81 -0
- data/lib/coltrane/interval.rb +177 -33
- data/lib/coltrane/interval_class.rb +147 -49
- data/lib/coltrane/interval_sequence.rb +53 -54
- data/lib/coltrane/key.rb +3 -1
- data/lib/coltrane/notable_progressions.rb +0 -1
- data/lib/coltrane/note.rb +4 -10
- data/lib/coltrane/note_set.rb +2 -2
- data/lib/coltrane/pitch_class.rb +24 -2
- data/lib/coltrane/qualities.rb +171 -169
- data/lib/coltrane/scale.rb +22 -15
- data/lib/coltrane/version.rb +1 -1
- data/lib/coltrane/voicing.rb +3 -2
- data/lib/coltrane_game/question.rb +4 -3
- data/lib/coltrane_instruments/guitar.rb +1 -1
- data/lib/coltrane_instruments/guitar/base.rb +1 -1
- data/lib/coltrane_instruments/guitar/chord.rb +18 -16
- data/lib/coltrane_synth.rb +3 -1
- data/lib/coltrane_synth/base.rb +9 -7
- data/lib/coltrane_synth/synth.rb +3 -1
- data/lib/core_ext.rb +1 -30
- data/lib/os.rb +4 -2
- metadata +19 -3
data/lib/coltrane/frequency.rb
CHANGED
@@ -38,7 +38,7 @@ module Coltrane
|
|
38
38
|
|
39
39
|
def /(other)
|
40
40
|
case other
|
41
|
-
when Frequency then
|
41
|
+
when Frequency then FrequencyInterval[1200 * Math.log2(other.frequency / frequency)]
|
42
42
|
when Numeric then Frequency[frequency / other]
|
43
43
|
end
|
44
44
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
# Interval describe the logarithmic distance between 2 frequencies.
|
5
|
+
# It's measured in cents.
|
6
|
+
class FrequencyInterval
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
attr_reader :cents
|
10
|
+
|
11
|
+
class << self
|
12
|
+
alias [] new
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(cents)
|
16
|
+
@cents = cents.round
|
17
|
+
end
|
18
|
+
|
19
|
+
def semitones
|
20
|
+
(cents.to_f / 100).round
|
21
|
+
end
|
22
|
+
|
23
|
+
def ascending
|
24
|
+
self.class[cents.abs]
|
25
|
+
end
|
26
|
+
|
27
|
+
def descending
|
28
|
+
self.class[-cents.abs]
|
29
|
+
end
|
30
|
+
|
31
|
+
def ascending?
|
32
|
+
cents > 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def descending?
|
36
|
+
cents < 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def inversion
|
40
|
+
self.class[(-cents.abs % 1200) * (ascending? ? +1 : -1)]
|
41
|
+
end
|
42
|
+
|
43
|
+
def opposite
|
44
|
+
self.class.new(-cents)
|
45
|
+
end
|
46
|
+
|
47
|
+
def interval_class
|
48
|
+
IntervalClass.new(semitones)
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(other)
|
52
|
+
return false unless other.is_a? FrequencyInterval
|
53
|
+
cents == other.cents
|
54
|
+
end
|
55
|
+
|
56
|
+
alias eql? ==
|
57
|
+
alias hash cents
|
58
|
+
|
59
|
+
def +(other)
|
60
|
+
case other
|
61
|
+
when Numeric then FrequencyInterval[cents + other]
|
62
|
+
when Interval then FrequencyInterval[cents + other.cents]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def -(other)
|
67
|
+
case other
|
68
|
+
when Numeric then FrequencyInterval[cents - other]
|
69
|
+
when Interval then FrequencyInterval[cents - other.cents]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def -@
|
74
|
+
FrequencyInterval[-cents]
|
75
|
+
end
|
76
|
+
|
77
|
+
def <=>(other)
|
78
|
+
cents <=> other.cents
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/coltrane/interval.rb
CHANGED
@@ -1,64 +1,208 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Coltrane
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
include Comparable
|
8
|
-
|
9
|
-
attr_reader :cents
|
4
|
+
class Interval < IntervalClass
|
5
|
+
attr_reader :letter_distance, :cents
|
6
|
+
alias compound? compound
|
10
7
|
|
11
8
|
class << self
|
12
|
-
|
9
|
+
def all
|
10
|
+
@all ||= super.map(&:interval)
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_compound
|
14
|
+
@all_compound ||= all.map(&:compound)
|
15
|
+
end
|
16
|
+
|
17
|
+
def all_including_compound
|
18
|
+
@all_including_compound ||= all + all_compound
|
19
|
+
end
|
13
20
|
|
14
|
-
def
|
15
|
-
|
21
|
+
def all_augmented
|
22
|
+
@all_augmented ||= all_including_compound.select(&:has_augmented?)
|
23
|
+
.map(&:augmented)
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_diminished
|
27
|
+
@all_diminished ||= all_including_compound.select(&:has_diminished?)
|
28
|
+
.map(&:diminished)
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_including_compound_and_altered
|
32
|
+
@all_including_compound_and_altered ||=
|
33
|
+
all_including_compound +
|
34
|
+
all_diminished +
|
35
|
+
all_augmented
|
16
36
|
end
|
17
37
|
end
|
18
38
|
|
19
|
-
def initialize(
|
20
|
-
|
39
|
+
def initialize(arg_1 = nil, arg_2 = nil, ascending: true,
|
40
|
+
letter_distance: nil,
|
41
|
+
semitones: nil,
|
42
|
+
compound: false)
|
43
|
+
if arg_1 && !arg_2 # assumes arg_1 is a letter
|
44
|
+
@compound = compound
|
45
|
+
IntervalClass[arg_1].interval.yield_self do |interval|
|
46
|
+
@letter_distance = interval.letter_distance
|
47
|
+
@cents = interval.cents
|
48
|
+
end
|
49
|
+
elsif arg_1 && arg_2 # assumes those are notes
|
50
|
+
if ascending
|
51
|
+
@compound = compound
|
52
|
+
@cents =
|
53
|
+
(arg_1.frequency / arg_2.frequency)
|
54
|
+
.interval_class
|
55
|
+
.cents
|
56
|
+
|
57
|
+
@letter_distance = calculate_letter_distance arg_1.letter,
|
58
|
+
arg_2.letter,
|
59
|
+
ascending
|
60
|
+
else
|
61
|
+
self.class.new(arg_1, arg_2).descending.yield_self do |base_interval|
|
62
|
+
@compound = base_interval.compound?
|
63
|
+
@cents = base_interval.cents
|
64
|
+
@letter_distance = base_interval.letter_distance
|
65
|
+
end
|
66
|
+
end
|
67
|
+
elsif letter_distance && semitones
|
68
|
+
@compound = compound || letter_distance > 8
|
69
|
+
@cents = semitones * 100
|
70
|
+
@letter_distance = letter_distance
|
71
|
+
else
|
72
|
+
raise WrongKeywordsError,
|
73
|
+
'[interval_class_name]' \
|
74
|
+
'Provide: [first_note, second_note] || ' \
|
75
|
+
'[letter_distance:, semitones:]'
|
76
|
+
end
|
21
77
|
end
|
22
78
|
|
23
|
-
def
|
24
|
-
(
|
79
|
+
def self.[](arg)
|
80
|
+
new(arg)
|
25
81
|
end
|
26
82
|
|
27
|
-
def
|
28
|
-
cents
|
83
|
+
def interval_class
|
84
|
+
FrequencyInterval[cents].interval_class
|
29
85
|
end
|
30
86
|
|
31
|
-
def
|
32
|
-
|
87
|
+
def compound?
|
88
|
+
@compound
|
33
89
|
end
|
34
90
|
|
35
|
-
def
|
36
|
-
|
91
|
+
def has_augmented?
|
92
|
+
name.match?(%r{M|P|A})
|
37
93
|
end
|
38
94
|
|
39
|
-
|
40
|
-
|
95
|
+
def has_diminished?
|
96
|
+
name.match?(%r{m|P|d})
|
97
|
+
end
|
98
|
+
|
99
|
+
def accidentals
|
100
|
+
case
|
101
|
+
when distance_to_starting.positive? then 'A'
|
102
|
+
when distance_to_starting.negative? then 'd'
|
103
|
+
else return ''
|
104
|
+
end * distance_to_starting.abs
|
105
|
+
end
|
41
106
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
107
|
+
def name
|
108
|
+
@name ||= begin
|
109
|
+
if distance_to_starting.zero? || distance_to_starting.abs > 2
|
110
|
+
compound? ? interval_class.compound_name : interval_class.name
|
111
|
+
else
|
112
|
+
"#{accidentals}#{starting_interval.distance + (compound? ? 7 : 0)}"
|
113
|
+
end
|
46
114
|
end
|
47
115
|
end
|
48
116
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
117
|
+
def as(n)
|
118
|
+
i = clone(letter_distance: n)
|
119
|
+
i if i.name.match?(n.to_s)
|
120
|
+
end
|
121
|
+
|
122
|
+
def as!(n)
|
123
|
+
i = as(n)
|
124
|
+
i unless i&.name&.match?(/d|A/)
|
125
|
+
end
|
126
|
+
|
127
|
+
def as_diminished(n = 1)
|
128
|
+
as(letter_distance + n)
|
129
|
+
end
|
130
|
+
|
131
|
+
def as_augmented(n = 1)
|
132
|
+
as(letter_distance - n)
|
133
|
+
end
|
134
|
+
|
135
|
+
def clone(override_args = {})
|
136
|
+
self.class.new({
|
137
|
+
semitones: semitones,
|
138
|
+
letter_distance: letter_distance,
|
139
|
+
compound: compound?
|
140
|
+
}.merge(override_args))
|
141
|
+
end
|
142
|
+
|
143
|
+
def diminish(n = 1)
|
144
|
+
clone(semitones: semitones - n)
|
145
|
+
end
|
146
|
+
|
147
|
+
alias diminished diminish
|
148
|
+
|
149
|
+
def augment(n = 1)
|
150
|
+
clone(semitones: semitones + n)
|
151
|
+
end
|
152
|
+
|
153
|
+
alias augmented augment
|
154
|
+
|
155
|
+
def opposite
|
156
|
+
clone(semitones: -semitones, letter_distance: (-letter_distance % 8) + 1)
|
157
|
+
end
|
158
|
+
|
159
|
+
def ascending
|
160
|
+
ascending? ? self : opposite
|
161
|
+
end
|
162
|
+
|
163
|
+
def descending
|
164
|
+
descending? ? self : opposite
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def starting_interval # select the closest interval possible to start from
|
170
|
+
@starting_interval ||= begin
|
171
|
+
IntervalClass.all
|
172
|
+
.select { |i| i.distance == normalized_letter_distance }
|
173
|
+
.sort_by do |i|
|
174
|
+
(cents - i.cents)
|
175
|
+
.yield_self { |d| [(d % 1200), (d % -1200)].min_by(&:abs) }
|
176
|
+
.abs
|
177
|
+
end
|
178
|
+
.first
|
53
179
|
end
|
54
180
|
end
|
55
181
|
|
56
|
-
def
|
57
|
-
|
182
|
+
def normalized_letter_distance
|
183
|
+
return letter_distance if letter_distance < 8
|
184
|
+
(letter_distance % 8) + 1
|
185
|
+
end
|
186
|
+
|
187
|
+
def distance_to_starting # calculate the closts distance to it
|
188
|
+
d = semitones - starting_interval.semitones
|
189
|
+
[(d % 12), (d % -12)].min_by(&:abs)
|
58
190
|
end
|
59
191
|
|
60
|
-
def
|
61
|
-
|
192
|
+
def all_letters
|
193
|
+
PitchClass.all_letters
|
194
|
+
end
|
195
|
+
|
196
|
+
def calculate_letter_distance(a, b, asc)
|
197
|
+
all_letters
|
198
|
+
.rotate(all_letters.index(asc ? a : b))
|
199
|
+
.index(b) + 1
|
200
|
+
end
|
201
|
+
|
202
|
+
public
|
203
|
+
|
204
|
+
all_including_compound_and_altered.each do |interval|
|
205
|
+
self.class.define_method(interval.full_name.underscore) { interval.clone }
|
62
206
|
end
|
63
207
|
end
|
64
208
|
end
|
@@ -7,85 +7,183 @@ module Coltrane
|
|
7
7
|
#
|
8
8
|
# This class in specific still takes into account the order of intervals.
|
9
9
|
# C to D is a major second, but D to C is a minor seventh.
|
10
|
-
class IntervalClass <
|
11
|
-
|
10
|
+
class IntervalClass < FrequencyInterval
|
11
|
+
QUALITY_SEQUENCE = [
|
12
|
+
%w[P],
|
13
|
+
%w[m M],
|
14
|
+
%w[m M],
|
15
|
+
%w[P A],
|
16
|
+
%w[P],
|
17
|
+
%w[m M],
|
18
|
+
%w[m M]
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
ALTERATIONS = {
|
22
|
+
'A' => +1,
|
23
|
+
'd' => -1
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
SINGLE_DISTANCES_NAMES = %w[
|
27
|
+
Unison
|
28
|
+
Second
|
29
|
+
Third
|
30
|
+
Fourth
|
31
|
+
Fifth
|
32
|
+
Sixth
|
33
|
+
Seventh
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
COMPOUND_DISTANCES_NAMES = [
|
37
|
+
'Octave',
|
38
|
+
'Ninth',
|
39
|
+
'Tenth',
|
40
|
+
'Eleventh',
|
41
|
+
'Twelfth',
|
42
|
+
'Thirteenth',
|
43
|
+
'Fourteenth',
|
44
|
+
'Double Octave'
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
DISTANCES_NAMES = (SINGLE_DISTANCES_NAMES + COMPOUND_DISTANCES_NAMES).freeze
|
48
|
+
|
49
|
+
QUALITY_NAMES = {
|
50
|
+
'P' => 'Perfect',
|
51
|
+
'm' => 'Minor',
|
52
|
+
'M' => 'Major',
|
53
|
+
'A' => 'Augmented',
|
54
|
+
'd' => 'Diminished'
|
55
|
+
}.freeze
|
56
|
+
|
57
|
+
class << self
|
58
|
+
def distances_names
|
59
|
+
DISTANCES_NAMES
|
60
|
+
end
|
12
61
|
|
13
|
-
|
14
|
-
|
15
|
-
|
62
|
+
def distance_name(n)
|
63
|
+
DISTANCES_NAMES[n - 1]
|
64
|
+
end
|
16
65
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
66
|
+
def quality_name(q)
|
67
|
+
QUALITY_NAMES[q]
|
68
|
+
end
|
21
69
|
|
22
|
-
|
70
|
+
def names
|
71
|
+
@names ||= begin
|
72
|
+
SINGLE_DISTANCES_NAMES.each_with_index.reduce([]) do |i_names, (_d, i)|
|
73
|
+
i_names + QUALITY_SEQUENCE[i % 7].reduce([]) do |qs, q|
|
74
|
+
qs + ["#{q}#{i + 1}"]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
23
79
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
num = o * 7 + i.to_i
|
31
|
-
prev_q = split(INTERVALS[(index - 1) % 12])[0]
|
32
|
-
next_q = split(INTERVALS[(index + 1) % 12])[0]
|
33
|
-
memo[interval] << full_name("#{q}#{num}")
|
34
|
-
memo[interval] << full_name("d#{(num - 1 + 1) % 14 + 1}") if next_q.match? /m|P/
|
35
|
-
next if q == 'A'
|
36
|
-
memo[interval] << full_name("A#{(num - 1 - 1) % 14 + 1}") if prev_q.match? /M|P/
|
80
|
+
def compound_names
|
81
|
+
@compound_names ||= all.map(&:compound_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
def all_names_including_compound
|
85
|
+
@all_names_including_compound ||= names + compound_names
|
37
86
|
end
|
38
|
-
end
|
39
87
|
|
40
|
-
|
88
|
+
def full_names
|
89
|
+
@full_names ||= names.map { |n| expand_name(n) }
|
90
|
+
end
|
41
91
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
92
|
+
def all
|
93
|
+
@all ||= names.map { |n| IntervalClass[n] }
|
94
|
+
end
|
95
|
+
|
96
|
+
def full_names_including_compound
|
97
|
+
@full_names_including_compound ||=
|
98
|
+
all_names_including_compound.map { |n| expand_name(n) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def split(interval)
|
102
|
+
interval.scan(/(\w)(\d\d?)/)[0]
|
103
|
+
end
|
104
|
+
|
105
|
+
def expand_name(name)
|
106
|
+
q, n = split(name)
|
107
|
+
(
|
108
|
+
case name
|
109
|
+
when /AA|dd/ then 'Double '
|
110
|
+
when /AAA|ddd/ then 'Triple '
|
111
|
+
else ''
|
112
|
+
end
|
113
|
+
) + "#{quality_name(q)} #{distance_name(n.to_i)}"
|
50
114
|
end
|
51
115
|
end
|
52
116
|
|
53
117
|
def initialize(arg)
|
54
118
|
super case arg
|
55
|
-
when
|
119
|
+
when FrequencyInterval then arg.semitones
|
56
120
|
when String
|
57
|
-
|
121
|
+
self.class.names.index(arg) ||
|
122
|
+
self.class.full_names.index(arg) ||
|
123
|
+
self.class.all_names_including_compound.index(arg) ||
|
124
|
+
self.class.full_names_including_compound.index(arg)
|
58
125
|
when Numeric then arg
|
59
|
-
else
|
126
|
+
else
|
127
|
+
raise WrongArgumentsError,
|
128
|
+
'Provide: [interval] || [name] || [number of semitones]'
|
60
129
|
end % 12 * 100
|
61
130
|
end
|
62
131
|
|
63
|
-
|
64
|
-
new semis
|
65
|
-
end
|
132
|
+
instance_eval { alias [] new }
|
66
133
|
|
67
|
-
def
|
68
|
-
|
134
|
+
def interval
|
135
|
+
Interval.new(letter_distance: distance, semitones: semitones)
|
69
136
|
end
|
70
137
|
|
71
|
-
def
|
72
|
-
|
138
|
+
def compound_interval
|
139
|
+
Interval.new(
|
140
|
+
letter_distance: distance,
|
141
|
+
semitones: semitones,
|
142
|
+
compound: true
|
143
|
+
)
|
73
144
|
end
|
74
145
|
|
146
|
+
alias compound compound_interval
|
147
|
+
|
75
148
|
def ==(other)
|
76
|
-
|
149
|
+
return false unless other.is_a? FrequencyInterval
|
150
|
+
(semitones % 12) == (other.semitones % 12)
|
77
151
|
end
|
78
152
|
|
79
|
-
def
|
80
|
-
|
153
|
+
def alteration
|
154
|
+
name.chars.reduce(0) { |a, s| a + (ALTERATIONS[s] || 0) }
|
155
|
+
end
|
156
|
+
|
157
|
+
def ascending
|
158
|
+
self.class[semitones.abs]
|
159
|
+
end
|
160
|
+
|
161
|
+
def descending
|
162
|
+
self.class[-semitones.abs]
|
163
|
+
end
|
164
|
+
|
165
|
+
def inversion
|
166
|
+
self.class[-semitones % 12]
|
81
167
|
end
|
82
168
|
|
83
169
|
def full_name
|
84
|
-
self.class.
|
170
|
+
self.class.expand_name(name)
|
171
|
+
end
|
172
|
+
|
173
|
+
def name
|
174
|
+
self.class.names[semitones % 12]
|
175
|
+
end
|
176
|
+
|
177
|
+
def compound_name
|
178
|
+
"#{quality}#{distance + 7}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def distance
|
182
|
+
self.class.split(name)[1].to_i
|
85
183
|
end
|
86
184
|
|
87
|
-
def
|
88
|
-
|
185
|
+
def quality
|
186
|
+
self.class.split(name)[0]
|
89
187
|
end
|
90
188
|
|
91
189
|
def +(other)
|