coltrane 0.0.2 → 1.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +15 -10
  5. data/Gemfile.lock +47 -128
  6. data/README.md +30 -8
  7. data/Rakefile +0 -4
  8. data/bin/bundle +105 -0
  9. data/bin/byebug +29 -0
  10. data/bin/coderay +12 -0
  11. data/bin/coltrane +25 -7
  12. data/bin/htmldiff +12 -0
  13. data/bin/kill-pry-rescue +12 -0
  14. data/bin/ldiff +12 -0
  15. data/bin/pry +12 -0
  16. data/bin/rake +12 -0
  17. data/bin/rescue +12 -0
  18. data/bin/rspec +12 -0
  19. data/bin/rubocop +12 -0
  20. data/bin/ruby-parse +12 -0
  21. data/bin/ruby-rewrite +12 -0
  22. data/exe/coltrane +173 -0
  23. data/lib/cli/bass_guitar.rb +9 -0
  24. data/lib/cli/chord.rb +24 -0
  25. data/lib/cli/errors.rb +28 -0
  26. data/lib/cli/guitar.rb +55 -0
  27. data/lib/cli/notes.rb +18 -0
  28. data/lib/cli/piano.rb +54 -0
  29. data/lib/cli/representation.rb +47 -0
  30. data/lib/cli/scale.rb +48 -0
  31. data/lib/cli/text.rb +13 -0
  32. data/lib/cli/ukulele.rb +11 -0
  33. data/lib/coltrane-cli.rb +12 -0
  34. data/lib/coltrane.rb +16 -31
  35. data/lib/coltrane/cache.rb +34 -0
  36. data/lib/coltrane/chord.rb +28 -41
  37. data/lib/coltrane/chord_quality.rb +14 -39
  38. data/lib/coltrane/classic_progressions.rb +37 -0
  39. data/lib/coltrane/classic_scales.rb +92 -118
  40. data/lib/coltrane/errors.rb +54 -0
  41. data/lib/coltrane/interval.rb +25 -17
  42. data/lib/coltrane/interval_sequence.rb +57 -27
  43. data/lib/coltrane/note.rb +44 -30
  44. data/lib/coltrane/note_set.rb +37 -22
  45. data/lib/coltrane/piano_representation.rb +0 -58
  46. data/lib/coltrane/pitch.rb +12 -3
  47. data/lib/coltrane/progression.rb +31 -0
  48. data/lib/coltrane/qualities.rb +115 -114
  49. data/lib/coltrane/roman_chord.rb +36 -0
  50. data/lib/coltrane/scale.rb +85 -77
  51. data/lib/coltrane/version.rb +1 -1
  52. data/lib/core_ext.rb +12 -0
  53. data/pkg/coltrane-0.0.2.gem +0 -0
  54. metadata +27 -14
  55. data/lib/coltrane/essential_guitar_chords.rb +0 -82
  56. data/lib/coltrane/fret_set.rb +0 -0
  57. data/lib/coltrane/guitar.rb +0 -15
  58. data/lib/coltrane/guitar_chord.rb +0 -50
  59. data/lib/coltrane/guitar_chord_finder.rb +0 -98
  60. data/lib/coltrane/guitar_note.rb +0 -50
  61. data/lib/coltrane/guitar_note_set.rb +0 -61
  62. data/lib/coltrane/guitar_representation.rb +0 -96
  63. data/lib/coltrane/guitar_string.rb +0 -52
  64. data/lib/coltrane/scale_cache.rb +0 -4
@@ -1,48 +1,23 @@
1
-
2
-
3
1
  module Coltrane
4
2
  # It describe the quality of a chord, like maj7 or dim.
5
- class ChordQuality
3
+ class ChordQuality < IntervalSequence
6
4
  attr_reader :name
7
5
  include Qualities
8
6
 
9
- def initialize(interval_sequence)
10
- if interval_sequence.class != IntervalSequence
11
- interval_sequence = IntervalSequence.new(interval_sequence)
7
+ def initialize(name: nil, notes: nil)
8
+ if !name.nil?
9
+ if(intervals = CHORD_QUALITIES[name])
10
+ @name = name
11
+ super(intervals: intervals)
12
+ else
13
+ raise ChordNotFoundError.new
14
+ end
15
+ elsif !notes.nil?
16
+ super(notes: notes)
17
+ @name = CHORD_QUALITIES.key(intervals_semitones)
18
+ else
19
+ raise WrongKeywords.new('[name:] || [notes:]')
12
20
  end
