coltrane 2.2.1 → 3.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +16 -1
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +25 -2
- data/README.md +3 -0
- data/bin/console +6 -2
- data/coltrane.gemspec +2 -1
- data/exe/coltrane +14 -224
- data/lib/coltrane.rb +3 -50
- data/lib/coltrane/commands.rb +14 -0
- data/lib/coltrane/commands/chords.rb +77 -0
- data/lib/coltrane/commands/command.rb +45 -0
- data/lib/coltrane/commands/common_chords.rb +33 -0
- data/lib/coltrane/commands/errors.rb +44 -0
- data/lib/coltrane/commands/find_progression.rb +28 -0
- data/lib/coltrane/commands/find_scale.rb +39 -0
- data/lib/coltrane/commands/notes.rb +44 -0
- data/lib/coltrane/commands/progression.rb +27 -0
- data/lib/{cli → coltrane/commands}/representation.rb +0 -0
- data/lib/coltrane/commands/scale.rb +46 -0
- data/lib/coltrane/renderers.rb +6 -0
- data/lib/coltrane/renderers/renderer.rb +42 -0
- data/lib/coltrane/renderers/text_renderer.rb +23 -0
- data/lib/coltrane/renderers/text_renderer/array_drawer.rb +45 -0
- data/lib/coltrane/renderers/text_renderer/base_drawer.rb +20 -0
- data/lib/coltrane/renderers/text_renderer/hash_drawer.rb +16 -0
- data/lib/coltrane/renderers/text_renderer/representation_guitar_chord_drawer.rb +95 -0
- data/lib/coltrane/renderers/text_renderer/representation_guitar_note_set_drawer.rb +76 -0
- data/lib/coltrane/renderers/text_renderer/representation_piano_note_set_drawer.rb +49 -0
- data/lib/coltrane/renderers/text_renderer/theory_chord_drawer.rb +14 -0
- data/lib/coltrane/renderers/text_renderer/theory_note_set_drawer.rb +17 -0
- data/lib/coltrane/renderers/text_renderer/theory_progression_drawer.rb +13 -0
- data/lib/coltrane/renderers/text_renderer/theory_progression_set_drawer.rb +22 -0
- data/lib/coltrane/renderers/text_renderer/theory_scale_drawer.rb +13 -0
- data/lib/coltrane/renderers/text_renderer/theory_scale_set_drawer.rb +27 -0
- data/lib/coltrane/representation.rb +12 -0
- data/lib/coltrane/representation/guitar.rb +34 -0
- data/lib/coltrane/representation/guitar/chord.rb +180 -0
- data/lib/coltrane/representation/guitar/note.rb +26 -0
- data/lib/coltrane/representation/guitar/note_set.rb +35 -0
- data/lib/coltrane/representation/guitar/string.rb +31 -0
- data/lib/coltrane/representation/guitar_like_instruments.rb +21 -0
- data/lib/coltrane/representation/piano.rb +36 -0
- data/lib/coltrane/representation/piano/note_set.rb +22 -0
- data/lib/{coltrane_synth.rb → coltrane/synth.rb.rb} +0 -0
- data/lib/{coltrane_synth → coltrane/synth}/base.rb +0 -0
- data/lib/{coltrane_synth → coltrane/synth}/synth.rb +0 -0
- data/lib/coltrane/theory.rb +54 -0
- data/lib/coltrane/theory/cadence.rb +9 -0
- data/lib/coltrane/{changes.rb → theory/changes.rb} +0 -0
- data/lib/coltrane/theory/chord.rb +101 -0
- data/lib/coltrane/theory/chord_quality.rb +113 -0
- data/lib/coltrane/theory/chord_substitutions.rb +11 -0
- data/lib/coltrane/theory/circle_of_fifths.rb +33 -0
- data/lib/coltrane/theory/classic_scales.rb +113 -0
- data/lib/coltrane/theory/diatonic_scale.rb +38 -0
- data/lib/coltrane/theory/errors.rb +97 -0
- data/lib/coltrane/theory/frequency.rb +52 -0
- data/lib/coltrane/theory/frequency_interval.rb +83 -0
- data/lib/coltrane/theory/interval.rb +209 -0
- data/lib/coltrane/theory/interval_class.rb +212 -0
- data/lib/coltrane/theory/interval_sequence.rb +157 -0
- data/lib/coltrane/theory/key.rb +18 -0
- data/lib/coltrane/{mode.rb → theory/mode.rb} +0 -0
- data/lib/coltrane/theory/notable_progressions.rb +30 -0
- data/lib/coltrane/theory/note.rb +104 -0
- data/lib/coltrane/theory/note_set.rb +101 -0
- data/lib/coltrane/theory/pitch.rb +94 -0
- data/lib/coltrane/theory/pitch_class.rb +154 -0
- data/lib/coltrane/theory/progression.rb +81 -0
- data/lib/coltrane/theory/progression_set.rb +18 -0
- data/lib/coltrane/{qualities.rb → theory/qualities.rb} +0 -0
- data/lib/coltrane/theory/roman_chord.rb +114 -0
- data/lib/coltrane/theory/scale.rb +161 -0
- data/lib/coltrane/theory/scale_search_result.rb +6 -0
- data/lib/coltrane/theory/scale_set.rb +40 -0
- data/lib/coltrane/theory/voicing.rb +41 -0
- data/lib/coltrane/version.rb +1 -1
- metadata +88 -63
- data/bin/rails +0 -17
- data/data/qualities.yml +0 -83
- data/db/cache.sqlite3 +0 -0
- data/db/cache_test.sqlite3 +0 -0
- data/db/config.yml +0 -11
- data/lib/cli.rb +0 -24
- data/lib/cli/bass_guitar.rb +0 -12
- data/lib/cli/chord.rb +0 -39
- data/lib/cli/config.rb +0 -39
- data/lib/cli/errors.rb +0 -46
- data/lib/cli/guitar.rb +0 -67
- data/lib/cli/guitar_chords.rb +0 -122
- data/lib/cli/notes.rb +0 -20
- data/lib/cli/piano.rb +0 -57
- data/lib/cli/scale.rb +0 -53
- data/lib/cli/text.rb +0 -16
- data/lib/cli/ukulele.rb +0 -14
- data/lib/coltrane/cache.rb +0 -43
- data/lib/coltrane/cadence.rb +0 -7
- data/lib/coltrane/chord.rb +0 -89
- data/lib/coltrane/chord_quality.rb +0 -111
- data/lib/coltrane/chord_substitutions.rb +0 -9
- data/lib/coltrane/circle_of_fifths.rb +0 -31
- data/lib/coltrane/classic_scales.rb +0 -94
- data/lib/coltrane/diatonic_scale.rb +0 -36
- data/lib/coltrane/errors.rb +0 -95
- data/lib/coltrane/frequency.rb +0 -50
- data/lib/coltrane/frequency_interval.rb +0 -81
- data/lib/coltrane/interval.rb +0 -208
- data/lib/coltrane/interval_class.rb +0 -210
- data/lib/coltrane/interval_sequence.rb +0 -155
- data/lib/coltrane/key.rb +0 -16
- data/lib/coltrane/notable_progressions.rb +0 -28
- data/lib/coltrane/note.rb +0 -98
- data/lib/coltrane/note_set.rb +0 -89
- data/lib/coltrane/pitch.rb +0 -92
- data/lib/coltrane/pitch_class.rb +0 -148
- data/lib/coltrane/progression.rb +0 -74
- data/lib/coltrane/roman_chord.rb +0 -112
- data/lib/coltrane/scale.rb +0 -154
- data/lib/coltrane/unordered_interval_class.rb +0 -7
- data/lib/coltrane/voicing.rb +0 -39
- data/lib/coltrane_game/question.rb +0 -7
- data/lib/coltrane_instruments.rb +0 -7
- data/lib/coltrane_instruments/guitar.rb +0 -10
- data/lib/coltrane_instruments/guitar/base.rb +0 -29
- data/lib/coltrane_instruments/guitar/chord.rb +0 -170
- data/lib/coltrane_instruments/guitar/note.rb +0 -24
- data/lib/coltrane_instruments/guitar/string.rb +0 -29
- data/lib/os.rb +0 -21
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
module Theory
|
5
|
+
class Frequency
|
6
|
+
attr_reader :frequency
|
7
|
+
|
8
|
+
def initialize(frequency)
|
9
|
+
@frequency = frequency.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
alias [] new
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#{frequency}hz"
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_f
|
21
|
+
frequency
|
22
|
+
end
|
23
|
+
|
24
|
+
def octave(n)
|
25
|
+
Frequency[frequency * 2**n]
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
frequency == (other.is_a?(Frequency) ? other.frequency : other)
|
30
|
+
end
|
31
|
+
|
32
|
+
def octave_up(n = 1)
|
33
|
+
octave(n)
|
34
|
+
end
|
35
|
+
|
36
|
+
def octave_down(n = 1)
|
37
|
+
octave(-n)
|
38
|
+
end
|
39
|
+
|
40
|
+
def /(other)
|
41
|
+
case other
|
42
|
+
when Frequency then FrequencyInterval[1200 * Math.log2(other.frequency / frequency)]
|
43
|
+
when Numeric then Frequency[frequency / other]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_missing(method, *args)
|
48
|
+
Frequency[frequency.send(method, args[0].to_f)]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
module Theory
|
5
|
+
# Interval describe the logarithmic distance between 2 frequencies.
|
6
|
+
# It's measured in cents.
|
7
|
+
class FrequencyInterval
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
attr_reader :cents
|
11
|
+
|
12
|
+
class << self
|
13
|
+
alias [] new
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(cents)
|
17
|
+
@cents = cents.round
|
18
|
+
end
|
19
|
+
|
20
|
+
def semitones
|
21
|
+
(cents.to_f / 100).round
|
22
|
+
end
|
23
|
+
|
24
|
+
def ascending
|
25
|
+
self.class[cents.abs]
|
26
|
+
end
|
27
|
+
|
28
|
+
def descending
|
29
|
+
self.class[-cents.abs]
|
30
|
+
end
|
31
|
+
|
32
|
+
def ascending?
|
33
|
+
cents > 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def descending?
|
37
|
+
cents < 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def inversion
|
41
|
+
self.class[(-cents.abs % 1200) * (ascending? ? +1 : -1)]
|
42
|
+
end
|
43
|
+
|
44
|
+
def opposite
|
45
|
+
self.class.new(-cents)
|
46
|
+
end
|
47
|
+
|
48
|
+
def interval_class
|
49
|
+
IntervalClass.new(semitones)
|
50
|
+
end
|
51
|
+
|
52
|
+
def ==(other)
|
53
|
+
return false unless other.is_a? FrequencyInterval
|
54
|
+
cents == other.cents
|
55
|
+
end
|
56
|
+
|
57
|
+
alias eql? ==
|
58
|
+
alias hash cents
|
59
|
+
|
60
|
+
def +(other)
|
61
|
+
case other
|
62
|
+
when Numeric then FrequencyInterval[cents + other]
|
63
|
+
when Interval then FrequencyInterval[cents + other.cents]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def -(other)
|
68
|
+
case other
|
69
|
+
when Numeric then FrequencyInterval[cents - other]
|
70
|
+
when Interval then FrequencyInterval[cents - other.cents]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def -@
|
75
|
+
FrequencyInterval[-cents]
|
76
|
+
end
|
77
|
+
|
78
|
+
def <=>(other)
|
79
|
+
cents <=> other.cents
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
module Theory
|
5
|
+
class Interval < IntervalClass
|
6
|
+
attr_reader :letter_distance, :cents
|
7
|
+
alias compound? compound
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def all
|
11
|
+
@all ||= super.map(&:interval)
|
12
|
+
end
|
13
|
+
|
14
|
+
def all_compound
|
15
|
+
@all_compound ||= all.map(&:compound)
|
16
|
+
end
|
17
|
+
|
18
|
+
def all_including_compound
|
19
|
+
@all_including_compound ||= all + all_compound
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_augmented
|
23
|
+
@all_augmented ||= all_including_compound.select(&:has_augmented?)
|
24
|
+
.map(&:augmented)
|
25
|
+
end
|
26
|
+
|
27
|
+
def all_diminished
|
28
|
+
@all_diminished ||= all_including_compound.select(&:has_diminished?)
|
29
|
+
.map(&:diminished)
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_including_compound_and_altered
|
33
|
+
@all_including_compound_and_altered ||=
|
34
|
+
all_including_compound +
|
35
|
+
all_diminished +
|
36
|
+
all_augmented
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(arg_1 = nil, arg_2 = nil, ascending: true,
|
41
|
+
letter_distance: nil,
|
42
|
+
semitones: nil,
|
43
|
+
compound: false)
|
44
|
+
if arg_1 && !arg_2 # assumes arg_1 is a letter
|
45
|
+
@compound = compound
|
46
|
+
IntervalClass[arg_1].interval.yield_self do |interval|
|
47
|
+
@letter_distance = interval.letter_distance
|
48
|
+
@cents = interval.cents
|
49
|
+
end
|
50
|
+
elsif arg_1 && arg_2 # assumes those are notes
|
51
|
+
if ascending
|
52
|
+
@compound = compound
|
53
|
+
@cents =
|
54
|
+
(arg_1.frequency / arg_2.frequency)
|
55
|
+
.interval_class
|
56
|
+
.cents
|
57
|
+
|
58
|
+
@letter_distance = calculate_letter_distance arg_1.letter,
|
59
|
+
arg_2.letter,
|
60
|
+
ascending
|
61
|
+
else
|
62
|
+
self.class.new(arg_1, arg_2).descending.yield_self do |base_interval|
|
63
|
+
@compound = base_interval.compound?
|
64
|
+
@cents = base_interval.cents
|
65
|
+
@letter_distance = base_interval.letter_distance
|
66
|
+
end
|
67
|
+
end
|
68
|
+
elsif letter_distance && semitones
|
69
|
+
@compound = compound || letter_distance > 8
|
70
|
+
@cents = semitones * 100
|
71
|
+
@letter_distance = letter_distance
|
72
|
+
else
|
73
|
+
raise WrongKeywordsError,
|
74
|
+
'[interval_class_name]' \
|
75
|
+
'Provide: [first_note, second_note] || ' \
|
76
|
+
'[letter_distance:, semitones:]'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.[](arg)
|
81
|
+
new(arg)
|
82
|
+
end
|
83
|
+
|
84
|
+
def interval_class
|
85
|
+
FrequencyInterval[cents].interval_class
|
86
|
+
end
|
87
|
+
|
88
|
+
def compound?
|
89
|
+
@compound
|
90
|
+
end
|
91
|
+
|
92
|
+
def has_augmented?
|
93
|
+
name.match? /M|P|A/
|
94
|
+
end
|
95
|
+
|
96
|
+
def has_diminished?
|
97
|
+
name.match? /m|P|d/
|
98
|
+
end
|
99
|
+
|
100
|
+
def accidentals
|
101
|
+
if distance_to_starting.positive? then 'A' * distance_to_starting.abs
|
102
|
+
elsif distance_to_starting.negative? then 'd' * distance_to_starting.abs
|
103
|
+
else ''
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
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
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
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
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
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)
|
190
|
+
end
|
191
|
+
|
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 }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
module Theory
|
5
|
+
# Interval class here is not related to the Object Oriented Programming context
|
6
|
+
# but to the fact that there is a class of intervals that can all be categorized
|
7
|
+
# as having the same quality.
|
8
|
+
#
|
9
|
+
# This class in specific still takes into account the order of intervals.
|
10
|
+
# C to D is a major second, but D to C is a minor seventh.
|
11
|
+
class IntervalClass < FrequencyInterval
|
12
|
+
QUALITY_SEQUENCE = [
|
13
|
+
%w[P],
|
14
|
+
%w[m M],
|
15
|
+
%w[m M],
|
16
|
+
%w[P A],
|
17
|
+
%w[P],
|
18
|
+
%w[m M],
|
19
|
+
%w[m M]
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
ALTERATIONS = {
|
23
|
+
'A' => +1,
|
24
|
+
'd' => -1
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
SINGLE_DISTANCES_NAMES = %w[
|
28
|
+
Unison
|
29
|
+
Second
|
30
|
+
Third
|
31
|
+
Fourth
|
32
|
+
Fifth
|
33
|
+
Sixth
|
34
|
+
Seventh
|
35
|
+
].freeze
|
36
|
+
|
37
|
+
COMPOUND_DISTANCES_NAMES = [
|
38
|
+
'Octave',
|
39
|
+
'Ninth',
|
40
|
+
'Tenth',
|
41
|
+
'Eleventh',
|
42
|
+
'Twelfth',
|
43
|
+
'Thirteenth',
|
44
|
+
'Fourteenth',
|
45
|
+
'Double Octave'
|
46
|
+
].freeze
|
47
|
+
|
48
|
+
DISTANCES_NAMES = (SINGLE_DISTANCES_NAMES + COMPOUND_DISTANCES_NAMES).freeze
|
49
|
+
|
50
|
+
QUALITY_NAMES = {
|
51
|
+
'P' => 'Perfect',
|
52
|
+
'm' => 'Minor',
|
53
|
+
'M' => 'Major',
|
54
|
+
'A' => 'Augmented',
|
55
|
+
'd' => 'Diminished'
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
class << self
|
59
|
+
def distances_names
|
60
|
+
DISTANCES_NAMES
|
61
|
+
end
|
62
|
+
|
63
|
+
def distance_name(n)
|
64
|
+
DISTANCES_NAMES[n - 1]
|
65
|
+
end
|
66
|
+
|
67
|
+
def quality_name(q)
|
68
|
+
QUALITY_NAMES[q]
|
69
|
+
end
|
70
|
+
|
71
|
+
def names
|
72
|
+
@names ||= begin
|
73
|
+
SINGLE_DISTANCES_NAMES.each_with_index.reduce([]) do |i_names, (_d, i)|
|
74
|
+
i_names + QUALITY_SEQUENCE[i % 7].reduce([]) do |qs, q|
|
75
|
+
qs + ["#{q}#{i + 1}"]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def compound_names
|
82
|
+
@compound_names ||= all.map(&:compound_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
def all_names_including_compound
|
86
|
+
@all_names_including_compound ||= names + compound_names
|
87
|
+
end
|
88
|
+
|
89
|
+
def full_names
|
90
|
+
@full_names ||= names.map { |n| expand_name(n) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def all
|
94
|
+
@all ||= names.map { |n| IntervalClass[n] }
|
95
|
+
end
|
96
|
+
|
97
|
+
def full_names_including_compound
|
98
|
+
@full_names_including_compound ||=
|
99
|
+
all_names_including_compound.map { |n| expand_name(n) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def split(interval)
|
103
|
+
interval.scan(/(\w)(\d\d?)/)[0]
|
104
|
+
end
|
105
|
+
|
106
|
+
def expand_name(name)
|
107
|
+
q, n = split(name)
|
108
|
+
(
|
109
|
+
case name
|
110
|
+
when /AA|dd/ then 'Double '
|
111
|
+
when /AAA|ddd/ then 'Triple '
|
112
|
+
else ''
|
113
|
+
end
|
114
|
+
) + "#{quality_name(q)} #{distance_name(n.to_i)}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize(arg)
|
119
|
+
super case arg
|
120
|
+
when FrequencyInterval then arg.semitones
|
121
|
+
when String
|
122
|
+
self.class.names.index(arg) ||
|
123
|
+
self.class.full_names.index(arg) ||
|
124
|
+
self.class.all_names_including_compound.index(arg) ||
|
125
|
+
self.class.full_names_including_compound.index(arg)
|
126
|
+
when Numeric then arg
|
127
|
+
else
|
128
|
+
raise WrongArgumentsError,
|
129
|
+
'Provide: [interval] || [name] || [number of semitones]'
|
130
|
+
end % 12 * 100
|
131
|
+
end
|
132
|
+
|
133
|
+
instance_eval { alias [] new }
|
134
|
+
|
135
|
+
def interval
|
136
|
+
Interval.new(letter_distance: distance, semitones: semitones)
|
137
|
+
end
|
138
|
+
|
139
|
+
def compound_interval
|
140
|
+
Interval.new(
|
141
|
+
letter_distance: distance,
|
142
|
+
semitones: semitones,
|
143
|
+
compound: true
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
alias compound compound_interval
|
148
|
+
|
149
|
+
def ==(other)
|
150
|
+
return false unless other.is_a? FrequencyInterval
|
151
|
+
(semitones % 12) == (other.semitones % 12)
|
152
|
+
end
|
153
|
+
|
154
|
+
def alteration
|
155
|
+
name.chars.reduce(0) { |a, s| a + (ALTERATIONS[s] || 0) }
|
156
|
+
end
|
157
|
+
|
158
|
+
def ascending
|
159
|
+
self.class[semitones.abs]
|
160
|
+
end
|
161
|
+
|
162
|
+
def descending
|
163
|
+
self.class[-semitones.abs]
|
164
|
+
end
|
165
|
+
|
166
|
+
def inversion
|
167
|
+
self.class[-semitones % 12]
|
168
|
+
end
|
169
|
+
|
170
|
+
def full_name
|
171
|
+
self.class.expand_name(name)
|
172
|
+
end
|
173
|
+
|
174
|
+
def name
|
175
|
+
self.class.names[semitones % 12]
|
176
|
+
end
|
177
|
+
|
178
|
+
def compound_name
|
179
|
+
"#{quality}#{distance + 7}"
|
180
|
+
end
|
181
|
+
|
182
|
+
def distance
|
183
|
+
self.class.split(name)[1].to_i
|
184
|
+
end
|
185
|
+
|
186
|
+
def quality
|
187
|
+
self.class.split(name)[0]
|
188
|
+
end
|
189
|
+
|
190
|
+
def +(other)
|
191
|
+
IntervalClass[semitones + other]
|
192
|
+
end
|
193
|
+
|
194
|
+
def -(other)
|
195
|
+
IntervalClass[semitones - other]
|
196
|
+
end
|
197
|
+
|
198
|
+
def -@
|
199
|
+
IntervalClass[-semitones]
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def self.interval_by_full_name(arg)
|
205
|
+
NAMES.invert.each do |full_names, interval_name|
|
206
|
+
return INTERVALS.index(interval_name) if full_names.include?(arg)
|
207
|
+
end
|
208
|
+
raise IntervalNotFoundError, arg
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|