coltrane 2.0.0 → 2.1.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 +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +21 -3
  5. data/README.md +0 -7
  6. data/Rakefile +5 -0
  7. data/bin/console +2 -0
  8. data/bin/opal +29 -0
  9. data/bin/opal-build +29 -0
  10. data/bin/opal-mspec +29 -0
  11. data/bin/opal-repl +29 -0
  12. data/bin/rackup +12 -0
  13. data/bin/sprockets +12 -0
  14. data/bin/tilt +12 -0
  15. data/coltrane.gemspec +1 -1
  16. data/config.ru +10 -0
  17. data/exe/coltrane +44 -13
  18. data/lib/cli.rb +7 -1
  19. data/lib/cli/bass_guitar.rb +1 -1
  20. data/lib/cli/chord.rb +11 -4
  21. data/lib/cli/config.rb +38 -0
  22. data/lib/cli/guitar.rb +8 -10
  23. data/lib/cli/guitar_chords.rb +122 -0
  24. data/lib/cli/notes.rb +3 -4
  25. data/lib/cli/piano.rb +1 -1
  26. data/lib/cli/representation.rb +6 -12
  27. data/lib/cli/scale.rb +4 -4
  28. data/lib/cli/text.rb +1 -1
  29. data/lib/cli/ukulele.rb +1 -1
  30. data/lib/coltrane.rb +35 -32
  31. data/lib/coltrane/chord_quality.rb +4 -3
  32. data/lib/coltrane/circle_of_fifths.rb +29 -0
  33. data/lib/coltrane/classic_scales.rb +36 -43
  34. data/lib/coltrane/diatonic_scale.rb +34 -0
  35. data/lib/coltrane/frequency.rb +1 -1
  36. data/lib/coltrane/interval.rb +14 -0
  37. data/lib/coltrane/interval_class.rb +6 -0
  38. data/lib/coltrane/interval_sequence.rb +0 -1
  39. data/lib/coltrane/key.rb +14 -0
  40. data/lib/coltrane/note.rb +37 -2
  41. data/lib/coltrane/note_set.rb +23 -2
  42. data/lib/coltrane/pitch.rb +65 -31
  43. data/lib/coltrane/pitch_class.rb +25 -12
  44. data/lib/coltrane/progression.rb +2 -2
  45. data/lib/coltrane/qualities.rb +257 -0
  46. data/lib/coltrane/roman_chord.rb +10 -10
  47. data/lib/coltrane/scale.rb +9 -3
  48. data/lib/coltrane/version.rb +1 -1
  49. data/lib/coltrane/voicing.rb +38 -0
  50. data/lib/coltrane_game/question.rb +6 -0
  51. data/lib/coltrane_instruments.rb +3 -0
  52. data/lib/coltrane_instruments/guitar.rb +5 -2
  53. data/lib/coltrane_instruments/guitar/base.rb +18 -3
  54. data/lib/coltrane_instruments/guitar/chord.rb +139 -12
  55. data/lib/coltrane_instruments/guitar/note.rb +16 -0
  56. data/lib/coltrane_instruments/guitar/string.rb +21 -0
  57. data/lib/coltrane_synth.rb +7 -0
  58. data/lib/coltrane_synth/base.rb +47 -0
  59. data/lib/coltrane_synth/synth.rb +24 -0
  60. data/lib/core_ext.rb +25 -7
  61. data/lib/os.rb +19 -0
  62. metadata +24 -9
  63. data/dist/coltrane +0 -0
  64. data/lib/coltrane/piano_representation.rb +0 -0
@@ -3,14 +3,15 @@
3
3
  module Coltrane
4
4
  # It describe the quality of a chord, like maj7 or dim.
5
5
  class ChordQuality < IntervalSequence
6
+ include Qualities
6
7
  attr_reader :name
8
+ # QUALITIES_FILE = File.expand_path("#{'../' * 3}data/qualities.json", __FILE__)
7
9
 
8
10
  private
9
11
 
10
12
  def self.chord_trie
11
- trie = YAML.load_file(
12
- File.expand_path("#{'../' * 3}data/qualities.yml", __FILE__)
13
- )
13
+ File.expand_path("#{'../' * 3}data/qualities.json", __FILE__)
14
+ trie = QUALITIES
14
15
 
15
16
  trie.clone_values from_keys: ['Perfect Unison', 'Major Third'],
