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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +16 -1
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile.lock +25 -2
  5. data/README.md +3 -0
  6. data/bin/console +6 -2
  7. data/coltrane.gemspec +2 -1
  8. data/exe/coltrane +14 -224
  9. data/lib/coltrane.rb +3 -50
  10. data/lib/coltrane/commands.rb +14 -0
  11. data/lib/coltrane/commands/chords.rb +77 -0
  12. data/lib/coltrane/commands/command.rb +45 -0
  13. data/lib/coltrane/commands/common_chords.rb +33 -0
  14. data/lib/coltrane/commands/errors.rb +44 -0
  15. data/lib/coltrane/commands/find_progression.rb +28 -0
  16. data/lib/coltrane/commands/find_scale.rb +39 -0
  17. data/lib/coltrane/commands/notes.rb +44 -0
  18. data/lib/coltrane/commands/progression.rb +27 -0
  19. data/lib/{cli → coltrane/commands}/representation.rb +0 -0
  20. data/lib/coltrane/commands/scale.rb +46 -0
  21. data/lib/coltrane/renderers.rb +6 -0
  22. data/lib/coltrane/renderers/renderer.rb +42 -0
  23. data/lib/coltrane/renderers/text_renderer.rb +23 -0
  24. data/lib/coltrane/renderers/text_renderer/array_drawer.rb +45 -0
  25. data/lib/coltrane/renderers/text_renderer/base_drawer.rb +20 -0
  26. data/lib/coltrane/renderers/text_renderer/hash_drawer.rb +16 -0
  27. data/lib/coltrane/renderers/text_renderer/representation_guitar_chord_drawer.rb +95 -0
  28. data/lib/coltrane/renderers/text_renderer/representation_guitar_note_set_drawer.rb +76 -0
  29. data/lib/coltrane/renderers/text_renderer/representation_piano_note_set_drawer.rb +49 -0
  30. data/lib/coltrane/renderers/text_renderer/theory_chord_drawer.rb +14 -0
  31. data/lib/coltrane/renderers/text_renderer/theory_note_set_drawer.rb +17 -0
  32. data/lib/coltrane/renderers/text_renderer/theory_progression_drawer.rb +13 -0
  33. data/lib/coltrane/renderers/text_renderer/theory_progression_set_drawer.rb +22 -0
  34. data/lib/coltrane/renderers/text_renderer/theory_scale_drawer.rb +13 -0
  35. data/lib/coltrane/renderers/text_renderer/theory_scale_set_drawer.rb +27 -0
  36. data/lib/coltrane/representation.rb +12 -0
  37. data/lib/coltrane/representation/guitar.rb +34 -0
  38. data/lib/coltrane/representation/guitar/chord.rb +180 -0
  39. data/lib/coltrane/representation/guitar/note.rb +26 -0
  40. data/lib/coltrane/representation/guitar/note_set.rb +35 -0
  41. data/lib/coltrane/representation/guitar/string.rb +31 -0
  42. data/lib/coltrane/representation/guitar_like_instruments.rb +21 -0
  43. data/lib/coltrane/representation/piano.rb +36 -0
  44. data/lib/coltrane/representation/piano/note_set.rb +22 -0
  45. data/lib/{coltrane_synth.rb → coltrane/synth.rb.rb} +0 -0
  46. data/lib/{coltrane_synth → coltrane/synth}/base.rb +0 -0
  47. data/lib/{coltrane_synth → coltrane/synth}/synth.rb +0 -0
  48. data/lib/coltrane/theory.rb +54 -0
  49. data/lib/coltrane/theory/cadence.rb +9 -0
  50. data/lib/coltrane/{changes.rb → theory/changes.rb} +0 -0
  51. data/lib/coltrane/theory/chord.rb +101 -0
  52. data/lib/coltrane/theory/chord_quality.rb +113 -0
  53. data/lib/coltrane/theory/chord_substitutions.rb +11 -0
  54. data/lib/coltrane/theory/circle_of_fifths.rb +33 -0
  55. data/lib/coltrane/theory/classic_scales.rb +113 -0
  56. data/lib/coltrane/theory/diatonic_scale.rb +38 -0
  57. data/lib/coltrane/theory/errors.rb +97 -0
  58. data/lib/coltrane/theory/frequency.rb +52 -0
  59. data/lib/coltrane/theory/frequency_interval.rb +83 -0
  60. data/lib/coltrane/theory/interval.rb +209 -0
  61. data/lib/coltrane/theory/interval_class.rb +212 -0
  62. data/lib/coltrane/theory/interval_sequence.rb +157 -0
  63. data/lib/coltrane/theory/key.rb +18 -0
  64. data/lib/coltrane/{mode.rb → theory/mode.rb} +0 -0
  65. data/lib/coltrane/theory/notable_progressions.rb +30 -0
  66. data/lib/coltrane/theory/note.rb +104 -0
  67. data/lib/coltrane/theory/note_set.rb +101 -0
  68. data/lib/coltrane/theory/pitch.rb +94 -0
  69. data/lib/coltrane/theory/pitch_class.rb +154 -0
  70. data/lib/coltrane/theory/progression.rb +81 -0
  71. data/lib/coltrane/theory/progression_set.rb +18 -0
  72. data/lib/coltrane/{qualities.rb → theory/qualities.rb} +0 -0
  73. data/lib/coltrane/theory/roman_chord.rb +114 -0
  74. data/lib/coltrane/theory/scale.rb +161 -0
  75. data/lib/coltrane/theory/scale_search_result.rb +6 -0
  76. data/lib/coltrane/theory/scale_set.rb +40 -0
  77. data/lib/coltrane/theory/voicing.rb +41 -0
  78. data/lib/coltrane/version.rb +1 -1
  79. metadata +88 -63
  80. data/bin/rails +0 -17
  81. data/data/qualities.yml +0 -83
  82. data/db/cache.sqlite3 +0 -0
  83. data/db/cache_test.sqlite3 +0 -0
  84. data/db/config.yml +0 -11
  85. data/lib/cli.rb +0 -24
  86. data/lib/cli/bass_guitar.rb +0 -12
  87. data/lib/cli/chord.rb +0 -39
  88. data/lib/cli/config.rb +0 -39
  89. data/lib/cli/errors.rb +0 -46
  90. data/lib/cli/guitar.rb +0 -67
  91. data/lib/cli/guitar_chords.rb +0 -122
  92. data/lib/cli/notes.rb +0 -20
  93. data/lib/cli/piano.rb +0 -57
  94. data/lib/cli/scale.rb +0 -53
  95. data/lib/cli/text.rb +0 -16
  96. data/lib/cli/ukulele.rb +0 -14
  97. data/lib/coltrane/cache.rb +0 -43
  98. data/lib/coltrane/cadence.rb +0 -7
  99. data/lib/coltrane/chord.rb +0 -89
  100. data/lib/coltrane/chord_quality.rb +0 -111
  101. data/lib/coltrane/chord_substitutions.rb +0 -9
  102. data/lib/coltrane/circle_of_fifths.rb +0 -31
  103. data/lib/coltrane/classic_scales.rb +0 -94
  104. data/lib/coltrane/diatonic_scale.rb +0 -36
  105. data/lib/coltrane/errors.rb +0 -95
  106. data/lib/coltrane/frequency.rb +0 -50
  107. data/lib/coltrane/frequency_interval.rb +0 -81
  108. data/lib/coltrane/interval.rb +0 -208
  109. data/lib/coltrane/interval_class.rb +0 -210
  110. data/lib/coltrane/interval_sequence.rb +0 -155
  111. data/lib/coltrane/key.rb +0 -16
  112. data/lib/coltrane/notable_progressions.rb +0 -28
  113. data/lib/coltrane/note.rb +0 -98
  114. data/lib/coltrane/note_set.rb +0 -89
  115. data/lib/coltrane/pitch.rb +0 -92
  116. data/lib/coltrane/pitch_class.rb +0 -148
  117. data/lib/coltrane/progression.rb +0 -74
  118. data/lib/coltrane/roman_chord.rb +0 -112
  119. data/lib/coltrane/scale.rb +0 -154
  120. data/lib/coltrane/unordered_interval_class.rb +0 -7
  121. data/lib/coltrane/voicing.rb +0 -39
  122. data/lib/coltrane_game/question.rb +0 -7
  123. data/lib/coltrane_instruments.rb +0 -7
  124. data/lib/coltrane_instruments/guitar.rb +0 -10
  125. data/lib/coltrane_instruments/guitar/base.rb +0 -29
  126. data/lib/coltrane_instruments/guitar/chord.rb +0 -170
  127. data/lib/coltrane_instruments/guitar/note.rb +0 -24
  128. data/lib/coltrane_instruments/guitar/string.rb +0 -29
  129. 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