coltrane 2.1.5 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|