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
@@ -0,0 +1,9 @@
1
+ module Coltrane
2
+ module Cli
3
+ class BassGuitar < Guitar
4
+ def initialize(notes, flavor, tuning: %w[E A D G])
5
+ super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Chord
4
+ def initialize(*chords, on: :text, flavor: 'intervals', notes: nil)
5
+ @chords =
6
+ if !chords.empty?
7
+ if chords[0].is_a?(String)
8
+ chords.map { |c| Coltrane::Chord.new(name: c) }
9
+ else
10
+ chords
11
+ end
12
+ elsif !notes.nil?
13
+ [Coltrane::Chord.new(notes: notes)]
14
+ end
15
+
16
+ @chords.each do |chord|
17
+ raise ChordNotFoundError unless chord.named?
18
+ desc = "#{chord.name} chord:"
19
+ Coltrane::Cli::Notes.new(chord.notes, on: on, desc: desc, flavor: flavor)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ module Coltrane
2
+ module Cli
3
+ class ColtraneCliError < StandardError
4
+ def initialize(msg)
5
+ super msg
6
+ end
7
+ end
8
+
9
+ class WrongFlavorError < ColtraneCliError
10
+ def initialize(msg=nil)
11
+ super msg || 'Wrong flavor. Check possible flavors with `coltrane list flavors`.'
12
+ end
13
+ end
14
+
15
+ class BadFindScales < ColtraneCliError
16
+ def initialize(msg=nil)
17
+ super msg || 'Provide --notes or --chords. Ex: `coltrane find-scale --notes C E G`.'
18
+ end
19
+ end
20
+
21
+ class WrongRepresentationTypeError < ColtraneCliError
22
+ def initialize(type)
23
+ super "The provided representation type (#{type}) "\
24
+ "is not available at the moment."
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ module Coltrane
2
+ module Cli
3
+ SPECIAL_FRETS = [3, 5, 7, 9, 12, 15, 17, 19]
4
+
5
+ class Guitar < Representation
6
+ def initialize(notes, flavor, tuning: %w[E A D G B E], frets: 22)
7
+ @notes = notes
8
+ @tuning = tuning.reverse
9
+ @frets = frets
10
+ @flavor = flavor
11
+ @ref_note = @notes.first
12
+ end
13
+
14
+ def render
15
+ [render_notes, render_special_frets, hint].join("\n"*2)
16
+ end
17
+
18
+ def render_notes
19
+ @tuning.map do |string|
20
+ string_note = Note[string]
21
+ (@frets+2).times.map do |i|
22
+ if i.zero?
23
+ string
24
+ else
25
+ fret = i - 1
26
+ note = string_note + fret
27
+ m = (@notes.include?(note) ? place_mark(note) : "--")
28
+ fret.zero? ? (m + " |") : m
29
+ end
30
+ end.join(' ')
31
+ end.join("\n")
32
+ end
33
+
34
+ def render_special_frets
35
+ (@frets+1).times.map do |fret|
36
+ m = SPECIAL_FRETS.include?(fret) ? fret.to_s.rjust(2, 0.to_s) : ' '
37
+ "#{m}#{' ' if fret.zero?}"
38
+ end.join(' ')
39
+ end
40
+
41
+ def render_dotted_frets
42
+ end
43
+
44
+ def place_mark(note)
45
+ case @flavor
46
+ when :notes then note.pretty_name.ljust(2, "\u266E")
47
+ when :intervals then (@ref_note - note).name.ljust(2, '-')
48
+ when :degrees then @notes.degree(note).to_s.rjust(2, '0')
49
+ when :marks then '◼◼'
50
+ else raise WrongFlavorError.new
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,18 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Notes
4
+ def initialize(notes, on: 'text', desc: 'The notes you supplied:', flavor: 'notes')
5
+ @desc = desc
6
+ flavor = flavor.underscore.to_sym
7
+ on = on.to_sym
8
+ notes = Coltrane::NoteSet.new(notes)
9
+ @representation = Representation.build(on, notes, flavor)
10
+ render
11
+ end
12
+
13
+ def render
14
+ puts "\n"+[@desc, @representation.render].join("\n"*2)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Piano < Representation
4
+ PIANO_TEMPLATE = <<~ASCII
5
+ ┌─┬─┬┬─┬─╥─┬─┬┬─┬┬─┬─╥─┬─┬┬─┬─╥─┬─┬┬─┬┬─┬─┐
6
+ │ │ ││ │ ║ │ ││ ││ │ ║ │ ││ │ ║ │ ││ ││ │ │
7
+ │ │X││X│ ║ │X││X││X│ ║ │X││X│ ║ │X││X││X│ │
8
+ │ │X││X│ ║ │X││X││X│ ║ │X││X│ ║ │X││X││X│ │
9
+ │ ┕╥┙┕╥┙ ║ ┕╥┙┕╥┙┕╥┙ ║ ┕╥┙┕╥┙ ║ ┕╥┙┕╥┙┕╥┙ │
10
+ │ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ │
11
+ │XX║XX║XX║XX║XX║XX║XX║XX║XX║XX║XX║XX║XX║XX│
12
+ └──╨──╨──╨──╨──╨──╨──╨──╨──╨──╨──╨──╨──╨──┘
13
+ ASCII
14
+
15
+ def render
16
+ PIANO_TEMPLATE.each_line.map.each_with_index do |l, ln|
17
+ case ln
18
+ when 2, 3 then replace_x(l, black_notes, 1, ln - 2)
19
+ when 6 then replace_x(l, white_notes, 2)
20
+ else l
21
+ end
22
+ end.join + "\n" + hint
23
+ end
24
+
25
+ private
26
+
27
+ def replace_x(line, notes, size, index=0)
28
+ line.gsub('X'*size).with_index do |match, i|
29
+ note = notes[i%notes.size]
30
+ next ' '*size unless @notes.include?(note)
31
+ Paint[replacer(note)[size == 2 ? 0..2 : index ], 'red']
32
+ end
33
+ end
34
+
35
+ def replacer(note)
36
+ # TODO: Maybe extract this method into its own class/module
37
+ case @flavor
38
+ when :intervals then (@ref_note - note).name
39
+ when :marks then '◼ '
40
+ when :degrees then @notes.degree(note).to_s.rjust(2,'0')
41
+ when :notes then note.pretty_name.to_s.ljust(2, "\u266E")
42
+ end
43
+ end
44
+
45
+ def white_notes
46
+ Coltrane::Scale.major.notes
47
+ end
48
+
49
+ def black_notes
50
+ Coltrane::Scale.pentatonic_major('C#',4).notes
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Representation
4
+ ACCEPTED_FLAVORS = %i[marks notes intervals degrees]
5
+
6
+ def self.inherited(subclass)
7
+ @@types ||= {}
8
+ @@types[subclass.to_s.split('::').last.underscore.to_sym] = subclass
9
+ end
10
+
11
+ def self.build(type, notes, flavor)
12
+ raise WrongFlavorError.new unless ACCEPTED_FLAVORS.include?(flavor)
13
+ type = case type
14
+ when :ukelele then :ukulele
15
+ when :bass then :bass_guitar
16
+ else type
17
+ end
18
+
19
+ if (the_class = @@types[type])
20
+ the_class.new(notes, flavor)
21
+ else
22
+ raise WrongRepresentationTypeError.new(type)
23
+ end
24
+ end
25
+
26
+ def initialize(notes, flavor)
27
+ @notes = notes
28
+ @flavor = flavor
29
+ @ref_note = notes.first
30
+ end
31
+
32
+ def hint
33
+ case @flavor
34
+ when :marks then ""
35
+ when :notes then "(\u266E means the note is natural, not flat nor sharp)"
36
+ when :intervals
37
+ <<~DESC
38
+ The letters represent the intervals relative to the root tone
39
+ Ex: 1P = Perfect First / 3m = Minor Third / 4A = Augmented Fourth
40
+ DESC
41
+
42
+ when :degrees then "(The numbers represent the degree of the note in the scale)"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Scale
4
+ def self.parse(str)
5
+ *scale_name, tone = str.split('-')
6
+ Coltrane::Scale.fetch(scale_name.join('_'), tone)
7
+ end
8
+
9
+ def self.find(notes: [], chords: [])
10
+ if notes.any?
11
+ puts "\nSearching for scales containing #{notes.join(', ')}:\n\n"
12
+ notes = NoteSet[*notes]
13
+ elsif chords.any?
14
+ puts "\nSearching for scales containing #{chords.join(', ')}:\n\n"
15
+ notes = chords.reduce(NoteSet[]) {|memo, c| memo + Coltrane::Chord.new(name: c).notes }
16
+ else raise BadFindScales.new
17
+ end
18
+ render_search(notes)
19
+ puts "\nUnderlined means the scale has all notes"
20
+ end
21
+
22
+ def self.render_search(searched_notes)
23
+ search = Coltrane::Scale.having_notes(searched_notes)
24
+ output = []
25
+ scale_width = search.results.keys.map(&:size).max
26
+ search.results.each do |name, scales_by_tone|
27
+ output << name.ljust(scale_width+1, ' ')
28
+ scales_by_tone.each do |tone_number, notes|
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
34
+ output << Paint["#{Note[tone_number].name}(#{notes.size})", color, und]
35
+ output << " "
36
+ end
37
+ output << "\n"
38
+ end
39
+ puts output.join
40
+ end
41
+
42
+ def initialize(scale, on: :text, flavor: 'degrees', notes: [], chords: [])
43
+ desc = "This is the #{scale.tone.name} #{scale.name} scale:"
44
+ Coltrane::Cli::Notes.new(scale.notes, on: on, desc: desc, flavor: flavor)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Text < Representation
4
+ def render
5
+ case @flavor
6
+ 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.new
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Coltrane
2
+ module Cli
3
+ class Ukulele < Guitar
4
+ SPECIAL_FRETS = [5, 7, 9, 12]
5
+
6
+ def initialize(notes, flavor, tuning: %w[G C E A], frets: 12)
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ require 'cli/errors'
2
+
3
+ require 'cli/representation'
4
+ require 'cli/text'
5
+ require 'cli/piano'
6
+ require 'cli/guitar'
7
+ require 'cli/bass_guitar'
8
+ require 'cli/ukulele'
9
+
10
+ require 'cli/notes'
11
+ require 'cli/chord'
12
+ require 'cli/scale'
@@ -1,48 +1,33 @@
1
- # $LOAD_PATH << __dir__
1
+ require 'bundler'
2
+ Bundler.require
2
3
 
