coltrane 1.0.1 → 1.0.2
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/.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
|