coltrane 1.0.2 → 1.0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -5
- data/Gemfile.lock +3 -3
- data/coltrane.gemspec +1 -1
- data/db/schema.rb +30 -0
- data/exe/coltrane +21 -27
- data/lib/cli/bass_guitar.rb +1 -4
- data/lib/cli/chord.rb +3 -10
- data/lib/cli/errors.rb +4 -10
- data/lib/cli/guitar.rb +10 -12
- data/lib/cli/notes.rb +4 -7
- data/lib/cli/piano.rb +8 -11
- data/lib/cli/representation.rb +14 -14
- data/lib/cli/scale.rb +11 -15
- data/lib/cli/text.rb +3 -6
- data/lib/cli/ukulele.rb +2 -5
- data/lib/{cli.rb → coltrane-cli.rb} +2 -4
- data/lib/coltrane.rb +1 -4
- data/lib/coltrane/cache.rb +9 -10
- data/lib/coltrane/cadence.rb +1 -3
- data/lib/coltrane/chord.rb +8 -16
- data/lib/coltrane/chord_cache.rb +4 -0
- data/lib/coltrane/chord_quality.rb +8 -7
- data/lib/coltrane/classic_progressions.rb +7 -10
- data/lib/coltrane/classic_scales.rb +43 -30
- data/lib/coltrane/errors.rb +10 -17
- data/lib/coltrane/interval.rb +23 -24
- data/lib/coltrane/interval_sequence.rb +7 -8
- data/lib/coltrane/interval_set.rb +0 -1
- data/lib/coltrane/note.rb +46 -23
- data/lib/coltrane/note_set.rb +14 -12
- data/lib/coltrane/pitch.rb +4 -5
- data/lib/coltrane/progression.rb +20 -9
- data/lib/coltrane/qualities.rb +112 -114
- data/lib/coltrane/roman_chord.rb +5 -8
- data/lib/coltrane/scale.rb +33 -25
- data/lib/coltrane/scale_chord.rb +4 -0
- data/lib/coltrane/version.rb +1 -3
- data/lib/core_ext.rb +4 -7
- metadata +10 -7
- data/.rubocop.yml +0 -19
data/lib/cli/scale.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
2
|
module Cli
|
5
|
-
# Interfaces commands with the scales functionality
|
6
3
|
class Scale
|
7
4
|
def self.parse(str)
|
8
5
|
*scale_name, tone = str.split('-')
|
@@ -15,10 +12,8 @@ module Coltrane
|
|
15
12
|
notes = NoteSet[*notes]
|
16
13
|
elsif chords.any?
|
17
14
|
puts "\nSearching for scales containing #{chords.join(', ')}:\n\n"
|
18
|
-
notes = chords.reduce(NoteSet[])
|
19
|
-
|
20
|
-
end
|
21
|
-
else raise BadFindScales
|
15
|
+
notes = chords.reduce(NoteSet[]) {|memo, c| memo + Coltrane::Chord.new(name: c).notes }
|
16
|
+
else raise BadFindScales.new
|
22
17
|
end
|
23
18
|
render_search(notes)
|
24
19
|
puts "\nUnderlined means the scale has all notes"
|
@@ -29,24 +24,25 @@ module Coltrane
|
|
29
24
|
output = []
|
30
25
|
scale_width = search.results.keys.map(&:size).max
|
31
26
|
search.results.each do |name, scales_by_tone|
|
32
|
-
output << name.ljust(scale_width
|
27
|
+
output << name.ljust(scale_width+1, ' ')
|
33
28
|
scales_by_tone.each do |tone_number, notes|
|
34
|
-
p
|
35
|
-
l
|
36
|
-
|
37
|
-
|
29
|
+
p = notes.size.to_f / searched_notes.size
|
30
|
+
l = p == 1 ? p : (p + 0.2) * 0.4
|
31
|
+
hue, val, sat = 30, val = (l * 100).round, sat = p
|
32
|
+
und = p == 1 ? :underline : nil
|
33
|
+
color = "hsv(#{hue},#{sat},#{val})".paint.to_hex
|
38
34
|
output << Paint["#{Note[tone_number].name}(#{notes.size})", color, und]
|
39
|
-
output <<
|
35
|
+
output << " "
|
40
36
|
end
|
41
37
|
output << "\n"
|
42
38
|
end
|
43
39
|
puts output.join
|
44
40
|
end
|
45
41
|
|
46
|
-
def initialize(scale, on: :text, flavor: 'degrees')
|
42
|
+
def initialize(scale, on: :text, flavor: 'degrees', notes: [], chords: [])
|
47
43
|
desc = "This is the #{scale.tone.name} #{scale.name} scale:"
|
48
44
|
Coltrane::Cli::Notes.new(scale.notes, on: on, desc: desc, flavor: flavor)
|
49
45
|
end
|
50
46
|
end
|
51
47
|
end
|
52
|
-
end
|
48
|
+
end
|
data/lib/cli/text.rb
CHANGED
@@ -1,16 +1,13 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
2
|
module Cli
|
5
|
-
# A text representation
|
6
3
|
class Text < Representation
|
7
4
|
def render
|
8
5
|
case @flavor
|
9
6
|
when :marks, :notes, :degrees then @notes.pretty_names.join(' ')
|
10
|
-
when :intervals then @notes.map {
|
11
|
-
else raise WrongFlavorError
|
7
|
+
when :intervals then @notes.map {|n| (@notes.first - n).name}.join(' ')
|
8
|
+
else raise WrongFlavorError.new
|
12
9
|
end
|
13
10
|
end
|
14
11
|
end
|
15
12
|
end
|
16
|
-
end
|
13
|
+
end
|
data/lib/cli/ukulele.rb
CHANGED
@@ -1,14 +1,11 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
2
|
module Cli
|
5
|
-
# Renders notes in a common most popular ukulele scheme
|
6
3
|
class Ukulele < Guitar
|
7
|
-
SPECIAL_FRETS = [5, 7, 9, 12]
|
4
|
+
SPECIAL_FRETS = [5, 7, 9, 12]
|
8
5
|
|
9
6
|
def initialize(notes, flavor, tuning: %w[G C E A], frets: 12)
|
10
7
|
super
|
11
8
|
end
|
12
9
|
end
|
13
10
|
end
|
14
|
-
end
|
11
|
+
end
|
@@ -1,8 +1,6 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require 'mercenary'
|
4
2
|
require 'paint'
|
5
|
-
require '
|
3
|
+
require 'chroma'
|
6
4
|
|
7
5
|
require 'cli/errors'
|
8
6
|
|
@@ -15,4 +13,4 @@ require 'cli/ukulele'
|
|
15
13
|
|
16
14
|
require 'cli/notes'
|
17
15
|
require 'cli/chord'
|
18
|
-
require 'cli/scale'
|
16
|
+
require 'cli/scale'
|
data/lib/coltrane.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require 'forwardable'
|
4
2
|
require 'facets/multiton'
|
5
|
-
require 'core_ext'
|
6
3
|
require 'ostruct'
|
7
4
|
|
8
5
|
require 'coltrane/version'
|
@@ -27,4 +24,4 @@ require 'coltrane/note'
|
|
27
24
|
require 'coltrane/pitch'
|
28
25
|
require 'coltrane/progression'
|
29
26
|
require 'coltrane/scale'
|
30
|
-
require 'coltrane/mode'
|
27
|
+
require 'coltrane/mode'
|
data/lib/coltrane/cache.rb
CHANGED
@@ -1,35 +1,34 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
|
-
# A simple caching based on serializing objects into files
|
5
|
-
# maybe this should changed to save in a single json file
|
6
2
|
class Cache
|
7
3
|
class << self
|
8
4
|
def find_or_record(key, &block)
|
9
|
-
|
5
|
+
if (cached = fetch(key))
|
6
|
+
return cached
|
7
|
+
else
|
10
8
|
cached = yield block
|
11
9
|
record(key, cached)
|
10
|
+
cached
|
12
11
|
end
|
13
|
-
cached
|
14
12
|
end
|
15
13
|
|
16
14
|
private
|
17
15
|
|
18
16
|
def dir
|
19
|
-
dir = File.expand_path('../../../', __FILE__) +
|
17
|
+
dir = File.expand_path('../../../', __FILE__) + "/cache/"
|
20
18
|
Dir.mkdir(dir) unless Dir.exist?(dir)
|
21
19
|
dir
|
22
20
|
end
|
23
21
|
|
24
22
|
def fetch(key)
|
25
|
-
|
23
|
+
return unless File.file?(dir+key)
|
24
|
+
Marshal.load File.read(dir+key)
|
26
25
|
end
|
27
26
|
|
28
27
|
def record(key, contents)
|
29
|
-
File.open(dir
|
28
|
+
File.open(dir+key, "w") do |f|
|
30
29
|
f.write Marshal.dump(contents)
|
31
30
|
end
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
35
|
-
end
|
34
|
+
end
|
data/lib/coltrane/cadence.rb
CHANGED
data/lib/coltrane/chord.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
2
|
# It describe a chord
|
5
3
|
class Chord < NoteSet
|
@@ -18,25 +16,19 @@ module Coltrane
|
|
18
16
|
elsif !name.nil?
|
19
17
|
@root_note, @quality, @notes = parse_from_name(name)
|
20
18
|
else
|
21
|
-
raise
|
22
|
-
'[notes:] || [root_note:, quality:] || [name:]'
|
19
|
+
raise WrongKeywords.new('[notes:] || [root_note:, quality:] || [name:]')
|
23
20
|
end
|
24
21
|
end
|
25
22
|
|
26
23
|
def name
|
27
|
-
return @notes.names.join('/')
|
24
|
+
return @notes.names.join('/') if !named?
|
28
25
|
"#{root_note.name}#{quality.name}"
|
29
26
|
end
|
30
27
|
|
31
|
-
def pretty_name
|
32
|
-
return @notes.names.join('/') unless named?
|
33
|
-
"#{root_note.pretty_name}#{quality.name}"
|
34
|
-
end
|
35
|
-
|
36
28
|
def named?
|
37
29
|
notes.size >= 3 &&
|
38
|
-
|
39
|
-
|
30
|
+
!root_note.nil? &&
|
31
|
+
!quality&.name.nil?
|
40
32
|
end
|
41
33
|
|
42
34
|
def intervals
|
@@ -48,14 +40,14 @@ module Coltrane
|
|
48
40
|
end
|
49
41
|
|
50
42
|
def scales
|
51
|
-
Scale.having_chord(name)
|
43
|
+
Scale.having_chord(self.name)
|
52
44
|
end
|
53
45
|
|
54
46
|
def next_inversion
|
55
47
|
Chord.new(notes.rotate(1))
|
56
48
|
end
|
57
49
|
|
58
|
-
def invert(n
|
50
|
+
def invert(n=1)
|
59
51
|
Chord.new(notes.rotate(n))
|
60
52
|
end
|
61
53
|
|
@@ -66,11 +58,11 @@ module Coltrane
|
|
66
58
|
protected
|
67
59
|
|
68
60
|
def parse_from_name(name)
|
69
|
-
_, name, quality_name = name.match(/([A-Z]
|
61
|
+
_, name, quality_name = name.match(/([A-Z]#?)(.*)/).to_a
|
70
62
|
root = Note[name]
|
71
63
|
quality = ChordQuality.new(name: quality_name)
|
72
64
|
notes = quality.notes_for(root)
|
73
65
|
[root, quality, notes]
|
74
66
|
end
|
75
67
|
end
|
76
|
-
end
|
68
|
+
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
2
|
# It describe the quality of a chord, like maj7 or dim.
|
5
3
|
class ChordQuality < IntervalSequence
|
@@ -8,15 +6,18 @@ module Coltrane
|
|
8
6
|
|
9
7
|
def initialize(name: nil, notes: nil)
|
10
8
|
if !name.nil?
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
if(intervals = CHORD_QUALITIES[name])
|
10
|
+
@name = name
|
11
|
+
super(intervals: intervals)
|
12
|
+
else
|
13
|
+
raise ChordNotFoundError.new
|
14
|
+
end
|
14
15
|
elsif !notes.nil?
|
15
16
|
super(notes: notes)
|
16
17
|
@name = CHORD_QUALITIES.key(intervals_semitones)
|
17
18
|
else
|
18
|
-
raise
|
19
|
+
raise WrongKeywords.new('[name:] || [notes:]')
|
19
20
|
end
|
20
21
|
end
|
21
22
|
end
|
22
|
-
end
|
23
|
+
end
|
@@ -1,16 +1,13 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
|
-
# It's totally a wip yet.
|
5
2
|
module ClassicProgressions
|
6
3
|
PROGRESSIONS = {
|
7
|
-
pop: [:major, [1,
|
8
|
-
fifties: [:major, [1,
|
9
|
-
blues: [:major, [1,
|
10
|
-
jazz: [:major, [2,
|
11
|
-
jazz_minor: [:minor, [2,
|
12
|
-
andalusian: [:minor, [1,
|
13
|
-
}
|
4
|
+
pop: [:major, [1,5,6,4]],
|
5
|
+
fifties: [:major, [1,6,4,5]],
|
6
|
+
blues: [:major, [1,4,1,5,4,1]],
|
7
|
+
jazz: [:major, [2,5,1]],
|
8
|
+
jazz_minor: [:minor, [2,5,1]],
|
9
|
+
andalusian: [:minor, [1,7,6,5]]
|
10
|
+
}
|
14
11
|
|
15
12
|
def pop(tone)
|
16
13
|
scale, degrees = PROGRESSIONS[:pop]
|
@@ -1,28 +1,36 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Coltrane
|
4
|
-
# This module deals with well known scales on music
|
5
2
|
module ClassicScales
|
3
|
+
|
6
4
|
SCALES = {
|
7
|
-
'Major' => [2,
|
8
|
-
'Pentatonic Major' => [2,
|
9
|
-
'Blues Major' => [2,
|
10
|
-
'Natural Minor' => [2,
|
11
|
-
'Harmonic Minor' => [2,
|
12
|
-
'Hungarian Minor' => [2,
|
13
|
-
'Pentatonic Minor' => [3,
|
14
|
-
'Blues Minor' => [3,
|
15
|
-
'Whole Tone' => [2,
|
16
|
-
'Flamenco' => [1,
|
17
|
-
}
|
5
|
+
'Major' => [2,2,1,2,2,2,1],
|
6
|
+
'Pentatonic Major' => [2,2,3,2,3],
|
7
|
+
'Blues Major' => [2,1,1,3,2,3],
|
8
|
+
'Natural Minor' => [2,1,2,2,1,2,2],
|
9
|
+
'Harmonic Minor' => [2,1,2,2,1,3,1],
|
10
|
+
'Hungarian Minor' => [2,1,2,1,1,3,1],
|
11
|
+
'Pentatonic Minor' => [3,2,2,3,2],
|
12
|
+
'Blues Minor' => [3,2,1,1,3,2],
|
13
|
+
'Whole Tone' => [2,2,2,2,2,2],
|
14
|
+
'Flamenco' => [1,3,1,2,1,2,2]
|
15
|
+
}
|
18
16
|
|
19
17
|
MODES = {
|
20
18
|
'Major' => %w[Ionian Dorian Phrygian Lydian Mixolydian Aeolian Locrian]
|
21
|
-
}
|
19
|
+
}
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# A little helper to build method names
|
24
|
+
# just make the code more clear
|
25
|
+
def self.methodize(string)
|
26
|
+
string.downcase.gsub(' ', '_')
|
27
|
+
end
|
28
|
+
|
29
|
+
public
|
22
30
|
|
23
31
|
# Creates factories for scales
|
24
32
|
SCALES.each do |name, distances|
|
25
|
-
define_method name
|
33
|
+
define_method methodize(name) do |tone='C', mode=1|
|
26
34
|
new(*distances, tone: tone, mode: mode, name: name)
|
27
35
|
end
|
28
36
|
end
|
@@ -30,33 +38,34 @@ module Coltrane
|
|
30
38
|
# Creates factories for Greek Modes and possibly others
|
31
39
|
MODES.each do |scale, modes|
|
32
40
|
modes.each_with_index do |mode, index|
|
41
|
+
scale_method = methodize(scale)
|
33
42
|
mode_name = mode
|
34
43
|
mode_n = index + 1
|
35
|
-
define_method mode
|
44
|
+
define_method methodize(mode) do |tone='C'|
|
36
45
|
new(*SCALES[scale], tone: tone, mode: mode_n, name: mode_name)
|
37
46
|
end
|
38
47
|
end
|
39
48
|
end
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
50
|
+
alias_method :minor, :natural_minor
|
51
|
+
alias_method :pentatonic, :pentatonic_major
|
52
|
+
alias_method :blues, :blues_major
|
44
53
|
|
45
54
|
def known_scales
|
46
55
|
SCALES.keys
|
47
56
|
end
|
48
57
|
|
49
|
-
def fetch(name, tone
|
58
|
+
def fetch(name, tone=nil)
|
50
59
|
Coltrane::Scale.public_send(name, tone)
|
51
60
|
end
|
52
61
|
|
53
62
|
def from_key(key)
|
54
|
-
|
55
|
-
|
56
|
-
scale
|
57
|
-
Scale.public_send(scale, note)
|
63
|
+
scale = key.delete!('m') ? :minor : :major
|
64
|
+
note = key
|
65
|
+
Scale.public_send(scale.nil? || scale == 'M' ? :major : :minor, note)
|
58
66
|
end
|
59
67
|
|
68
|
+
|
60
69
|
# Will output a OpenStruct like the following:
|
61
70
|
# {
|
62
71
|
# scales: [array of scales]
|
@@ -68,15 +77,19 @@ module Coltrane
|
|
68
77
|
# }
|
69
78
|
|
70
79
|
def having_notes(notes)
|
80
|
+
|
71
81
|
format = { scales: [], results: {} }
|
72
82
|
OpenStruct.new(
|
73
83
|
SCALES.each_with_object(format) do |(name, intervals), output|
|
74
84
|
Note.all.each.map do |tone|
|
75
85
|
scale = new(*intervals, tone: tone, name: scale)
|
76
86
|
output[:results][name] ||= {}
|
77
|
-
|
78
|
-
|
79
|
-
|
87
|
+
if output[:results][name].has_key?(tone.number)
|
88
|
+
next
|
89
|
+
else
|
90
|
+
output[:scales] << scale if scale.include?(notes)
|
91
|
+
output[:results][name][tone.number] = scale.notes & notes
|
92
|
+
end
|
80
93
|
end
|
81
94
|
end
|
82
95
|
)
|
@@ -90,6 +103,6 @@ module Coltrane
|
|
90
103
|
having_notes(notes)
|
91
104
|
end
|
92
105
|
|
93
|
-
|
106
|
+
alias_method :having_chord, :having_chords
|
94
107
|
end
|
95
|
-
end
|
108
|
+
end
|
data/lib/coltrane/errors.rb
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# rubocop:disable Style/Documentation
|
4
|
-
|
5
1
|
module Coltrane
|
6
2
|
class ColtraneError < StandardError
|
7
3
|
def initialize(msg)
|
@@ -9,41 +5,40 @@ module Coltrane
|
|
9
5
|
end
|
10
6
|
end
|
11
7
|
|
12
|
-
class
|
13
|
-
def initialize(msg
|
8
|
+
class BadConstructor < ColtraneError
|
9
|
+
def initialize(msg=nil)
|
14
10
|
super "Bad constructor. #{msg}"
|
15
11
|
end
|
16
12
|
end
|
17
13
|
|
18
|
-
class
|
14
|
+
class WrongKeywords < BadConstructor
|
19
15
|
def initialize(msg)
|
20
16
|
super "Use one of the following set of keywords: #{msg}"
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
24
|
-
class
|
20
|
+
class InvalidNote < BadConstructor
|
25
21
|
def initialize(note)
|
26
22
|
super "#{note} is not a valid note"
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
30
|
-
class
|
26
|
+
class InvalidNotes < BadConstructor
|
31
27
|
def initialize(notes)
|
32
28
|
super "#{notes} are not a valid set of notes"
|
33
29
|
end
|
34
30
|
end
|
35
31
|
|
36
|
-
class
|
32
|
+
class HasNoNotes < BadConstructor
|
37
33
|
def initialize(obj)
|
38
34
|
super "The given object (#{obj.inspect} does not respond to :notes, "\
|
39
35
|
"thereby it can't be used for this operation)"
|
40
36
|
end
|
41
37
|
end
|
42
38
|
|
43
|
-
class
|
39
|
+
class WrongDegree
|
44
40
|
def initialize(degree)
|
45
|
-
super "#{degree} is not a valid degree. Degrees for this scale must be"
|
46
|
-
"between 1 and #{degrees}"
|
41
|
+
super "#{degree} is not a valid degree. Degrees for this scale must be between 1 and #{degrees}"
|
47
42
|
end
|
48
43
|
end
|
49
44
|
|
@@ -52,10 +47,8 @@ module Coltrane
|
|
52
47
|
super "The chord you provided wasn't found. "\
|
53
48
|
"If you're sure this chord exists, "\
|
54
49
|
"would you mind to suggest it's inclusion here: "\
|
55
|
-
|
50
|
+
"https://github.com/pedrozath/coltrane/issues "\
|
56
51
|
"\n\nA tip tho: always include the letter M for major"
|
57
52
|
end
|
58
53
|
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# rubocop:enable Style/Documentation
|
54
|
+
end
|