3
- # require 'bundler'
4
+ require 'facets/multiton'
5
+ require 'ostruct'
6
+ require 'paint'
4
7
 
5
- # Bundler.require
6
-
7
- # ActiveSupport::Inflector.inflections do |inflect|
8
- # inflect.irregular 'cache', 'caches'
9
- # end
10
-
11
- # ActiveRecord::Base.establish_connection YAML.load_file("#{__dir__}/../db/config.yml")['production']
12
- # require "#{__dir__}/../db/schema.rb"
8
+ require 'coltrane/version'
9
+ require 'coltrane/errors'
13
10
 
11
+ require 'coltrane/cache'
14
12
  require 'coltrane/cadence'
15
13
 
14
+ require 'coltrane/interval_sequence'
15
+ require 'coltrane/interval_set'
16
+ require 'coltrane/interval'
16
17
  require 'coltrane/qualities'
17
18
  require 'coltrane/chord_quality'
19
+ require 'coltrane/note_set'
18
20
  require 'coltrane/chord'
21
+ require 'coltrane/roman_chord'
19
22
 
20
23
  require 'coltrane/classic_scales'
21
- require 'coltrane/fret_set'
22
-
23
- # require 'coltrane/piano_representation'
24
- # require 'coltrane/guitar_representation'
25
- require 'coltrane/essential_guitar_chords'
26
- require 'coltrane/guitar_chord_finder'
27
- require 'coltrane/guitar_note_set'
28
- require 'coltrane/guitar_chord'
29
- require 'coltrane/guitar_note'
30
- require 'coltrane/guitar_string'
31
- require 'coltrane/guitar'
24
+ require 'coltrane/classic_progressions'
32
25
 
