coltrane 1.2.4 → 2.0.0
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/CHANGELOG.md +14 -1
- data/Gemfile +3 -0
- data/Gemfile.lock +48 -3
- data/Rakefile +1 -1
- data/bin/erubis +12 -0
- data/bin/flay +29 -0
- data/bin/gitlab +29 -0
- data/bin/httparty +29 -0
- data/bin/pronto +29 -0
- data/bin/ruby_parse +29 -0
- data/bin/ruby_parse_extract_error +29 -0
- data/bin/thor +12 -0
- data/exe/coltrane +8 -6
- data/lib/cli/guitar.rb +7 -7
- data/lib/cli/representation.rb +1 -1
- data/lib/coltrane.rb +22 -1
- data/lib/coltrane/cadence.rb +0 -1
- data/lib/coltrane/changes.rb +5 -7
- data/lib/coltrane/chord.rb +7 -7
- data/lib/coltrane/chord_quality.rb +17 -17
- data/lib/coltrane/chord_substitutions.rb +3 -1
- data/lib/coltrane/classic_scales.rb +7 -7
- data/lib/coltrane/errors.rb +26 -1
- data/lib/coltrane/frequency.rb +50 -0
- data/lib/coltrane/interval.rb +23 -86
- data/lib/coltrane/interval_class.rb +106 -0
- data/lib/coltrane/interval_sequence.rb +14 -13
- data/lib/coltrane/notable_progressions.rb +8 -3
- data/lib/coltrane/note.rb +44 -73
- data/lib/coltrane/note_set.rb +4 -4
- data/lib/coltrane/pitch.rb +43 -22
- data/lib/coltrane/pitch_class.rb +113 -0
- data/lib/coltrane/progression.rb +6 -9
- data/lib/coltrane/roman_chord.rb +14 -14
- data/lib/coltrane/scale.rb +8 -10
- data/lib/coltrane/unordered_interval_class.rb +7 -0
- data/lib/coltrane/version.rb +1 -1
- data/lib/coltrane_instruments.rb +4 -0
- data/lib/coltrane_instruments/guitar.rb +7 -0
- data/lib/coltrane_instruments/guitar/base.rb +14 -0
- data/lib/coltrane_instruments/guitar/chord.rb +41 -0
- data/lib/coltrane_instruments/guitar/note.rb +8 -0
- data/lib/coltrane_instruments/guitar/string.rb +8 -0
- data/lib/core_ext.rb +16 -27
- metadata +18 -2
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
#
|
5
|
+
# Pitch classes, and by classes here we don't mean in the sense of a ruby class
|
6
|
+
# are all the classes of pitches (frequencies) that are in a whole number of
|
7
|
+
# octaves apart.
|
8
|
+
#
|
9
|
+
# For example, C1, C2, C3 are all pitches from the C pitch class. Take a look into
|
10
|
+
# Notes description if you somehow feel this is confuse and that it could just be
|
11
|
+
# called as notes instead.
|
12
|
+
#
|
13
|
+
class PitchClass
|
14
|
+
attr_reader :integer
|
15
|
+
|
16
|
+
NOTATION = %w[C C# D D# E F F# G G# A A# B].freeze
|
17
|
+
|
18
|
+
def self.all
|
19
|
+
NOTATION.map { |n| new(n) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(arg, frequency: nil)
|
23
|
+
@integer = case arg
|
24
|
+
when String then NOTATION.index(arg)
|
25
|
+
when Frequency, Float then frequency_to_integer(Frequency.new(arg))
|
26
|
+
when Integer then (arg % 12)
|
27
|
+
when NilClass then frequency_to_integer(Frequency.new(frequency))
|
28
|
+
when PitchClass then arg.integer
|
29
|
+
else raise(InvalidArgumentError)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.[](arg, frequency: nil)
|
34
|
+
new(arg, frequency: nil)
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
integer == other.integer
|
39
|
+
end
|
40
|
+
|
41
|
+
alias eql? ==
|
42
|
+
alias hash integer
|
43
|
+
|
44
|
+
def true_notation
|
45
|
+
NOTATION[integer]
|
46
|
+
end
|
47
|
+
|
48
|
+
alias name true_notation
|
49
|
+
|
50
|
+
def pretty_name
|
51
|
+
name.tr('b', "\u266D").tr('#', "\u266F")
|
52
|
+
end
|
53
|
+
|
54
|
+
def accidental?
|
55
|
+
notation.match? /#|b/
|
56
|
+
end
|
57
|
+
|
58
|
+
alias notation true_notation
|
59
|
+
alias to_s true_notation
|
60
|
+
|
61
|
+
def +(other)
|
62
|
+
case other
|
63
|
+
when Interval then PitchClass[integer + other.semitones]
|
64
|
+
when Integer then PitchClass[integer + other]
|
65
|
+
when PitchClass then Note.new(integer + other.integer)
|
66
|
+
when Frequency then PitchClass.new(frequency: frequency + other)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def -(other)
|
71
|
+
case other
|
72
|
+
when Interval then PitchClass[integer - other.semitones]
|
73
|
+
when Integer then PitchClass[integer - other]
|
74
|
+
when PitchClass then IntervalClass[frequency / other.frequency]
|
75
|
+
when Frequency then PitchClass.new(frequency: frequency - other)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def fundamental_frequency
|
80
|
+
@fundamental_frequency ||=
|
81
|
+
Frequency[
|
82
|
+
Coltrane.base_tuning *
|
83
|
+
(2**((integer - Coltrane::BASE_PITCH_INTEGER.to_f) / 12))
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
alias frequency fundamental_frequency
|
88
|
+
|
89
|
+
def self.size
|
90
|
+
NOTATION.size
|
91
|
+
end
|
92
|
+
|
93
|
+
def size
|
94
|
+
self.class.size
|
95
|
+
end
|
96
|
+
|
97
|
+
def enharmonic?(other)
|
98
|
+
case other
|
99
|
+
when String then integer == Note[other].integer
|
100
|
+
when Note then integer == other.integer
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def frequency_to_integer(f)
|
107
|
+
begin
|
108
|
+
(Coltrane::BASE_PITCH_INTEGER +
|
109
|
+
size * Math.log(f.to_f / Coltrane.base_tuning.to_f, 2)) % size
|
110
|
+
end.round
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/coltrane/progression.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Coltrane
|
4
|
-
|
5
4
|
# Allows the creation of chord progressions using standard notations.
|
6
5
|
# Ex: Progression.new('I-IV-V', key: 'Am')
|
7
6
|
class Progression
|
@@ -10,9 +9,7 @@ module Coltrane
|
|
10
9
|
attr_reader :scale, :chords, :notation
|
11
10
|
|
12
11
|
def self.find(*chords)
|
13
|
-
if chords[0].is_a?(String)
|
14
|
-
chords.map! { |c| Chord.new(name: c) }
|
15
|
-
end
|
12
|
+
chords.map! { |c| Chord.new(name: c) } if chords[0].is_a?(String)
|
16
13
|
|
17
14
|
root_notes = NoteSet[*chords.map(&:root_note)]
|
18
15
|
|
@@ -24,12 +21,12 @@ module Coltrane
|
|
24
21
|
progressions.sort_by(&:notes_out_size)
|
25
22
|
end
|
26
23
|
|
27
|
-
def initialize(notation=nil, chords: nil, roman_chords: nil, key: nil, scale: nil)
|
24
|
+
def initialize(notation = nil, chords: nil, roman_chords: nil, key: nil, scale: nil)
|
28
25
|
if notation.nil? && chords.nil? && roman_chords.nil? || key.nil? && scale.nil?
|
29
26
|
raise WrongKeywordsError,
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
'[chords:, [scale: || key:]] '\
|
28
|
+
'[roman_chords:, [scale: || key:]] '\
|
29
|
+
'[notation:, [scale: || key:]] '\
|
33
30
|
end
|
34
31
|
|
35
32
|
@scale = scale || Scale.from_key(key)
|
@@ -40,7 +37,7 @@ module Coltrane
|
|
40
37
|
roman_chords.map(&:chord)
|
41
38
|
elsif !notation.nil?
|
42
39
|
@notation = notation
|
43
|
-
notation.split('-').map {|c| RomanChord.new(c, scale: @scale).chord }
|
40
|
+
notation.split('-').map { |c| RomanChord.new(c, scale: @scale).chord }
|
44
41
|
end
|
45
42
|
end
|
46
43
|
|
data/lib/coltrane/roman_chord.rb
CHANGED
@@ -4,27 +4,27 @@ module Coltrane
|
|
4
4
|
# This class deals with chords in roman notation. Ex: IVº.
|
5
5
|
class RomanChord
|
6
6
|
DIGITS = %w[I II III IV V VI VII].freeze
|
7
|
-
NOTATION_REGEX =
|
7
|
+
NOTATION_REGEX = /
|
8
8
|
(?<degree>b?[ivIV]*)
|
9
9
|
(?<quality>.*)
|
10
|
-
|
10
|
+
/x
|
11
11
|
|
12
12
|
NOTATION_SUBSTITUTIONS = [
|
13
13
|
%w[º dim],
|
14
14
|
%w[o dim],
|
15
15
|
%w[ø m7b5]
|
16
|
-
]
|
16
|
+
].freeze
|
17
17
|
|
18
|
-
def initialize(notation=nil, chord: nil, key: nil, scale: nil)
|
18
|
+
def initialize(notation = nil, chord: nil, key: nil, scale: nil)
|
19
19
|
if notation.nil? && chord.nil? || key.nil? && scale.nil?
|
20
20
|
raise WrongKeywordsError,
|
21
|
-
|
22
|
-
|
21
|
+
'[notation, [scale: || key:]] '\
|
22
|
+
'[chord:, [scale: || key:]] '\
|
23
23
|
end
|
24
24
|
@scale = scale || Scale.from_key(key)
|
25
|
-
if
|
25
|
+
if notation
|
26
26
|
@notation = notation
|
27
|
-
elsif
|
27
|
+
elsif chord
|
28
28
|
@chord = chord.is_a?(String) ? Chord.new(name: chord) : chord
|
29
29
|
end
|
30
30
|
end
|
@@ -62,7 +62,7 @@ module Coltrane
|
|
62
62
|
def quality_name
|
63
63
|
return @chord.quality.name unless @chord.nil?
|
64
64
|
q = normalize_quality_name(regexed_notation['quality'])
|
65
|
-
minor = 'm' if !q.match?(
|
65
|
+
minor = 'm' if !q.match?(/dim|m7b5/) && !upcase?
|
66
66
|
q = [minor, q].join
|
67
67
|
q.empty? ? 'M' : q
|
68
68
|
end
|
@@ -79,21 +79,21 @@ module Coltrane
|
|
79
79
|
|
80
80
|
def notation
|
81
81
|
q = case quality_name
|
82
|
-
|
83
|
-
|
84
|
-
|
82
|
+
when 'm', 'M' then ''
|
83
|
+
when 'm7', 'M' then '7'
|
84
|
+
else quality_name
|
85
85
|
end
|
86
86
|
|
87
87
|
@notation ||= [
|
88
88
|
roman_numeral,
|
89
|
-
q
|
89
|
+
q
|
90
90
|
].join
|
91
91
|
end
|
92
92
|
|
93
93
|
def function
|
94
94
|
return if @scale.name != 'Major'
|
95
95
|
%w[Tonic Supertonic Mediant Subdominant
|
96
|
-
|
96
|
+
Dominant Submediant Leading][degree - 1]
|
97
97
|
end
|
98
98
|
|
99
99
|
private
|
data/lib/coltrane/scale.rb
CHANGED
@@ -8,11 +8,11 @@ module Coltrane
|
|
8
8
|
|
9
9
|
def initialize(*distances, tone: 'C', mode: 1, name: nil, notes: nil)
|
10
10
|
@name = name
|
11
|
-
if
|
11
|
+
if distances.any? && tone
|
12
12
|
@tone = Note[tone]
|
13
13
|
distances = distances.rotate(mode - 1)
|
14
14
|
@interval_sequence = IntervalSequence.new(distances: distances)
|
15
|
-
elsif
|
15
|
+
elsif notes
|
16
16
|
ds = NoteSet[*notes].interval_sequence.distances
|
17
17
|
new(*ds, tone: notes.first)
|
18
18
|
else
|
@@ -119,25 +119,23 @@ module Coltrane
|
|
119
119
|
Progression.new(self, degrees)
|
120
120
|
end
|
121
121
|
|
122
|
-
def all_chords
|
123
|
-
chords
|
124
|
-
end
|
125
|
-
|
126
122
|
def chords(size = 3..12)
|
127
123
|
size = (size..size) if size.is_a?(Integer)
|
128
|
-
included_names = []
|
129
124
|
scale_rotations = interval_sequence.inversions
|
130
125
|
ChordQuality.intervals_per_name.reduce([]) do |memo1, (qname, qintervals)|
|
131
126
|
next memo1 unless size.include?(qintervals.size)
|
132
|
-
memo1 + scale_rotations.each_with_index
|
127
|
+
memo1 + scale_rotations.each_with_index
|
128
|
+
.reduce([]) do |memo2, (rot, index)|
|
133
129
|
if (rot & qintervals).size == qintervals.size
|
134
|
-
memo2 + [
|
135
|
-
|
130
|
+
memo2 + [Chord.new(root_note: degree(index + 1),
|
131
|
+
quality: ChordQuality.new(name: qname))]
|
136
132
|
else
|
137
133
|
memo2
|
138
134
|
end
|
139
135
|
end
|
140
136
|
end
|
141
137
|
end
|
138
|
+
|
139
|
+
alias all_chords chords
|
142
140
|
end
|
143
141
|
end
|
data/lib/coltrane/version.rb
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColtraneInstruments
|
4
|
+
module Guitar
|
5
|
+
# A base class for operations involving Guitars
|
6
|
+
class Base
|
7
|
+
def initialize; end
|
8
|
+
|
9
|
+
def find_chord(target_chord)
|
10
|
+
Chord.new(target_chord, guitar: self).fetch_descendant_chords
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ColtraneInstruments
|
4
|
+
module Guitar
|
5
|
+
# This class represents a group of guitar notes, strummed at the same time
|
6
|
+
class Chord
|
7
|
+
def initialize(target_chord, guitar_notes: [],
|
8
|
+
free_fingers: 4,
|
9
|
+
barre: nil,
|
10
|
+
guitar:)
|
11
|
+
@target_chord = target_chord
|
12
|
+
@guitar_notes = guitar_notes
|
13
|
+
@free_fingers = free_fingers
|
14
|
+
@barre = barre
|
15
|
+
end
|
16
|
+
|
17
|
+
def barre?
|
18
|
+
!@barre.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_descendant_chords
|
22
|
+
return self if guitar_notes.size < guitar.strings
|
23
|
+
possible_new_notes.reduce([]) do |memo, n|
|
24
|
+
fingers_change = n.fret == @barre ? 0 : 1
|
25
|
+
return self unless (@free_fingers - fingers_change).negative?
|
26
|
+
guitar.create_chord target_chord,
|
27
|
+
guitar_notes: @guitar_notes + n,
|
28
|
+
free_fingers: @free_fingers - fingers_change,
|
29
|
+
barre: @barre
|
30
|
+
.fetch_descendant_chords + memo
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def possible_new_notes; end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_writer :barre
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/core_ext.rb
CHANGED
@@ -24,6 +24,7 @@ class String
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
# Here we add some syntax sugar to make the code more understandable later
|
27
28
|
class Integer
|
28
29
|
def interval_name
|
29
30
|
{
|
@@ -46,12 +47,17 @@ class Integer
|
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
50
|
+
# Here we add some methods better work with Tries
|
49
51
|
class Hash
|
50
|
-
def clone_values(from_keys: nil,
|
51
|
-
|
52
|
+
def clone_values(from_keys: nil,
|
53
|
+
to_keys: nil,
|
54
|
+
suffix: nil,
|
55
|
+
branch_a: nil,
|
56
|
+
branch_b: nil)
|
57
|
+
branch_a ||= dig(*from_keys)
|
52
58
|
if branch_b.nil?
|
53
|
-
|
54
|
-
branch_b =
|
59
|
+
create_branch!(*to_keys)
|
60
|
+
branch_b = dig(*to_keys)
|
55
61
|
end
|
56
62
|
|
57
63
|
branch_a.each do |key, val|
|
@@ -73,30 +79,13 @@ class Hash
|
|
73
79
|
|
74
80
|
def deep_dup
|
75
81
|
dup_hash = {}
|
76
|
-
|
77
|
-
if v.is_a?(Hash)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
+
each do |k, v|
|
83
|
+
dup_hash[k] = if v.is_a?(Hash)
|
84
|
+
v.deep_dup
|
85
|
+
else
|
86
|
+
v.dup
|
87
|
+
end
|
82
88
|
end
|
83
89
|
dup_hash
|
84
90
|
end
|
85
91
|
end
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|