16
17
  to_keys: ['Perfect Unison', 'Major Second'],
@@ -0,0 +1,29 @@
1
+ module Coltrane
2
+ class CircleOfFifths
3
+ attr_reader :notes
4
+
5
+ LETTER_SEQUENCE = %w[C G D A E B F].freeze
6
+
7
+ def initialize(start_note = Note['C'], size = Float::INFINITY)
8
+ index = letters.index(start_note.name[0])
9
+ @notes = fifths(note: start_note - Interval.perfect_fifth,
10
+ size: size,
11
+ index: (index - 1) % LETTER_SEQUENCE.size)
12
+ end
13
+
14
+ private
15
+
16
+ def fifths(note:, index: nil, notes: [], size:)
17
+ return notes if size == 0
18
+ return notes if notes.any? && note.as(letters(index)).name == notes.first.name
19
+ fifths note: note + Interval.perfect_fifth,
20
+ notes: notes + [note.as(letters(index))],
21
+ index: index + 1,
22
+ size: size - 1
23
+ end
24
+
25
+ def letters(i=nil)
26
+ i.nil? ? LETTER_SEQUENCE : LETTER_SEQUENCE[i % LETTER_SEQUENCE.size]
27
+ end
28
+ end
29
+ end
@@ -4,10 +4,8 @@ module Coltrane
4
4
  # This module deals with well known scales on music
5
5
  module ClassicScales
6
6
  SCALES = {
7
- 'Major' => [2, 2, 1, 2, 2, 2, 1],
8
7
  'Pentatonic Major' => [2, 2, 3, 2, 3],
9
8
  'Blues Major' => [2, 1, 1, 3, 2, 3],
10
- 'Natural Minor' => [2, 1, 2, 2, 1, 2, 2],
11
9
  'Harmonic Minor' => [2, 1, 2, 2, 1, 3, 1],
12
10
  'Hungarian Minor' => [2, 1, 2, 1, 1, 3, 1],
13
11
  'Pentatonic Minor' => [3, 2, 2, 3, 2],
@@ -17,9 +15,15 @@ module Coltrane
17
15
  'Chromatic' => [1] * 12
18
16
  }.freeze
19
17
 
20
- MODES = {
21
- 'Major' => %w[Ionian Dorian Phrygian Lydian Mixolydian Aeolian Locrian]
22
- }.freeze
18
+ GREEK_MODES = %w[
19
+ Ionian
20
+ Dorian
21
+ Phrygian
22
+ Lydian
23
+ Mixolydian
24
+ Aeolian
25
+ Locrian
26
+ ].freeze
23
27
 
24
28
  # Creates factories for scales
25
29
  SCALES.each do |name, distances|
@@ -28,62 +32,51 @@ module Coltrane
28
32
  end
29
33
  end
30
34
 
31
- # Creates factories for Greek Modes and possibly others
32
- MODES.each do |scale, modes|
33
- modes.each_with_index do |mode, index|
34
- mode_name = mode
35
- mode_n = index + 1
36
- define_method mode.underscore do |tone = 'C'|
37
- new(*SCALES[scale], tone: tone, mode: mode_n, name: mode_name)
38
- end
35
+ # Creates factories for Greek Modes
36
+ GREEK_MODES.each_with_index do |mode, index|
37
+ mode_name = mode
38
+ mode_n = index + 1
39
+ define_method mode.underscore do |tone = 'C'|
40
+ new(notes: DiatonicScale.new(tone).notes.rotate(index))
39
41
  end
40
42
  end
41
43
 
42
- alias minor natural_minor
44
+ # Factories for the diatonic scale
45
+ def major(note='C')
46
+ DiatonicScale.new(note)
47
+ end
48
+
49
+ def minor(note='A')
50
+ DiatonicScale.new(note, major: false)
51
+ end
52
+
53
+ alias diatonic major
54
+ alias natural_minor minor
43
55
  alias pentatonic pentatonic_major
44
56
  alias blues blues_major
45
57
 
46
58
  def known_scales
47
- SCALES.keys
59
+ SCALES.keys + ['Major', 'Natural Minor']
48
60
  end
49
61
 
50
- # All but the chromatic
62
+ # List of scales appropriate for search
51
63
  def standard_scales
52
- SCALES.reject { |k, _v| k == 'Chromatic' }
64
+ known_scales - ['Chromatic']
53
65
  end