33
- require 'coltrane/interval_sequence'
34
- require 'coltrane/interval_set'
35
- require 'coltrane/interval'
26
+ require 'coltrane/piano_representation'
36
27
 
37
- require 'coltrane/note_set'
38
28
  require 'coltrane/note'
39
29
 
40
30
  require 'coltrane/pitch'
41
31
  require 'coltrane/progression'
42
32
  require 'coltrane/scale'
43
- require 'coltrane/mode'
44
-
45
- # require 'coltrane/scale_chord'
46
- # require 'coltrane/chord_cache'
47
- # require 'coltrane/scale_cache'
48
- # require 'terminal_input'
33
+ require 'coltrane/mode'
@@ -0,0 +1,34 @@
1
+ module Coltrane
2
+ class Cache
3
+ class << self
4
+ def find_or_record(key, &block)
5
+ if (cached = fetch(key))
6
+ return cached
7
+ else
8
+ cached = yield block
9
+ record(key, cached)
10
+ cached
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def dir
17
+ dir = File.expand_path('../../../', __FILE__) + "/cache/"
18
+ Dir.mkdir(dir) unless Dir.exist?(dir)
19
+ dir
20
+ end
21
+
22
+ def fetch(key)
23
+ return unless File.file?(dir+key)
24
+ Marshal.load File.read(dir+key)
25
+ end
26
+
27
+ def record(key, contents)
28
+ File.open(dir+key, "w") do |f|
29
+ f.write Marshal.dump(contents)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,52 +1,40 @@
1
1
  module Coltrane
