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.
- 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,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coltrane
|
|
4
|
+
module Theory
|
|
5
|
+
#
|
|
6
|
+
# Pitch classes, and by classes here we don't mean in the sense of a ruby class
|
|
7
|
+
# are all the classes of pitches (frequencies) that are in a whole number of
|
|
8
|
+
# octaves apart.
|
|
9
|
+
#
|
|
10
|
+
# For example, C1, C2, C3 are all pitches from the C pitch class. Take a look into
|
|
11
|
+
# Notes description if you somehow feel this is confuse and that it could just be
|
|
12
|
+
# called as notes instead.
|
|
13
|
+
#
|
|
14
|
+
class PitchClass
|
|
15
|
+
attr_reader :integer
|
|
16
|
+
include Comparable
|
|
17
|
+
|
|
18
|
+
NOTATION = %w[C C# D D# E F F# G G# A A# B].freeze
|
|
19
|
+
|
|
20
|
+
def self.all_letters
|
|
21
|
+
%w[C D E F G A B]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.all
|
|
25
|
+
NOTATION.map { |n| new(n) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(arg = nil, frequency: nil)
|
|
29
|
+
@integer = case arg
|
|
30
|
+
when String then NOTATION.index(arg)
|
|
31
|
+
when Frequency then frequency_to_integer(Frequency.new(arg))
|
|
32
|
+
when Numeric then (arg % 12)
|
|
33
|
+
when nil then frequency_to_integer(Frequency.new(frequency))
|
|
34
|
+
when PitchClass then arg.integer
|
|
35
|
+
else raise(WrongArgumentsError)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.[](arg, frequency: nil)
|
|
40
|
+
new(arg, frequency: nil)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ==(other)
|
|
44
|
+
integer == other.integer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias eql? ==
|
|
48
|
+
alias hash integer
|
|
49
|
+
|
|
50
|
+
def true_notation
|
|
51
|
+
NOTATION[integer]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def letter
|
|
55
|
+
name[0]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ascending_interval_to(other)
|
|
59
|
+
Interval.new(self, (other.is_a?(PitchClass) ? other : Note.new(other)))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias interval_to ascending_interval_to
|
|
63
|
+
|
|
64
|
+
def descending_interval_to(other)
|
|
65
|
+
Interval.new(
|
|
66
|
+
self,
|
|
67
|
+
(other.is_a?(PitchClass) ? other : Note.new(other)),
|
|
68
|
+
ascending: false
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
alias name true_notation
|
|
73
|
+
|
|
74
|
+
def pretty_name
|
|
75
|
+
name.tr('b', "\u266D").tr('#', "\u266F")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def accidental?
|
|
79
|
+
notation.match? /#|b/
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sharp?
|
|
83
|
+
notation.match? /#/
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def flat?
|
|
87
|
+
notation.match? /b/
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
alias notation true_notation
|
|
91
|
+
alias to_s true_notation
|
|
92
|
+
|
|
93
|
+
def +(other)
|
|
94
|
+
case other
|
|
95
|
+
when Interval then self.class[integer + other.semitones]
|
|
96
|
+
when Integer then self.class[integer + other]
|
|
97
|
+
when PitchClass then self.class[integer + other.integer]
|
|
98
|
+
when Frequency then self.class.new(frequency: frequency + other)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def -(other)
|
|
103
|
+
case other
|
|
104
|
+
when Interval then self.class[integer - other.semitones]
|
|
105
|
+
when Integer then self.class[integer - other]
|
|
106
|
+
when PitchClass then Interval.new(self, other)
|
|
107
|
+
when Frequency then self.class.new(frequency: frequency - other)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pitch_class
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def <=>(other)
|
|
116
|
+
integer <=> other.integer
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def fundamental_frequency
|
|
120
|
+
@fundamental_frequency ||=
|
|
121
|
+
Frequency[
|
|
122
|
+
Theory.base_tuning *
|
|
123
|
+
(2**((integer - Theory::BASE_PITCH_INTEGER.to_f) / 12))
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
alias frequency fundamental_frequency
|
|
128
|
+
|
|
129
|
+
def self.size
|
|
130
|
+
NOTATION.size
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def size
|
|
134
|
+
self.class.size
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def enharmonic?(other)
|
|
138
|
+
case other
|
|
139
|
+
when String then integer == Note[other].integer
|
|
140
|
+
when Note then integer == other.integer
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def frequency_to_integer(f)
|
|
147
|
+
begin
|
|
148
|
+
(::BASE_PITCH_INTEGER +
|
|
149
|
+
size * Math.log(f.to_f / Theory.base_tuning.to_f, 2)) % size
|
|
150
|
+
end.round
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coltrane
|
|
4
|
+
module Theory
|
|
5
|
+
# Allows the creation of chord progressions using standard notations.
|
|
6
|
+
# Ex: Progression.new('I-IV-V', key: 'Am')
|
|
7
|
+
class Progression
|
|
8
|
+
extend NotableProgressions
|
|
9
|
+
include Changes
|
|
10
|
+
attr_reader :scale, :chords, :notation
|
|
11
|
+
|
|
12
|
+
def self.find(*chords)
|
|
13
|
+
chords
|
|
14
|
+
.yield_self { |chords|
|
|
15
|
+
next chords if chords[0].is_a?(Chord)
|
|
16
|
+
chords.map {|c| Chord.new(name: c) }
|
|
17
|
+
}
|
|
18
|
+
.yield_self { |chords|
|
|
19
|
+
NoteSet[*chords.map(&:root_note)]
|
|
20
|
+
.yield_self { |root_notes|
|
|
21
|
+
Scale.having_notes(*root_notes).strict_scales
|
|
22
|
+
}
|
|
23
|
+
.reduce([]) { |memo, scale|
|
|
24
|
+
memo + [Progression.new(chords: chords, scale: scale)]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
.yield_self { |progressions| ProgressionSet.new(*progressions) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(notation = nil, chords: nil, roman_chords: nil, key: nil, scale: nil)
|
|
31
|
+
if notation.nil? && chords.nil? && roman_chords.nil? || key.nil? && scale.nil?
|
|
32
|
+
raise WrongKeywordsError,
|
|
33
|
+
'[chords:, [scale: || key:]] '\
|
|
34
|
+
'[roman_chords:, [scale: || key:]] '\
|
|
35
|
+
'[notation:, [scale: || key:]] '\
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@scale = scale || Key[key]
|
|
39
|
+
@chords =
|
|
40
|
+
if !chords.nil?
|
|
41
|
+
chords
|
|
42
|
+
elsif !roman_chords.nil?
|
|
43
|
+
roman_chords.map(&:chord)
|
|
44
|
+
elsif !notation.nil?
|
|
45
|
+
@notation = notation
|
|
46
|
+
notation.split('-').map { |c| RomanChord.new(c, scale: @scale).chord }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def interval_sequence
|
|
51
|
+
@interval_sequence ||= IntervalSequence(notes: @root_notes)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def root_notes
|
|
55
|
+
@root_notes ||= @chords.map(&:root_note)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def roman_chords
|
|
59
|
+
@roman_chords ||= chords.map do |c|
|
|
60
|
+
RomanChord.new(chord: c, scale: scale)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def notation
|
|
65
|
+
roman_chords.map(&:notation).join('-')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def notes
|
|
69
|
+
NoteSet[*chords.map(&:notes).map(&:notes).flatten]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def notes_out
|
|
73
|
+
notes - scale.notes
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def notes_out_size
|
|
77
|
+
notes_out.size
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coltrane
|
|
4
|
+
module Theory
|
|
5
|
+
class ProgressionSet
|
|
6
|
+
extend Forwardable
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
def_delegators :progressions, :each
|
|
10
|
+
|
|
11
|
+
attr_accessor :progressions
|
|
12
|
+
|
|
13
|
+
def initialize(*progressions)
|
|
14
|
+
@progressions = progressions.sort_by(&:notes_out_size)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coltrane
|
|
4
|
+
module Theory
|
|
5
|
+
# This class deals with chords in roman notation. Ex: IVº.
|
|
6
|
+
class RomanChord
|
|
7
|
+
DIGITS = %w[I II III IV V VI VII].freeze
|
|
8
|
+
NOTATION_REGEX = /(b?[ivIV]*)(.*)/
|
|
9
|
+
|
|
10
|
+
NOTATION_SUBSTITUTIONS = [
|
|
11
|
+
%w[º dim],
|
|
12
|
+
%w[o dim],
|
|
13
|
+
%w[ø m7b5]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(notation = nil, chord: nil, key: nil, scale: nil)
|
|
17
|
+
if notation.nil? && chord.nil? || key.nil? && scale.nil?
|
|
18
|
+
raise WrongKeywordsError,
|
|
19
|
+
'[notation, [scale: || key:]] '\
|
|
20
|
+
'[chord:, [scale: || key:]] '\
|
|
21
|
+
end
|
|
22
|
+
@scale = scale || Key[key]
|
|
23
|
+
if notation
|
|
24
|
+
@notation = notation
|
|
25
|
+
elsif chord
|
|
26
|
+
@chord = chord.is_a?(String) ? Chord.new(name: chord) : chord
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def degree
|
|
31
|
+
return @scale.degree_of_note(root_note) unless @chord.nil?
|
|
32
|
+
d = regexed_notation[:degree]
|
|
33
|
+
@flats = d.count('b')
|
|
34
|
+
d = d.delete('b')
|
|
35
|
+
@degree ||= DIGITS.index(d.upcase) + 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def roman_numeral
|
|
39
|
+
r = DIGITS[degree]
|
|
40
|
+
minor? ? r.downcase : r
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def upcase?
|
|
44
|
+
!!(regexed_notation[:degree][0].match /[[:upper:]]/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def chord
|
|
48
|
+
@chord ||= Chord.new root_note: root_note,
|
|
49
|
+
quality: quality
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def minor?
|
|
53
|
+
quality.has_minor_third?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def major?
|
|
57
|
+
quality.has_major_third?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def quality_name
|
|
61
|
+
return @chord.quality.name unless @chord.nil?
|
|
62
|
+
q = normalize_quality_name(regexed_notation[:quality])
|
|
63
|
+
minor = 'm' if (!q.match? /dim|m7b5/) && !upcase?
|
|
64
|
+
q = [minor, q].join
|
|
65
|
+
q.empty? ? 'M' : q
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def quality
|
|
69
|
+
return @chord.quality unless @chord.nil?
|
|
70
|
+
ChordQuality.new(name: quality_name) if quality_name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def root_note
|
|
74
|
+
return @chord.root_note unless @chord.nil?
|
|
75
|
+
@scale[degree] - @flats
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def notation
|
|
79
|
+
q = case quality_name
|
|
80
|
+
when 'm', 'M' then ''
|
|
81
|
+
when 'm7', 'M' then '7'
|
|
82
|
+
else quality_name
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@notation ||= [
|
|
86
|
+
roman_numeral,
|
|
87
|
+
q
|
|
88
|
+
].join
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def function
|
|
92
|
+
return if @scale.name != 'Major'
|
|
93
|
+
%w[Tonic Supertonic Mediant Subdominant
|
|
94
|
+
Dominant Submediant Leading][degree - 1]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def regexed_notation
|
|
100
|
+
@regexed_notation ||= begin
|
|
101
|
+
matchdata = @notation.match(NOTATION_REGEX)
|
|
102
|
+
{ degree: matchdata[1], quality: matchdata[2] }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def normalize_quality_name(quality_name)
|
|
107
|
+
NOTATION_SUBSTITUTIONS.reduce(quality_name) do |memo, subs|
|
|
108
|
+
break memo if memo.empty?
|
|
109
|
+
memo.gsub(*subs)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coltrane
|
|
4
|
+
module Theory
|
|
5
|
+
# Musical scale creation and manipulation
|
|
6
|
+
class Scale
|
|
7
|
+
extend ClassicScales
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators :notes, :accidentals, :sharps, :flats
|
|
11
|
+
|
|
12
|
+
attr_reader :interval_sequence, :tone
|
|
13
|
+
|
|
14
|
+
def initialize(*relative_intervals, tone: 'C',
|
|
15
|
+
mode: 1,
|
|
16
|
+
name: nil,
|
|
17
|
+
notes: nil)
|
|
18
|
+
@name = name
|
|
19
|
+
if relative_intervals.any? && tone
|
|
20
|
+
@tone = Note[tone]
|
|
21
|
+
relative_intervals = relative_intervals.rotate(mode - 1)
|
|
22
|
+
@interval_sequence = IntervalSequence.new(
|
|
23
|
+
relative_intervals: relative_intervals
|
|
24
|
+
)
|
|
25
|
+
elsif notes
|
|
26
|
+
@notes = NoteSet[*notes]
|
|
27
|
+
@tone = @notes.first
|
|
28
|
+
ds = @notes.interval_sequence.relative_intervals
|
|
29
|
+
@interval_sequence = IntervalSequence.new(relative_intervals: ds)
|
|
30
|
+
else
|
|
31
|
+
raise WrongKeywordsError,
|
|
32
|
+
'[*relative_intervals, tone: "C", mode: 1] || [notes:]'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def id
|
|
37
|
+
[(name || @interval_sequence), tone.number]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
id == other.id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def name
|
|
45
|
+
@name ||= begin
|
|
46
|
+
is = interval_sequence.relative_intervals
|
|
47
|
+
(0...is.size).each do |i|
|
|
48
|
+
if (scale_name = ::ClassicScales::SCALES.key(is.rotate(i)))
|
|
49
|
+
return scale_name
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def full_name
|
|
57
|
+
"#{tone} #{name}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
alias to_s full_name
|
|
61
|
+
|
|
62
|
+
def pretty_name
|
|
63
|
+
"#{tone.pretty_name} #{name}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
alias full_name pretty_name
|
|
67
|
+
|
|
68
|
+
def degree(d)
|
|
69
|
+
raise WrongDegreeError, d if d < 1 || d > size
|
|
70
|
+
tone + interval_sequence[d - 1].semitones
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
alias [] degree
|
|
74
|
+
|
|
75
|
+
def degrees
|
|
76
|
+
(1..size)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def degree_of_chord(chord)
|
|
80
|
+
return if chords(chord.size).map(&:name).include?(chord.name)
|
|
81
|
+
degree_of_note(chord.root_note)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def degree_of_note(note)
|
|
85
|
+
notes.index(note)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def &(other)
|
|
89
|
+
raise HasNoNotesError unless other.respond_to?(:notes)
|
|
90
|
+
notes & other
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def include_notes?(arg)
|
|
94
|
+
noteset = arg.is_a?(Note) ? NoteSet[arg] : arg
|
|
95
|
+
(self & noteset).size == noteset.size
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
alias include? include_notes?
|
|
99
|
+
|
|
100
|
+
def notes
|
|
101
|
+
@notes ||= NoteSet[*degrees.map { |d| degree(d) }]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def interval(i)
|
|
105
|
+
interval_sequence[(i - 1) % size]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def size
|
|
109
|
+
interval_sequence.size
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def tertians(n = 3)
|
|
113
|
+
degrees.size.times.reduce([]) do |memo, d|
|
|
114
|
+
ns = NoteSet[ *Array.new(n) { |i| notes[(d + (i * 2)) % size] } ]
|
|
115
|
+
begin
|
|
116
|
+
chord = Chord.new(notes: ns)
|
|
117
|
+
rescue ChordNotFoundError
|
|
118
|
+
memo
|
|
119
|
+
else
|
|
120
|
+
memo + [chord]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def triads
|
|
126
|
+
tertians(3)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def sevenths
|
|
130
|
+
tertians(4)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def pentads
|
|
134
|
+
tertians(5)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def progression(*degrees)
|
|
138
|
+
Progression.new(self, degrees)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def chords(size = 3..12)
|
|
142
|
+
size = (size..size) if size.is_a?(Integer)
|
|
143
|
+
scale_rotations = interval_sequence.inversions
|
|
144
|
+
ChordQuality.intervals_per_name.reduce([]) do |memo1, (qname, qintervals)|
|
|
145
|
+
next memo1 unless size.include?(qintervals.size)
|
|
146
|
+
memo1 + scale_rotations.each_with_index
|
|
147
|
+
.reduce([]) do |memo2, (rot, index)|
|
|
148
|
+
if (rot & qintervals).size == qintervals.size
|
|
149
|
+
memo2 + [Chord.new(root_note: degree(index + 1),
|
|
150
|
+
quality: ChordQuality.new(name: qname))]
|
|
151
|
+
else
|
|
152
|
+
memo2
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alias all_chords chords
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|