coltrane 2.2.1 → 3.0.0.pre

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.
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