coltrane 1.2.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +48 -3
  5. data/Rakefile +1 -1
  6. data/bin/erubis +12 -0
  7. data/bin/flay +29 -0
  8. data/bin/gitlab +29 -0
  9. data/bin/httparty +29 -0
  10. data/bin/pronto +29 -0
  11. data/bin/ruby_parse +29 -0
  12. data/bin/ruby_parse_extract_error +29 -0
  13. data/bin/thor +12 -0
  14. data/exe/coltrane +8 -6
  15. data/lib/cli/guitar.rb +7 -7
  16. data/lib/cli/representation.rb +1 -1
  17. data/lib/coltrane.rb +22 -1
  18. data/lib/coltrane/cadence.rb +0 -1
  19. data/lib/coltrane/changes.rb +5 -7
  20. data/lib/coltrane/chord.rb +7 -7
  21. data/lib/coltrane/chord_quality.rb +17 -17
  22. data/lib/coltrane/chord_substitutions.rb +3 -1
  23. data/lib/coltrane/classic_scales.rb +7 -7
  24. data/lib/coltrane/errors.rb +26 -1
  25. data/lib/coltrane/frequency.rb +50 -0
  26. data/lib/coltrane/interval.rb +23 -86
  27. data/lib/coltrane/interval_class.rb +106 -0
  28. data/lib/coltrane/interval_sequence.rb +14 -13
  29. data/lib/coltrane/notable_progressions.rb +8 -3
  30. data/lib/coltrane/note.rb +44 -73
  31. data/lib/coltrane/note_set.rb +4 -4
  32. data/lib/coltrane/pitch.rb +43 -22
  33. data/lib/coltrane/pitch_class.rb +113 -0
  34. data/lib/coltrane/progression.rb +6 -9
  35. data/lib/coltrane/roman_chord.rb +14 -14
  36. data/lib/coltrane/scale.rb +8 -10
  37. data/lib/coltrane/unordered_interval_class.rb +7 -0
  38. data/lib/coltrane/version.rb +1 -1
  39. data/lib/coltrane_instruments.rb +4 -0
  40. data/lib/coltrane_instruments/guitar.rb +7 -0
  41. data/lib/coltrane_instruments/guitar/base.rb +14 -0
  42. data/lib/coltrane_instruments/guitar/chord.rb +41 -0
  43. data/lib/coltrane_instruments/guitar/note.rb +8 -0
  44. data/lib/coltrane_instruments/guitar/string.rb +8 -0
  45. data/lib/core_ext.rb +16 -27
  46. metadata +18 -2
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coltrane
4
+ # Interval class here is not related to the Object Oriented Programming context
5
+ # but to the fact that there is a class of intervals that can all be categorized
6
+ # as having the same quality.
7
+ #
8
+ # This class in specific still takes into account the order of intervals.
9
+ # C to D is a major second, but D to C is a minor seventh.
10
+ class IntervalClass < Interval
11
+ INTERVALS = %w[P1 m2 M2 m3 M3 P4 A4 P5 m6 M6 m7 M7].freeze
12
+
13
+ def self.split(interval)
14
+ interval.scan(/(\w)(\d\d?)/)[0]
15
+ end
16
+
17
+ def self.full_name(interval)
18
+ q, n = split(interval)
19
+ "#{q.interval_quality} #{n.to_i.interval_name}"
20
+ end
21
+
22
+ # Create full names and methods such as major_third? minor_seventh?
23
+ # TODO: It's a mess and it really needs a refactor someday
24
+ NAMES = INTERVALS.each_with_index.each_with_object({}) do |(interval, index), memo|
25
+ memo[interval] ||= []
26
+ 2.times do |o|
27
+ q, i = split(interval)
28
+ num = o * 7 + i.to_i
29
+ prev_q = split(INTERVALS[(index - 1) % 12])[0]
30
+ next_q = split(INTERVALS[(index + 1) % 12])[0]
31
+ memo[interval] << full_name("#{q}#{num}")
32
+ memo[interval] << full_name("d#{(num - 1 + 1) % 14 + 1}") if next_q.match? /m|P/
33
+ next if q == 'A'
34
+ memo[interval] << full_name("A#{(num - 1 - 1) % 14 + 1}") if prev_q.match? /M|P/
35
+ end
36
+ end
37
+
38
+ ALL_FULL_NAMES = NAMES.values.flatten
39
+
40
+ NAMES.each do |interval_name, full_names|
41
+ full_names.each do |the_full_name|
42
+ define_method "#{the_full_name.underscore}?" do
43
+ name == interval_name
44
+ end
45
+ IntervalClass.class.define_method the_full_name.underscore.to_s do
46
+ IntervalClass.new(interval_name)
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(arg)
52
+ super case arg
53
+ when Interval then arg.semitones
54
+ when String
55
+ INTERVALS.index(arg) || self.class.interval_by_full_name(arg)
56
+ when Numeric then arg
57
+ else raise WrongArgumentsError
58
+ end % 12 * 100
59
+ end
60
+
61
+ def self.[](semis)
62
+ new semis
63
+ end
64
+
65
+ def all_full_names
66
+ self.class.all_full_names
67
+ end
68
+
69
+ def self.all_full_names
70
+ ALL_FULL_NAMES
71
+ end
72
+
73
+ def ==(other)
74
+ (cents % 12) == (other.cents % 12)
75
+ end
76
+
77
+ def name
78
+ INTERVALS[semitones % 12]
79
+ end
80
+
81
+ def full_name
82
+ self.class.full_name(name)
83
+ end
84
+
85
+ def full_names
86
+ NAMES[name]
87
+ end
88
+
89
+ def +(other)
90
+ IntervalClass[semitones + other]
91
+ end
92
+
93
+ def -(other)
94
+ IntervalClass[semitones - other]
95
+ end
96
+
97
+ private
98
+
99
+ def self.interval_by_full_name(arg)
100
+ NAMES.invert.each do |full_names, interval_name|
101
+ return INTERVALS.index(interval_name) if full_names.include?(arg)
102
+ end
103
+ raise IntervalNotFoundError, arg
104
+ end
105
+ end
106
+ end
@@ -7,11 +7,11 @@ module Coltrane
7
7
  attr_reader :intervals