54
66
 
55
67
  def fetch(name, tone = nil)
56
68
  Coltrane::Scale.public_send(name, tone)
57
69
  end
58
70
 
59
- def from_key(key)
60
- key_regex = /([A-G][#b]?)([mM]?)/
61
- _, note, s = *key.match(key_regex)
62
- scale = s == 'm' ? :minor : :major
63
- Scale.public_send(scale, note)
64
- end
65
-
66
- # Will output a OpenStruct like the following:
67
- # {
68
- # scales: [array of scales]
69
- # results: {
70
- # scale_name: {
71
- # note_number => found_notes
72
- # }
73
- # }
74
- # }
75
-
76
71
  def having_notes(notes)
77
72
  format = { scales: [], results: {} }
78
- OpenStruct.new begin
79
- standard_scales.each_with_object(format) do |(name, intervals), output|
80
- PitchClass.all.each.map do |tone|
81
- scale = new(*intervals, tone: tone, name: scale)
82
- output[:results][name] ||= {}
83
- next if output[:results][name].key?(tone.integer)
84
- output[:scales] << scale if scale.include?(notes)
85
- output[:results][name][tone.integer] = scale.notes & notes
86
- end
73
+ standard_scales.each_with_object(format) do |name, output|
74
+ PitchClass.all.each.map do |tone|
75
+ scale = send(name.underscore, tone)
76
+ output[:results][name] ||= {}
77
+ next if output[:results][name].key?(tone.integer)
78
+ output[:scales] << scale if scale.include?(notes)
79
+ output[:results][name][tone.integer] = scale.notes & notes
87
80
  end
88
81
  end
89
82
  end
@@ -0,0 +1,34 @@
1
+ module Coltrane
2
+ class DiatonicScale < Scale
3
+ def initialize(tone, major: true)
4
+ @major = major
5
+ tone = Note[tone]
6
+ notes = CircleOfFifths.new(tone - (major? ? 0 : 9), 7).notes.sort
7
+ super notes: notes.rotate(notes.index(tone))
8
+ end
9
+
10
+ def name
11
+ major? ? 'Major' : 'Natural Minor'
12
+ end
13
+
14
+ def relative_minor
15
+ minor? ? self : self.class.new(@tone + 9, major: false)
16
+ end
17
+
18
+ def relative_major
19
+ major? ? self : self.class.new(@tone - 9, major: true)
20
+ end
21
+
22
+ def relative
23
+ major? ? relative_minor : relative_major
24
+ end
25
+
26
+ def major?
27
+ !!@major
28
+ end
29
+
30
+ def minor?
31
+ !@major
32
+ end
33
+ end
34
+ end
@@ -21,7 +21,7 @@ module Coltrane
21
21
  end
22
22
 
23
23
  def octave(n)
24
- frequency * 2**n
24
+ Frequency[frequency * 2**n]
25
25
  end
26
26
 
27
27
  def ==(other)
@@ -4,10 +4,16 @@ module Coltrane
4
4
  # Interval describe the logarithmic distance between 2 frequencies.
5
5
  # It's measured in cents.
6
6
  class Interval
7
+ include Comparable
8
+
7
9
  attr_reader :cents
8
10
 
9
11
  class << self
10
12
  alias [] new
13
+
14
+ def method_missing(method, *args)
15
+ IntervalClass.send(method, *args)
16
+ end
11
17
  end
12
18
 
13
19
  def initialize(cents)
@@ -46,5 +52,13 @@ module Coltrane
46
52
  when Interval then Interval[cents - other.cents]
47
53
  end
48
54
  end
55
+
56
+ def -@
57
+ Interval[-cents]
58
+ end
59
+
60
+ def <=>(other)
61
+ cents <=> other.cents
62
+ end
49
63
  end
50
64
  end
@@ -19,6 +19,8 @@ module Coltrane
19
19
  "#{q.interval_quality} #{n.to_i.interval_name}"
20
20
  end
21
21
 
22
+ def self.method_missing; end
23
+
22
24
  # Create full names and methods such as major_third? minor_seventh?
23
25
  # TODO: It's a mess and it really needs a refactor someday
24
26
  NAMES = INTERVALS.each_with_index.each_with_object({}) do |(interval, index), memo|
@@ -94,6 +96,10 @@ module Coltrane
94
96
  IntervalClass[semitones - other]
95
97
  end
96
98
 
99
+ def -@
100
+ IntervalClass[-semitones]
101
+ end
102
+
97
103
  private
98
104
 
99
105
  def self.interval_by_full_name(arg)
@@ -54,7 +54,6 @@ module Coltrane
54
54
  @distances = distances
55
55
  @intervals = intervals_from_distances(distances)
56
56
  else
57
- require 'pry'; binding.pry
58
57
  raise 'Provide: [notes:] || [intervals:] || [distances:]'
59
58
  end
60
59
  end
@@ -0,0 +1,14 @@
1
+ module Coltrane
2
+ class Key < DiatonicScale
3
+ KEY_REGEX = /([A-G][#b]?)([mM]?)/
4
+
5
+ def initialize(notation)
6
+ _, note, s = *notation.match(KEY_REGEX)
7
+ super(note, major: s != 'm')
8
+ end
9
+
10
+ def self.[](notation)
11
+ new(notation)
12
+ end
13
+ end
14
+ end
data/lib/coltrane/note.rb CHANGED
@@ -30,9 +30,9 @@ module Coltrane
30
30
 
31
31
  chars = note_name.chars
32
32
  letter = chars.shift
33
- raise InvalidNoteError, arg unless ('A'..'G').cover?(letter)
33
+ raise InvalidNoteLetterError, arg unless ('A'..'G').cover?(letter)
34
34
  @alteration = chars.reduce(0) do |alt, symbol|
35
- raise InvalidNoteError, arg unless ALTERATIONS.include?(symbol)
35
+ raise InvalidNoteLetterError, arg unless ALTERATIONS.include?(symbol)
36
36
  alt + ALTERATIONS[symbol]
37
37
  end
38
38
  super((PitchClass[letter].integer + @alteration) % PitchClass.size)
@@ -58,12 +58,47 @@ module Coltrane
58
58
  Note.new(name).tap { |n| n.alteration = x }
59
59
  end
60
60
 
61
+ def sharp?
62
+ alteration == 1
63
+ end
64
+
65
+ def double_sharp?
66
+ alteration == 2
67
+ end
68
+
69
+ def flat?
70
+ alteration == -1
71
+ end
72
+
73
+ def double_flat?
74
+ alteration == -2
75
+ end
76
+
77
+ def natural?
78
+ alteration == 0
79
+ end
80
+
61
81
  def accidents
62
82
  (@alteration > 0 ? '#' : 'b') * alteration.abs
63
83
  end
64
84
 
85
+ def -(other)
86
+ result = super(other)
87
+ result.is_a?(Note) ? result.alter(alteration) : result
88
+ end
89
+
90
+ def +(other)
91
+ result = super(other)
92
+ result.is_a?(Note) ? result.alter(alteration) : result
93
+ end
94
+
65
95
  def interval_to(note_name)
66
96
  Note[note_name] - self
67
97
  end
98
+
99
+ def as(letter)
100
+ a = (self - Note[letter])
101
+ alter([a.semitones, -((-a).semitones)].min_by(&:abs))
102
+ end
68
103
  end
69
104
  end
@@ -6,7 +6,8 @@ module Coltrane
6
6
  extend Forwardable
7
7
 
8
8
  def_delegators :@notes, :first, :each, :size, :map, :reduce, :index,
9
- :[], :index, :empty?, :permutation, :include?, :<<
9
+ :[], :index, :empty?, :permutation, :include?, :<<, :any?,
10
+ :count, :rotate
10
11
 
11
12
  attr_reader :notes
12
13
 
@@ -21,7 +22,7 @@ module Coltrane
21
22
  @notes =
22
23
  case arg
23
24
  when NoteSet then arg.notes
24
- when Array then arg.map { |n| n.is_a?(Note) ? n : Note[n] }.uniq
25
+ when Array then arg.compact.map { |n| n.is_a?(PitchClass) ? n : Note[n] }.uniq
25
26
  else raise InvalidNotesError, arg
26
27
  end
27
28
  end
@@ -61,6 +62,26 @@ module Coltrane
61
62
  map(&:integer)
62
63
  end
63
64
 
65
+ def accidentals
66
+ count(&:accidental?)
67
+ end
68
+
69
+ def sharps
70
+ count(&:sharp?)
71
+ end
72
+
73
+ def flats
74
+ count(&:flat?)
75
+ end
76
+
77
+ def alter(alteration)
78
+ NoteSet[*map {|n| n.alter(alteration)}]
79
+ end
80
+
81
+ def alter_accidentals(alteration)
82
+ NoteSet[*map {|n| n.alter(alteration) if n.accidental?}]
83
+ end
84
+
64
85
  def interval_sequence
65
86
  IntervalSequence.new(notes: self)
66
87
  end
@@ -3,56 +3,90 @@
3
3
  module Coltrane
4
4
  # It describes a pitch, like E4 or Bb5. It's like a note, but it has an octave
5
5
  class Pitch
6
- attr_reader :pitch_class, :octave
6
+ attr_reader :integer
7
7
 
8
8
  def initialize(notation_arg = nil,
9
- pitch_class: nil,
9
+ note: nil,
10
10
  octave: nil,
11
11
  notation: nil,
12
12
  frequency: nil)
13
13
 
14
- @pitch_class, @octave =
15
- if notation_arg || notation
16
- pitch_class_and_octave_from_notation(notation_arg || notation)
17
- elsif pitch_class && octave
18
- [pitch_class, octave]
14
+ @integer = begin
15
+ if (n = notation_arg || notation)
16
+ n.is_a?(Integer) ? n : integer_from_notation(n)
17
+ elsif note && octave
18
+ integer_from_note_and_octave(Note[note], octave)
19
19
  elsif frequency
20
- pitch_class_and_octave_from_frequency(frequency)
20
+ integer_from_frequency(frequency)
21
21
  else
22
- raise InvalidArgumentsError
22
+ raise(InvalidArgumentsError)
23
23
  end
24
+ end
24
25
  end
25
26
 
26
- private
27
+ def self.[](*args)
28
+ new *args
29
+ end
30
+
31
+ def scientific_notation
32
+ "#{pitch_class}#{octave}"
33
+ end
34
+
35
+ def pitch_class
36
+ PitchClass[integer]
37
+ end
38
+
39
+ def octave
40
+ (integer / 12) - 1
41
+ end
42
+
43
+ alias notation scientific_notation
44
+ alias name scientific_notation
45
+ alias to_s scientific_notation
46
+
47
+ alias hash integer
48
+ alias midi integer
27
49
 
28
- def pitch_class_and_octave_from_notation(name)
29
- pitch_class_notation, octaves = *name.match(/(.*)(\d)/)
30
- [PitchClass.new(pitch_class_notation), octaves.to_f]
50
+ def ==(other)
51
+ integer == other.integer
31
52
  end
32
53
 
33
- def pitch_class_and_octave_from_frequency(frequency)
34
- [PitchClass[frequency], Math.log(f.to_f / TUNING, 2) / 12]
54
+ def frequency
55
+ pitch_class.frequency.octave(octave)
35
56
  end
36
57
 
37
- # def number_from_name(pitch_string)
38
- # Note[note].number + 12 * octaves.to_i
39
- # end
58
+ alias eql? ==
59
+ alias eq ==
40
60
 
41
- # def name
42
- # "#{note.name}#{octave}"
43
- # end
61
+ def +(other)
62
+ case other
63
+ when Integer then Pitch[integer + other]
64
+ end
65
+ end
66
+
67
+ def -(other)
68
+ case other
69
+ when Integer then Pitch[integer - other]
70
+ end
71
+ end
72
+
73
+ private
44
74
 
45
- # def octave
46
- # number / 12
47
- # end
75
+ def integer_from_notation(the_notation)
76
+ _, n, o = *the_notation.match(/(\D+)(\d+)/)
77
+ integer_from_note_and_octave(Note[n], o)
78
+ end
48
79
 
49
- # def note
50
- # Note[number]
51
- # end
80
+ def integer_from_note_and_octave(p, o)
81
+ 12 * (o.to_i + 1) + p.integer
82
+ end
52
83
 
53
- # def +(other)
54
- # Pitch.new(number + (other.is_a?(Pitch) ? other.number : other))
55
- # end
56
- # end
84
+ def integer_from_frequency(f)
85
+ octave_from_frequency(f) * 12 + PitchClass.new(frequency: f).integer
86
+ end
87
+
88
+ def octave_from_frequency(f)
89
+ Math.log(f.to_f / PitchClass['C'].frequency.to_f, 2).ceil
90
+ end
57
91
  end
58
92
  end