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