8
8
 
9
9
  def_delegators :@intervals, :map, :each, :[], :size,
10
- :reduce, :delete
10
+ :reduce, :delete, :reject!, :delete_if
11
11
 
12
- Interval::ALL_FULL_NAMES.each do |full_name|
12
+ IntervalClass.all_full_names.each do |full_name|
13
13
  define_method "has_#{full_name.underscore}?" do
14
- !!(intervals.detect {|i| i.public_send("#{full_name.underscore}?")})
14
+ !!(intervals.detect { |i| i.public_send("#{full_name.underscore}?") })
15
15
  end
16
16
  end
17
17
 
@@ -22,7 +22,7 @@ module Coltrane
22
22
  return priority unless priority.nil?
23
23
  @intervals.each do |ix|
24
24
  ix.full_names.detect do |ixx|
25
- return ixx if ixx.match(/#{i.interval_name}/)
25
+ return ixx if ixx.match?(/#{i.interval_name}/)
26
26
  end
27
27
  end
28
28
  nil
@@ -31,7 +31,7 @@ module Coltrane
31
31
  define_method "#{i.interval_name.underscore}!" do
32
32
  @intervals.each do |ix|
33
33
  ix.full_names.detect do |ixx|
34
- next if ixx.match(/Diminished|Augmented/)
34
+ next if ixx.match?(/Diminished|Augmented/)
35
35
  return ixx if ixx.match? /#{i.interval_name}/
36
36
  end
37
37
  end
@@ -40,20 +40,21 @@ module Coltrane
40
40
 
41
41
  # defines methods like :has_fifth?, :has_third?, has_eleventh?:
42
42
  define_method "has_#{i.interval_name.underscore}?" do
43
- !!@intervals.detect {|ix| ix.full_name.match(/#{i.interval_name}/) }
43
+ !!@intervals.detect { |ix| ix.full_name.match(/#{i.interval_name}/) }
44
44
  end
45
45
  end
46
46
 
47
47
  def initialize(notes: nil, intervals: nil, distances: nil)
48
- if !notes.nil?
48
+ if notes
49
49
  notes = NoteSet[*notes] if notes.is_a?(Array)
50
50
  @intervals = intervals_from_notes(notes)
51
- elsif !intervals.nil?
52
- @intervals = intervals.map { |i| Interval[i] }
53
- elsif !distances.nil?
51
+ elsif intervals
52
+ @intervals = intervals.map { |i| IntervalClass[i] }
53
+ elsif distances
54
54
  @distances = distances
55
55
  @intervals = intervals_from_distances(distances)
56
56
  else
57
+ require 'pry'; binding.pry
57
58
  raise 'Provide: [notes:] || [intervals:] || [distances:]'
58
59
  end
59
60
  end
@@ -73,7 +74,7 @@ module Coltrane
73
74
  end
74
75
 
75
76
  def has?(interval_name)
76
- @intervals.include?(Interval[interval_name])
77
+ @intervals.include?(IntervalClass[interval_name])
77
78
  end
78
79
 
79
80
  alias interval_names names
@@ -144,13 +145,13 @@ module Coltrane
144
145
  private
145
146
 
146
147
  def intervals_from_distances(distances)
147
- distances[0..-2].reduce([Interval[0]]) do |memo, d|
148
+ distances[0..-2].reduce([IntervalClass[0]]) do |memo, d|
148
149
  memo + [memo.last + d]
149
150
  end
150
151
  end
151
152
 
152
153
  def intervals_from_notes(notes)
153
- notes.map { |n| notes.root - n }.sort_by(&:semitones)
154
+ notes.map { |n| n - notes.root }.sort_by(&:semitones)
154
155
  end
155
156
  end
156
157
  end
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Coltrane
4
+
5
+ # This module takes care of adding to progressions knowledge that is more
6
+ # based on common standards and practices.
2
7
  module NotableProgressions
3
8
  PROGRESSIONS = {
4
- 'Pop' => ['I-V-vi-IV', :major],
9
+ 'Pop' => ['I-V-vi-IV', :major],
5
10
  'Jazzy Pop' => ['IM7-V7-vi7-IVM7', :major],
6
11
  'Jazz' => ['ii7-V7-I7', :major],
7
12
  'Jazz Minor' => ['ii7-V7-i7', :major],
@@ -10,7 +15,7 @@ module Coltrane
10
15
  'Fifties' => ['I-vi-IV-V', :major],
11
16
  'Circle' => ['vi-ii-V-I', :major],
12
17
  'Tune Up' => ['ii7-V7-IM7-i7-IV7-IVM7-VIIM7', :minor]
13
- }
18
+ }.freeze
14
19
 
15
20
  PROGRESSIONS.each do |name, values|
16
21
  notation, scale_name = values
@@ -21,4 +26,4 @@ module Coltrane
21
26
  end
22
27
  end
23
28
  end
24
- end
29
+ end
data/lib/coltrane/note.rb CHANGED
@@ -1,98 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coltrane
4
- # Describes a musical note, independent of octave (that'd be pitch)
5
- class Note
6
- include Multiton
7
-
8
- attr_reader :name, :number
9
- alias id number
10
- alias to_s name
11
-
12
- NOTES = {
13
- 'C' => 0,
14
- 'C#' => 1,
15
- 'Db' => 1,
16
- 'D' => 2,
17
- 'D#' => 3,
18
- 'Eb' => 3,
19
- 'E' => 4,
20
- 'F' => 5,
21
- 'F#' => 6,
22
- 'Gb' => 6,
23
- 'G' => 7,
24
- 'G#' => 8,
25
- 'Ab' => 8,
26
- 'A' => 9,
27
- 'A#' => 10,
28
- 'Bb' => 10,
29
- 'B' => 11
4
+ # Notes are different ways of calling pitch classes. In the context of equal
5
+ # tempered scales, they're more like a conceptual subject for
6
+ # matters of convention than an actual thing.
7
+ #
8
+ # Take for example A# and Bb. Those are different notes. Nevertheless, in the
9
+ # context of equal tempered scales they represent pretty much the same
10
+ # frequency.
11
+ #
12
+ # The theory of notes have changed too much in the course of time, which
13
+ # lead us with a lot of conventions and strategies when dealing with music.
14
+ # That's what this class is for.
15
+ class Note < PitchClass
16
+ attr_reader :alteration
17
+
18
+ ALTERATIONS = {
19
+ 'b' => -1,
20
+ '#' => 1
30
21
  }.freeze
31
22
 
32
- def initialize(name)
33
- @name = name
34
- @number = NOTES[name]
23
+ def initialize(arg)
24
+ note_name = case arg
25
+ when String then arg
26
+ when PitchClass then arg.true_notation
27
+ when Numeric, Frequency then PitchClass.new(arg).true_notation
28
+ else raise(WrongArgumentsError, arg)
29
+ end
30
+
31
+ chars = note_name.chars
32
+ letter = chars.shift
33
+ raise InvalidNoteError, arg unless ('A'..'G').cover?(letter)
34
+ @alteration = chars.reduce(0) do |alt, symbol|
35
+ raise InvalidNoteError, arg unless ALTERATIONS.include?(symbol)
36
+ alt + ALTERATIONS[symbol]
37
+ end
38
+ super((PitchClass[letter].integer + @alteration) % PitchClass.size)
35
39
  end
36
40
 
37
- private_class_method :new
38
-
39
41
  def self.[](arg)
40
- name =
41
- case arg
42
- when Note then return arg
43
- when String then find_note(arg)
44
- when Numeric then NOTES.key(arg % 12)
45
- else
46
- raise InvalidNoteError, "Wrong type: #{arg.class}"
47
- end
48
-
49
- new(name) || (raise InvalidNoteError, arg.to_s)
42
+ new(arg)
50
43
  end
51
44
 
52
- def self.all
53
- %w[C C# D D# E F F# G G# A A# B].map { |n| Note[n] }
45
+ def name
46
+ "#{base_pitch_class}#{accidents}".gsub(/#b|b#/, '')
54
47
  end
55
48
 
56
- def self.find_note(str)
57
- NOTES.each_key { |k, _v| return k if str.casecmp?(k) }
58
- nil
49
+ def base_pitch_class
50
+ PitchClass[integer - alteration]
59
51
  end
60
52
 
61
- def pretty_name
62
- @name.tr('b', "\u266D").tr('#', "\u266F")
53
+ def alteration=(a)
54
+ @alteration = a unless PitchClass[integer - a].accidental?
63
55
  end
64
56
 
65
- alias to_s name
66
-
67
- def accident?
68
- [1, 3, 6, 8, 10].include?(number)
57
+ def alter(x)
58
+ Note.new(name).tap { |n| n.alteration = x }
69
59
  end
70
60
 
71
- def +(other)
72
- case other
73
- when Interval then Note[number + other.semitones]
74
- when Numeric then Note[number + other]
75
- when Note then Chord.new(number + other.number)
76
- end
77
- end
78
-
79
- def -(other)
80
- case other
81
- when Numeric then Note[number - other]
82
- when Interval then Note[number - other.semitones]
83
- when Note then Interval[other.number - number]
84
- end
61
+ def accidents
62
+ (@alteration > 0 ? '#' : 'b') * alteration.abs
85
63
  end
86
64
 
87
65
  def interval_to(note_name)
88
66
  Note[note_name] - self
89
67
  end
90
-
91
- def enharmonic?(other)
92
- case other
93
- when String then number == Note[other].number
94
- when Note then number == other.number
95
- end
96
- end
97
68
  end
98
69
  end
@@ -38,14 +38,14 @@ module Coltrane
38
38
  case other
39
39
  when Note then NoteSet[*(notes + [other])]
40
40
  when NoteSet then NoteSet[*notes, *other.notes]
41
- when Interval then NoteSet[*notes.map {|n| n + other}]
41
+ when Interval then NoteSet[*notes.map { |n| n + other }]
42
42
  end
43
43
  end
44
44
 
45
45
  def -(other)
46
46
  case other
47
47
  when NoteSet then NoteSet[*(notes - other.notes)]
48
- when Interval then NoteSet[*notes.map {|n| n - other}]
48
+ when Interval then NoteSet[*notes.map { |n| n - other }]
49
49
  end
50
50
  end
51
51
 
@@ -57,8 +57,8 @@ module Coltrane
57
57
  map(&:name)
58
58
  end
59
59
 
60
- def numbers
61
- map(&:number)
60
+ def integers
61
+ map(&:integer)
62
62
  end
63
63
 
64
64
  def interval_sequence
@@ -3,35 +3,56 @@
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 :number
7
-
8
- def initialize(pitch)
9
- case pitch
10
- when String then @number = number_from_name(pitch)
11
- when Numeric then @number = pitch
12
- when Pitch then @number = pitch.number
13
- end
14
- end
6
+ attr_reader :pitch_class, :octave
15
7
 
16
- def number_from_name(pitch_string)
17
- _, note, octaves = pitch_string.match(/(.*)(\d)/).to_a
18
- Note[note].number + 12 * octaves.to_i
19
- end
8
+ def initialize(notation_arg = nil,
9
+ pitch_class: nil,
10
+ octave: nil,
11
+ notation: nil,
12
+ frequency: nil)
20
13
 
21
- def name
22
- "#{note.name}#{octave}"
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]
19
+ elsif frequency
20
+ pitch_class_and_octave_from_frequency(frequency)
21
+ else
22
+ raise InvalidArgumentsError
23
+ end
23
24
  end
24
25
 
25
- def octave
26
- number / 12
27
- end
26
+ private
28
27
 
29
- def note
30
- Note[number]
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]
31
31
  end
32
32
 
33
- def +(other)
34
- Pitch.new(number + (other.is_a?(Pitch) ? other.number : other))
33
+ def pitch_class_and_octave_from_frequency(frequency)
34
+ [PitchClass[frequency], Math.log(f.to_f / TUNING, 2) / 12]
35
35
  end
36
+
37
+ # def number_from_name(pitch_string)
38
+ # Note[note].number + 12 * octaves.to_i
39
+ # end
40
+
41
+ # def name
42
+ # "#{note.name}#{octave}"
43
+ # end
44
+
45
+ # def octave
46
+ # number / 12
47
+ # end
48
+
49
+ # def note
50
+ # Note[number]
51
+ # end
52
+
53
+ # def +(other)
54
+ # Pitch.new(number + (other.is_a?(Pitch) ? other.number : other))
55
+ # end
56
+ # end
36
57
  end
37
58
  end