13
- @name = find_chord_name(interval_sequence)
14
- end
15
-
16
- def self.new_from_notes(notes)
17
- note_set = NoteSet.new(notes) unless notes.class == NoteSet
18
- new(note_set.interval_sequence.reordered)
19
- end
20
-
21
- def self.new_from_pitches(*pitches)
22
- notes = pitches.sort_by(&:number)
23
- .collect(&:note)
24
- .collect(&:name)
25
- .uniq
26
-
27
- new_from_notes(notes)
28
- end
29
-
30
- def self.new_from_string(quality_string)
31
- new(CHORD_QUALITIES[quality_string])
32
- end
33
-
34
- def intervals
35
- CHORD_QUALITIES[name]
36
- end
37
-
38
- protected
39
-
40
- def find_chord_name(note_intervals, inversion = 0)
41
- inversion >= note_intervals.all.size && return
42
- CHORD_QUALITIES.key(note_intervals.numbers) ||
43
- CHORD_QUALITIES.key(note_intervals.numbers.sort) ||
44
- find_chord_name(note_intervals.next_inversion, inversion + 1) ||
45
- nil
46
21
  end
47
22
  end
48
23
  end
@@ -0,0 +1,37 @@
1
+ module Coltrane
2
+ module ClassicProgressions
3
+ 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
+ }
11
+
12
+ def pop(tone)
13
+ scale, degrees = PROGRESSIONS[:pop]
14
+ Scale.public_send(scale, tone).progression(*degrees)
15
+ end
16
+
17
+ def fifties(tone)
18
+ scale, degrees = PROGRESSIONS[:fifties]
19
+ Scale.public_send(scale, tone).progression(*degrees)
20
+ end
21
+
22
+ def blues(tone)
23
+ scale, degrees = PROGRESSIONS[:blues]
24
+ Scale.public_send(scale, tone).progression(*degrees)
25
+ end
26
+
27
+ def jazz(tone)
28
+ scale, degrees = PROGRESSIONS[:jazz]
29
+ Scale.public_send(scale, tone).progression(*degrees)
30
+ end
31
+
32
+ def andalusian(tone)
33
+ scale, degrees = PROGRESSIONS[:andalusian]
34
+ Scale.public_send(scale, tone).progression(*degrees)
35
+ end
36
+ end
37
+ end
@@ -1,134 +1,108 @@
1
- require 'ostruct'
2
-
3
- module ClassicScales
4
-
5
- SCALES = {
6
- 'Major' => [2,2,1,2,2,2,1],
7
- 'Natural Minor' => [2,1,2,2,1,2,2],
8
- 'Harmonic Minor' => [2,1,2,2,1,3,1],
9
- 'Hungarian Minor' => [2,1,2,1,1,3,1],
10
- 'Pentatonic Major' => [2,2,3,2,3],
11
- 'Pentatonic Minor' => [3,2,2,3,2],
12
- 'Blues Major' => [2,1,1,3,2,3],
13
- 'Blues Minor' => [3,2,1,1,3,2],
14
- 'Whole Tone' => [2,2,2,2,2,2]
15
- }
16
-
17
- def major(tone='C', mode=1)
18
- new(*SCALES['Major'], tone: tone, mode: mode)
19
- end
20
-
21
- def natural_minor(tone='C', mode=1)
22
- new(*SCALES['Natural Minor'], tone: tone, mode: mode)
23
- end
24
-
25
- def harmonic_minor(tone='C', mode=1)
26
- new(*SCALES['Harmonic Minor'], tone: tone, mode: mode)
27
- end
28
-
29
- def hungarian_minor(tone='C', mode=1)
30
- new(*SCALES['Hungarian Minor'], tone: tone, mode: mode)
31
- end
32
-
33
- def pentatonic_major(tone='C', mode=1)
34
- new(*SCALES['Pentatonic Major'], tone: tone, mode: mode)
35
- end
36
-
37
- def pentatonic_minor(tone='C', mode=1)
38
- new(*SCALES['Pentatonic Minor'], tone: tone, mode: mode)
39
- end
40
-
41
- def blues_major(tone='C', mode=1)
42
- new(*SCALES['Blues Major'], tone: tone, mode: mode)
43
- end
44
-
45
- def blues_minor(tone='C', mode=1)
46
- new(*SCALES['Blues Minor'], tone: tone, mode: mode)
47
- end
48
-
49
- def whole_tone(tone='C', mode=1)
50
- new(*SCALES['Blues Minor'], tone: tone, mode: mode)
51
- end
1
+ module Coltrane
2
+ module ClassicScales
3
+
4
+ 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
+ }
16
+
17
+ MODES = {
18
+ '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
52
28
 
53
- def chromatic(tone='C', mode=1)
54
- new(*([1]*12), tone: tone, mode: mode)
55
- end
29
+ public
56
30
 
57
- def ionian(tone='C')
58
- new(*SCALES['Major'], tone: tone, mode: 1)
59
- end
31
+ # Creates factories for scales
32
+ SCALES.each do |name, distances|
33
+ define_method methodize(name) do |tone='C', mode=1|
34
+ new(*distances, tone: tone, mode: mode, name: name)
35
+ end
36
+ end
60
37
 
61
- def dorian(tone='C')
62
- new(*SCALES['Major'], tone: tone, mode: 2)
63
- end
38
+ # Creates factories for Greek Modes and possibly others
39
+ MODES.each do |scale, modes|
40
+ modes.each_with_index do |mode, index|
41
+ scale_method = methodize(scale)
42
+ mode_name = mode
43
+ mode_n = index + 1
44
+ define_method methodize(mode) do |tone='C'|
45
+ new(*SCALES[scale], tone: tone, mode: mode_n, name: mode_name)
46
+ end
47
+ end
48
+ end
64
49
 
65
- def phrygian(tone='C')
66
- new(*SCALES['Major'], tone: tone, mode: 3)
67
- end
50
+ alias_method :minor, :natural_minor
51
+ alias_method :pentatonic, :pentatonic_major
52
+ alias_method :blues, :blues_major
68
53
 
69
- def lydian(tone='C')
70
- new(*SCALES['Major'], tone: tone, mode: 4)
71
- end
54
+ def known_scales
55
+ SCALES.keys
56
+ end
72
57
 
73
- def mixolydian(tone='C')
74
- new(*SCALES['Major'], tone: tone, mode: 5)
75
- end
58
+ def fetch(name, tone=nil)
59
+ Coltrane::Scale.public_send(name, tone)
60
+ end
76
61
 
77
- def aeolian(tone='C')
78
- new(*SCALES['Major'], tone: tone, mode: 6)
79
- end
62
+ def from_key(key)
63
+ scale = key.delete!('m') ? :minor : :major
64
+ note = key
65
+ Scale.public_send(scale.nil? || scale == 'M' ? :major : :minor, note)
66
+ end
80
67
 
81
- def locrian(tone='C')
82
- new(*SCALES['Major'], tone: tone, mode: 7)
83
- end
84
68
 
85
- def having_chord(chord)
86
- puts "\nSearching #{chord} in scales:\n\n"
87
- scale_name_size = SCALES.keys.map(&:size).max
88
- chord = Chord.new(chord)
89
- SCALES.each_with_object([]) do |scale_obj, results|
90
- scale_name, intervals = scale_obj
91
- print Paint[scale_name.ljust(scale_name_size), 'yellow']
92
- Note.all.each do |note|
93
- found_sth = false
94
- print ' '
95
- scale = Scale.new(*intervals, tone: note.name)
96
- degree = scale.degree_of_chord(chord)
97
- unless degree.nil?
98
- found_sth = true
99
- results << OpenStruct.new({ name: "#{note.name} #{scale_name}",
100
- degree: degree,
101
- scale: scale})
69
+ # Will output a OpenStruct like the following:
70
+ # {
71
+ # scales: [array of scales]
72
+ # results: {
73
+ # scale_name: {
74
+ # note_number => found_notes
75
+ # }
76
+ # }
77
+ # }
78
+
79
+ def having_notes(notes)
80
+
81
+ format = { scales: [], results: {} }
82
+ OpenStruct.new(
83
+ SCALES.each_with_object(format) do |(name, intervals), output|
84
+ Note.all.each.map do |tone|
85
+ scale = new(*intervals, tone: tone, name: scale)
86
+ output[:results][name] ||= {}
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
93
+ end
102
94
  end
103
- print Paint[note.name, found_sth ? 'yellow' : 'black']
104
- end
105
- print "\n"
95
+ )
106
96
  end
107
- end
108
97
 
109
- def having_notes(*notes)
110
- puts "\nSearching #{notes.join(', ')} in scales:\n\n"
111
- scale_name_size = SCALES.keys.map(&:size).max
112
- notes = notes.map { |n| Note.new(n) }
113
- SCALES.each_with_object([]) do |scale_obj, results|
114
- scale_name, intervals = scale_obj
115
- print Paint[scale_name.ljust(scale_name_size), 'yellow']
116
- Note.all.each do |note|
117
- found_sth = false
118
- print ' '
119
- scale = Scale.new(*intervals, tone: note.name)
120
- notes_included = scale.include_notes?(*notes)
121
- if notes_included == notes
122
- found_sth = true
123
- results << OpenStruct.new({
124
- name: "#{note.name} #{scale_name}",
125
- notes: notes_included
126
- })
127
- end
128
- print Paint[note.name, found_sth ? 'yellow' : 'black']
129
- print Paint["(#{notes_included.count || 0})", found_sth ? 'gray' : 'black']
98
+ def having_chords(*chords)
99
+ should_create = !chords.first.is_a?(Chord)
100
+ notes = chords.reduce(NoteSet[]) do |memo, c|
101
+ memo + (should_create ? Chord.new(name: c) : c).notes
130
102
  end
131
- print "\n"
103
+ having_notes(notes)
132
104
  end
105
+
106
+ alias_method :having_chord, :having_chords
133
107
  end
134
108
  end
@@ -0,0 +1,54 @@
1
+ module Coltrane
2
+ class ColtraneError < StandardError
3
+ def initialize(msg)
4
+ super msg
5
+ end
6
+ end
7
+
8
+ class BadConstructor < ColtraneError
9
+ def initialize(msg=nil)
10
+ super "Bad constructor. #{msg}"
11
+ end
12
+ end
13
+
14
+ class WrongKeywords < BadConstructor
15
+ def initialize(msg)
16
+ super "Use one of the following set of keywords: #{msg}"
17
+ end
18
+ end
19
+
20
+ class InvalidNote < BadConstructor
21
+ def initialize(note)
22
+ super "#{note} is not a valid note"
23
+ end
24
+ end
25
+
26
+ class InvalidNotes < BadConstructor
27
+ def initialize(notes)
28
+ super "#{notes} are not a valid set of notes"
29
+ end
30
+ end
31
+
32
+ class HasNoNotes < BadConstructor
33
+ def initialize(obj)
34
+ super "The given object (#{obj.inspect} does not respond to :notes, "\
35
+ "thereby it can't be used for this operation)"
36
+ end
37
+ end
38
+
39
+ class WrongDegree
40
+ def initialize(degree)
41
+ super "#{degree} is not a valid degree. Degrees for this scale must be between 1 and #{degrees}"
42
+ end
43
+ end
44
+
45
+ class ChordNotFoundError < ColtraneError
46
+ def initialize
47
+ super "The chord you provided wasn't found. "\
48
+ "If you're sure this chord exists, "\
49
+ "would you mind to suggest it's inclusion here: "\
50
+ "https://github.com/pedrozath/coltrane/issues "\
51
+ "\n\nA tip tho: always include the letter M for major"
52
+ end
53
+ end
54
+ end
@@ -1,33 +1,41 @@
1
1
  module Coltrane
2
2
  # It describes a interval between 2 pitches
3
3
  class Interval
4
- attr_reader :number
4
+ attr_reader :semitones
5
5
 
6
6
  NAMES = [
7
7
  '1P',
8
- '2m', '2M',
9
- '3m', '3M',
10
- '4P', '4A',
8
+ '2m',
9
+ '2M',
10
+ '3m',
11
+ '3M',
12
+ '4P',
13
+ '4A',
11
14
  '5P',
12
- '6m', '6M',
15
+ '6m',
16
+ '6M',
13
17
  '7m',
14
- '7M',
15
- '8P',
16
- '9m', '9M',
17
- '10m', '10M',
18
- '11P',
19
- '12P',
20
- '13m', '13M',
21
- '14m', '14M',
22
- '15P', '15A'
18
+ '7M'
23
19
  ].freeze
24
20
 
25
- def initialize(number)
26
- @number = number
21
+ def initialize(arg)
22
+ @semitones = (case arg
23
+ when Interval then arg.semitones
24
+ when String then NAMES.index(arg)
25
+ when Numeric then arg
26
+ end) % 12
27
27
  end
28
28
 
29
29
  def name
30
- NAMES[number]
30
+ NAMES[semitones]
31
31
  end
32
+
33
+ def +(x)
34
+ case x
35
+ when Numeric then Interval.new(semitones + x)
36
+ when Interval then Interval.new(semitones + x.semitones)
37
+ end
38
+ end
39
+
32
40
  end
33
41
  end
@@ -1,30 +1,33 @@
1
1
  module Coltrane
2
2
  # It describes a sequence of intervals
3
3
  class IntervalSequence
4
+ extend Forwardable
4
5
  attr_reader :intervals
5
6
 
6
- def initialize(arg)
7
- arg = [arg] if arg.class != Array
8
- @intervals = arg.reduce([]) do |memo, arg_item|
9
- case arg_item
10
- when Numeric then memo << Interval.new(arg_item)
11
- when Interval then memo << arg_item
12
- when NoteSet then memo + intervals_from_note_set(arg_item)
13
- end
14
- end
15
- end
7
+ def_delegators :@intervals, :map, :each, :[], :size, :reduce
16
8
 
17
- def intervals_from_note_set(note_set)
18
- note_numbers = note_set.notes.collect(&:number)
19
- root = note_numbers.shift
20
- note_numbers.reduce([Interval.new(0)]) do |memo, number|
21
- number += 12 if number < root
22
- memo << Interval.new(number - root)
9
+ def initialize(notes: nil, intervals: nil, distances: nil)
10
+ if !notes.nil?
11
+ notes = NoteSet[*notes] if notes.is_a?(Array)
12
+ @intervals = intervals_from_notes(notes)
13
+ elsif !intervals.nil?
14
+ @intervals = intervals.map { |i| Interval.new(i) }
15
+ elsif !distances.nil?
16
+ @distances = distances
17
+ @intervals = intervals_from_distances(distances)
18
+ else
19
+ raise 'Provide: [notes:] || [intervals:] || [distances:]'
23
20
  end
24
21
  end
25
22
 
26
- def reordered
27
- IntervalSequence.new @intervals.sort_by!(&:number)
23
+ def distances
24
+ intervals_semitones[1..-1].each_with_index.map do |n, i|
25
+ if i == 0
26
+ n
27
+ elsif i < intervals_semitones.size
28
+ n - intervals_semitones[i]
29
+ end
30
+ end + [12 - intervals_semitones.last]
28
31
  end
29
32
 
30
33
  def all
@@ -36,35 +39,62 @@ module Coltrane
36
39
  end
37
40
 
38
41
  def shift(ammount)
39
- IntervalSequence.new(intervals.map do |i|
40
- (i.number + ammount) % 12
42
+ IntervalSequence.new(intervals: intervals.map do |i|
43
+ (i.semitones + ammount) % 12
41
44
  end)
42
45
  end
43
46
 
44
47
  def zero_it
45
- self.shift(-intervals.first.number)
48
+ self.shift(-intervals.first.semitones)
49
+ end
50
+
51
+ def inversion(index)
52
+ IntervalSequence.new(intervals: intervals.rotate(index)).zero_it
46
53
  end
47
54
 
48
55
  def next_inversion
49
- IntervalSequence.new(intervals.rotate(+1))
56
+ inversion(index+1)
50
57
  end
51
58
 
52
59
  def previous_inversion
53
- IntervalSequence.new(intervals.rotate(-1))
60
+ inversion(index-1)
54
61
  end
55
62
 
56
63
  def inversions
57
64
  Array.new(intervals.length) do |index|
58
- IntervalSequence.new(interval.rotate(index))
65
+ inversion(index)
59
66
  end
60
67
  end
61
68
 
62
- def numbers
63
- intervals.collect(&:number)
69
+ def quality
70
+ end
71
+
72
+ def intervals_semitones
73
+ map(&:semitones)
64
74
  end
65
75
 
66
76
  def names
67
- intervals.collect(&:name)
77
+ map(&:name)
78
+ end
79
+
80
+ def notes_for(root_note)
81
+ NoteSet[
82
+ *intervals.reduce([]) do |memo, interval|
83
+ memo + [root_note + interval]
84
+ end
85
+ ]
86
+ end
87
+
88
+ private
89
+
90
+ def intervals_from_distances(distances)
91
+ distances[0..-2].reduce([Interval.new(0)]) do |memo, d|
92
+ memo + [memo.last + d]
93
+ end
94
+ end
95
+
96
+ def intervals_from_notes(notes)
97
+ notes.map { |n| notes.root - n }.sort_by(&:semitones)
68
98
  end
69
99
  end
70
100
  end