2
2
  # It describe a chord
3
- class Chord
4
- attr_reader :root_note, :quality
5
-
6
- def initialize(arg)
7
- @root_note, @quality = case arg
8
- when String then note_and_quality_from_name(arg)
9
- when GuitarNoteSet then [arg.root_note, arg.chord_quality]
10
- when Array
11
- case arg[0]
12
- when String then note_and_quality_from_notes(*arg.map{|a| Note.new(a)})
13
- when Note then note_and_quality_from_notes(*arg)
14
- end
3
+ class Chord < NoteSet
4
+ attr_reader :root_note, :quality, :notes
5
+
6
+ def initialize(notes: nil, root_note: nil, quality: nil, name: nil)
7
+ if !notes.nil?
8
+ notes = NoteSet[*notes] if notes.is_a?(Array)
9
+ @notes = notes
10
+ @root_note = notes.first
11
+ @quality = ChordQuality.new(notes: notes)
12
+ elsif !root_note.nil? && !quality.nil?
13
+ @notes = quality.notes_for(root_note)
14
+ @root_note = root_note
15
+ @quality = quality
16
+ elsif !name.nil?
17
+ @root_note, @quality, @notes = parse_from_name(name)
18
+ else
19
+ raise WrongKeywords.new('[notes:] || [root_note:, quality:] || [name:]')
15
20
  end
16
21
  end
17
22
 
18
- def guitar_chords
19
- GuitarChordFinder.by_chord(self)
20
- end
21
-
22
- def guitar_notes_for_root
23
- root_note.guitar_notes
24
- end
25
-
26
23
  def name
27
- return '' if root_note.nil? || quality.nil?
24
+ return @notes.names.join('/') if !named?
28
25
  "#{root_note.name}#{quality.name}"
29
26
  end
30
27
 
31
- def notes
32
- quality.intervals.each_with_object([]) do |interval, notes|
33
- notes << Note.new(root_note.number + interval)
34
- end
28
+ def named?
29
+ notes.size >= 3 &&
30
+ !root_note.nil? &&
31
+ !quality&.name.nil?
35
32
  end
36
33
 
37
34
  def intervals
38
35
  IntervalSequence.new(NoteSet.new(notes))
39
36
  end
40
37
 
41
- def on_guitar
42
- name + "\n" +
43
- NoteSet.new(notes).guitar_notes.render(root_note) + "\n\n"
44
- end
45
-
46
- def on_piano
47
- PianoRepresentation.render_intervals(notes, root_note)
48
- end
49
-
50
38
  def size
51
39
  notes.size
52
40
  end
@@ -69,13 +57,12 @@ module Coltrane
69
57
 
70
58
  protected
71
59
 
72
- def note_and_quality_from_name(chord_name)
73
- _, name, quality = chord_name.match(/([A-Z]#?)(.*)/).to_a
74
- [Note.new(name), ChordQuality.new_from_string(quality)]
75
- end
76
-
77
- def note_and_quality_from_notes(*notes)
78
- [notes.first, ChordQuality.new_from_notes(notes)]
60
+ def parse_from_name(name)
61
+ _, name, quality_name = name.match(/([A-Z]#?)(.*)/).to_a
62
+ root = Note[name]
63
+ quality = ChordQuality.new(name: quality_name)
64
+ notes = quality.notes_for(root)
65
+ [root, quality, notes]
79
66
  end
80
67
  end
81
68
  end