coltrane 1.2.4 → 2.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 (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