coltrane 1.2.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/cli/guitar.rb
CHANGED
@@ -24,7 +24,7 @@ module Coltrane
|
|
24
24
|
string_note = Note[string]
|
25
25
|
Array.new(@frets + 2) do |i|
|
26
26
|
if i.zero?
|
27
|
-
Paint[string, HSL.new(140 + str_i * 20,50,50).html]
|
27
|
+
Paint[string, HSL.new(140 + str_i * 20, 50, 50).html]
|
28
28
|
else
|
29
29
|
fret = i - 1
|
30
30
|
note = string_note + fret
|
@@ -37,14 +37,14 @@ module Coltrane
|
|
37
37
|
|
38
38
|
def render_special_frets
|
39
39
|
' ' +
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
Array.new(@frets + 2) do |fret|
|
41
|
+
m = SPECIAL_FRETS.include?(fret) ? fret.to_s.rjust(2, 0.to_s) : ' '
|
42
|
+
"#{m}#{' ' if fret.zero?}"
|
43
|
+
end.join(' ')
|
44
44
|
end
|
45
45
|
|
46
46
|
def place_empty(str_i)
|
47
|
-
Paint['--', HSL.new(180 + str_i * 3,50,30).html]
|
47
|
+
Paint['--', HSL.new(180 + str_i * 3, 50, 30).html]
|
48
48
|
end
|
49
49
|
|
50
50
|
def place_mark(note)
|
@@ -57,7 +57,7 @@ module Coltrane
|
|
57
57
|
else raise WrongFlavorError
|
58
58
|
end
|
59
59
|
|
60
|
-
base_hue = (180 + note.
|
60
|
+
base_hue = (180 + note.integer * 10) % 360 # + 260
|
61
61
|
Paint[
|
62
62
|
mark,
|
63
63
|
HSL.new(0, 0, 100).html,
|
data/lib/cli/representation.rb
CHANGED
data/lib/coltrane.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'yaml'
|
3
4
|
|
4
5
|
require 'forwardable'
|
@@ -10,11 +11,15 @@ require 'coltrane/version'
|
|
10
11
|
require 'coltrane/errors'
|
11
12
|
require 'coltrane/cadence'
|
12
13
|
|
13
|
-
require 'coltrane/
|
14
|
+
require 'coltrane/frequency'
|
14
15
|
require 'coltrane/pitch'
|
16
|
+
require 'coltrane/pitch_class'
|
17
|
+
require 'coltrane/note'
|
15
18
|
require 'coltrane/note_set'
|
16
19
|
|
17
20
|
require 'coltrane/interval'
|
21
|
+
require 'coltrane/interval_class'
|
22
|
+
require 'coltrane/unordered_interval_class'
|
18
23
|
require 'coltrane/interval_sequence'
|
19
24
|
|
20
25
|
require 'coltrane/chord_quality'
|
@@ -31,3 +36,19 @@ require 'coltrane/changes'
|
|
31
36
|
require 'coltrane/progression'
|
32
37
|
|
33
38
|
require 'coltrane/mode'
|
39
|
+
|
40
|
+
# The main module for working with Music Theory
|
41
|
+
module Coltrane
|
42
|
+
BASE_OCTAVE = 4
|
43
|
+
BASE_PITCH_INTEGER = 9
|
44
|
+
|
45
|
+
def self.tuning=(f)
|
46
|
+
@base_tuning = Frequency[f].octave(-4)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.base_tuning
|
50
|
+
@base_tuning
|
51
|
+
end
|
52
|
+
|
53
|
+
@base_tuning = Frequency[440].octave(-4)
|
54
|
+
end
|
data/lib/coltrane/cadence.rb
CHANGED
data/lib/coltrane/changes.rb
CHANGED
data/lib/coltrane/chord.rb
CHANGED
@@ -8,19 +8,19 @@ module Coltrane
|
|
8
8
|
include ChordSubstitutions
|
9
9
|
|
10
10
|
def initialize(notes: nil, root_note: nil, quality: nil, name: nil)
|
11
|
-
if
|
11
|
+
if notes
|
12
12
|
notes = NoteSet[*notes] if notes.is_a?(Array)
|
13
13
|
@notes = notes
|
14
14
|
@root_note = notes.first
|
15
15
|
@quality = ChordQuality.new(notes: notes)
|
16
|
-
elsif
|
16
|
+
elsif root_note && quality
|
17
17
|
@notes = quality.notes_for(root_note)
|
18
18
|
@root_note = root_note
|
19
19
|
@quality = quality
|
20
|
-
elsif
|
20
|
+
elsif name
|
21
21
|
@root_note, @quality, @notes = parse_from_name(name)
|
22
22
|
else
|
23
|
-
|
23
|
+
raise WrongKeywordsError,
|
24
24
|
'[notes:] || [root_note:, quality:] || [name:]'
|
25
25
|
end
|
26
26
|
end
|
@@ -76,13 +76,13 @@ module Coltrane
|
|
76
76
|
protected
|
77
77
|
|
78
78
|
def parse_from_name(name)
|
79
|
-
chord_name, bass = name.split('/')
|
80
|
-
chord_regex =
|
79
|
+
chord_name, bass = name.match?(/\/9/) ? [name, nil] : name.split('/')
|
80
|
+
chord_regex = /([A-Z](?:#|b)?)(.*)/
|
81
81
|
_, root_name, quality_name = chord_name.match(chord_regex).to_a
|
82
82
|
root = Note[root_name]
|
83
83
|
quality = ChordQuality.new(name: quality_name, bass: bass)
|
84
84
|
notes = quality.notes_for(root)
|
85
|
-
notes
|
85
|
+
notes << Note[bass] unless bass.nil?
|
86
86
|
[root, quality, notes]
|
87
87
|
end
|
88
88
|
end
|
@@ -9,7 +9,7 @@ module Coltrane
|
|
9
9
|
|
10
10
|
def self.chord_trie
|
11
11
|
trie = YAML.load_file(
|
12
|
-
File.expand_path("#{'../'*3}data/qualities.yml", __FILE__)
|
12
|
+
File.expand_path("#{'../' * 3}data/qualities.yml", __FILE__)
|
13
13
|
)
|
14
14
|
|
15
15
|
trie.clone_values from_keys: ['Perfect Unison', 'Major Third'],
|
@@ -26,7 +26,7 @@ module Coltrane
|
|
26
26
|
hash ||= chord_trie
|
27
27
|
return quality_names if hash.empty?
|
28
28
|
if hash['name']
|
29
|
-
quality_names
|
29
|
+
quality_names[hash.delete('name')] = intervals.map { |n| IntervalClass.new(n) }
|
30
30
|
end
|
31
31
|
hash.reduce(quality_names) do |memo, (interval, values)|
|
32
32
|
memo.merge intervals_per_name(hash: values,
|
@@ -65,7 +65,7 @@ module Coltrane
|
|
65
65
|
ints = IntervalSequence.new(intervals: self)
|
66
66
|
chord_sequence.map do |int_sym|
|
67
67
|
next unless interval_name = ints.public_send(int_sym)
|
68
|
-
ints.
|
68
|
+
ints.delete_if { |i| i.cents == IntervalClass.new(interval_name).cents }
|
69
69
|
interval_name
|
70
70
|
end
|
71
71
|
end
|
@@ -73,31 +73,25 @@ module Coltrane
|
|
73
73
|
public
|
74
74
|
|
75
75
|
def get_name
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
elsif result = find_chord([*retrieve_chord_intervals(sus4_sequence)].compact)
|
81
|
-
return result
|
82
|
-
else
|
83
|
-
raise ChordNotFoundError
|
84
|
-
end
|
76
|
+
find_chord(retrieve_chord_intervals.compact) ||
|
77
|
+
find_chord(retrieve_chord_intervals(sus2_sequence).compact) ||
|
78
|
+
find_chord(retrieve_chord_intervals(sus4_sequence).compact) ||
|
79
|
+
raise(ChordNotFoundError)
|
85
80
|
end
|
86
81
|
|
87
|
-
|
88
82
|
def suspension_type
|
89
83
|
if has_major_second?
|
90
84
|
'sus2'
|
91
85
|
else has_fourth?
|
92
|
-
|
86
|
+
'sus4'
|
93
87
|
end
|
94
88
|
end
|
95
89
|
|
96
90
|
def initialize(name: nil, notes: nil, bass: nil)
|
97
|
-
if
|
91
|
+
if name
|
98
92
|
@name = bass.nil? ? name : [name, bass].join('/')
|
99
|
-
super(intervals:
|
100
|
-
elsif
|
93
|
+
super(intervals: intervals_from_name(name))
|
94
|
+
elsif notes
|
101
95
|
super(notes: notes)
|
102
96
|
@name = get_name
|
103
97
|
else
|
@@ -106,5 +100,11 @@ module Coltrane
|
|
106
100
|
end
|
107
101
|
|
108
102
|
alias to_s name
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def intervals_from_name(name)
|
107
|
+
NAMES[name] || NAMES["M#{name}"] || raise(ChordNotFoundError)
|
108
|
+
end
|
109
109
|
end
|
110
110
|
end
|
@@ -14,7 +14,7 @@ module Coltrane
|
|
14
14
|
'Blues Minor' => [3, 2, 1, 1, 3, 2],
|
15
15
|
'Whole Tone' => [2, 2, 2, 2, 2, 2],
|
16
16
|
'Flamenco' => [1, 3, 1, 2, 1, 2, 2],
|
17
|
-
'Chromatic' => [1]*12
|
17
|
+
'Chromatic' => [1] * 12
|
18
18
|
}.freeze
|
19
19
|
|
20
20
|
MODES = {
|
@@ -49,7 +49,7 @@ module Coltrane
|
|
49
49
|
|
50
50
|
# All but the chromatic
|
51
51
|
def standard_scales
|
52
|
-
SCALES.reject { |k,
|
52
|
+
SCALES.reject { |k, _v| k == 'Chromatic' }
|
53
53
|
end
|
54
54
|
|
55
55
|
def fetch(name, tone = nil)
|
@@ -75,17 +75,17 @@ module Coltrane
|
|
75
75
|
|
76
76
|
def having_notes(notes)
|
77
77
|
format = { scales: [], results: {} }
|
78
|
-
OpenStruct.new
|
78
|
+
OpenStruct.new begin
|
79
79
|
standard_scales.each_with_object(format) do |(name, intervals), output|
|
80
|
-
|
80
|
+
PitchClass.all.each.map do |tone|
|
81
81
|
scale = new(*intervals, tone: tone, name: scale)
|
82
82
|
output[:results][name] ||= {}
|
83
|
-
next if output[:results][name].key?(tone.
|
83
|
+
next if output[:results][name].key?(tone.integer)
|
84
84
|
output[:scales] << scale if scale.include?(notes)
|
85
|
-
output[:results][name][tone.
|
85
|
+
output[:results][name][tone.integer] = scale.notes & notes
|
86
86
|
end
|
87
87
|
end
|
88
|
-
|
88
|
+
end
|
89
89
|
end
|
90
90
|
|
91
91
|
def having_chords(*chords)
|
data/lib/coltrane/errors.rb
CHANGED
@@ -21,6 +21,12 @@ module Coltrane
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
class WrongArgumentsError < BadConstructorError
|
25
|
+
def initialize(_msg)
|
26
|
+
super 'Wrong argument(s).'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
24
30
|
class InvalidNoteError < BadConstructorError
|
25
31
|
def initialize(note)
|
26
32
|
super "#{note} is not a valid note"
|
@@ -35,7 +41,7 @@ module Coltrane
|
|
35
41
|
|
36
42
|
class HasNoNotesError < BadConstructorError
|
37
43
|
def initialize
|
38
|
-
super
|
44
|
+
super 'The given object does not respond to :notes, '\
|
39
45
|
"thereby it can't be used for this operation)"
|
40
46
|
end
|
41
47
|
end
|
@@ -65,6 +71,25 @@ module Coltrane
|
|
65
71
|
'https://github.com/pedrozath/coltrane/issues '\
|
66
72
|
end
|
67
73
|
end
|
74
|
+
|
75
|
+
class InvalidPitchClassError < ColtraneError
|
76
|
+
def initialize(arg)
|
77
|
+
super "The given frequency(#{arg}) is not considered "\
|
78
|
+
'part of a pitch class'\
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class InvalidNoteSymbolError < ColtraneError
|
83
|
+
def initialize(arg)
|
84
|
+
super "The musical notation included an unrecognizable symbol (#{arg})."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class InvalidNoteLetterError < ColtraneError
|
89
|
+
def initialize(arg)
|
90
|
+
super "The musical notation included an unrecognizable letter (#{arg})."
|
91
|
+
end
|
92
|
+
end
|
68
93
|
end
|
69
94
|
|
70
95
|
# rubocop:enable Style/Documentation
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
class Frequency
|
5
|
+
attr_reader :frequency
|
6
|
+
|
7
|
+
def initialize(frequency)
|
8
|
+
@frequency = frequency.to_f
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
alias [] new
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{frequency}hz"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_f
|
20
|
+
frequency
|
21
|
+
end
|
22
|
+
|
23
|
+
def octave(n)
|
24
|
+
frequency * 2**n
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
frequency == (other.is_a?(Frequency) ? other.frequency : other)
|
29
|
+
end
|
30
|
+
|
31
|
+
def octave_up(n = 1)
|
32
|
+
octave(n)
|
33
|
+
end
|
34
|
+
|
35
|
+
def octave_down(n = 1)
|
36
|
+
octave(-n)
|
37
|
+
end
|
38
|
+
|
39
|
+
def /(other)
|
40
|
+
case other
|
41
|
+
when Frequency then Interval[1200 * Math.log2(frequency / other.frequency)]
|
42
|
+
when Numeric then Frequency[frequency / other]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(method, *args)
|
47
|
+
Frequency[frequency.send(method, args[0].to_f)]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/coltrane/interval.rb
CHANGED
@@ -1,113 +1,50 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Coltrane
|
4
|
-
#
|
4
|
+
# Interval describe the logarithmic distance between 2 frequencies.
|
5
|
+
# It's measured in cents.
|
5
6
|
class Interval
|
6
|
-
|
7
|
-
attr_reader :semitones
|
7
|
+
attr_reader :cents
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
m2
|
12
|
-
M2
|
13
|
-
m3
|
14
|
-
M3
|
15
|
-
P4
|
16
|
-
A4
|
17
|
-
P5
|
18
|
-
m6
|
19
|
-
M6
|
20
|
-
m7
|
21
|
-
M7
|
22
|
-
].freeze
|
23
|
-
|
24
|
-
def self.split(interval)
|
25
|
-
interval.scan(/(\w)(\d\d?)/)[0]
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.full_name(interval)
|
29
|
-
q,n = split(interval)
|
30
|
-
"#{q.interval_quality} #{n.to_i.interval_name}"
|
31
|
-
end
|
32
|
-
|
33
|
-
# Create full names and methods such as major_third? minor_seventh?
|
34
|
-
# TODO: It's a mess and it really needs a refactor one day
|
35
|
-
NAMES = INTERVALS.each_with_index.reduce({}) do |memo, (interval, index)|
|
36
|
-
memo[interval] ||= []
|
37
|
-
2.times do |o|
|
38
|
-
q,i = split(interval)
|
39
|
-
num = o * 7 + i.to_i
|
40
|
-
prev_q = split(INTERVALS[(index - 1) % 12])[0]
|
41
|
-
next_q = split(INTERVALS[(index + 1) % 12])[0]
|
42
|
-
memo[interval] << full_name("#{q}#{num}")
|
43
|
-
memo[interval] << full_name("d#{(num - 1 + 1) % 14 + 1}") if next_q.match? /m|P/
|
44
|
-
next if q == 'A'
|
45
|
-
memo[interval] << full_name("A#{(num - 1 - 1) % 14 + 1}") if prev_q.match? /M|P/
|
46
|
-
end
|
47
|
-
memo
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.[](arg)
|
51
|
-
new(case arg
|
52
|
-
when Interval then arg.semitones
|
53
|
-
when String then INTERVALS.index(arg) || interval_by_full_name(arg)
|
54
|
-
when Numeric then arg
|
55
|
-
end % 12)
|
9
|
+
class << self
|
10
|
+
alias [] new
|
56
11
|
end
|
57
12
|
|
58
|
-
|
59
|
-
|
60
|
-
NAMES.each do |interval_name, full_names|
|
61
|
-
full_names.each do |the_full_name|
|
62
|
-
define_method "#{the_full_name.underscore}?" do
|
63
|
-
name == interval_name
|
64
|
-
end
|
65
|
-
self.class.define_method "#{the_full_name.underscore}" do
|
66
|
-
self[interval_name]
|
67
|
-
end
|
68
|
-
end
|
13
|
+
def initialize(cents)
|
14
|
+
@cents = cents.round
|
69
15
|
end
|
70
16
|
|
71
|
-
def
|
72
|
-
|
17
|
+
def semitones
|
18
|
+
(cents.to_f / 100).round
|
73
19
|
end
|
74
20
|
|
75
|
-
|
76
|
-
|
77
|
-
def all_full_names
|
78
|
-
ALL_FULL_NAMES
|
21
|
+
def ascending?
|
22
|
+
cents < 0
|
79
23
|
end
|
80
24
|
|
81
|
-
|
82
|
-
|
83
|
-
def name
|
84
|
-
INTERVALS[semitones]
|
25
|
+
def descending?
|
26
|
+
cents > 0
|
85
27
|
end
|
86
28
|
|
87
|
-
def
|
88
|
-
|
29
|
+
def ==(other)
|
30
|
+
cents == other.cents
|
89
31
|
end
|
90
32
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
33
|
+
alias eql? ==
|
34
|
+
alias hash cents
|
94
35
|
|
95
36
|
def +(other)
|
96
37
|
case other
|
97
|
-
when Numeric then Interval[
|
98
|
-
when Interval then Interval[
|
38
|
+
when Numeric then Interval[cents + other]
|
39
|
+
when Interval then Interval[cents + other.cents]
|
99
40
|
end
|
100
41
|
end
|
101
42
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
if full_names.include?(arg)
|
107
|
-
return INTERVALS.index(interval_name)
|
108
|
-
end
|
43
|
+
def -(other)
|
44
|
+
case other
|
45
|
+
when Numeric then Interval[cents - other]
|
46
|
+
when Interval then Interval[cents - other.cents]
|
109
47
|
end
|
110
|
-
raise IntervalNotFoundError, arg
|
111
48
|
end
|
112
49
|
end
|
113
50
|
end
|