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