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
data/lib/cli/guitar.rb CHANGED
@@ -24,7 +24,7 @@ module Coltrane
24
24
  string_note = Note[string]
25
25
  Array.new(@frets + 2) do |i|
26
26
  if i.zero?
27
- Paint[string, HSL.new(140 + str_i * 20,50,50).html]
27
+ Paint[string, HSL.new(140 + str_i * 20, 50, 50).html]
28
28
  else
29
29
  fret = i - 1
30
30
  note = string_note + fret
@@ -37,14 +37,14 @@ module Coltrane
37
37
 
38
38
  def render_special_frets
39
39
  ' ' +
40
- Array.new(@frets + 2) do |fret|
41
- m = SPECIAL_FRETS.include?(fret) ? fret.to_s.rjust(2, 0.to_s) : ' '
42
- "#{m}#{' ' if fret.zero?}"
43
- end.join(' ')
40
+ Array.new(@frets + 2) do |fret|
41
+ m = SPECIAL_FRETS.include?(fret) ? fret.to_s.rjust(2, 0.to_s) : ' '
42
+ "#{m}#{' ' if fret.zero?}"
43
+ end.join(' ')
44
44
  end
45
45
 
46
46
  def place_empty(str_i)
47
- Paint['--', HSL.new(180 + str_i * 3,50,30).html]
47
+ Paint['--', HSL.new(180 + str_i * 3, 50, 30).html]
48
48
  end
49
49
 
50
50
  def place_mark(note)
@@ -57,7 +57,7 @@ module Coltrane
57
57
  else raise WrongFlavorError
58
58
  end
59
59
 
60
- base_hue = (180 + note.number * 10) % 360 # + 260
60
+ base_hue = (180 + note.integer * 10) % 360 # + 260
61
61
  Paint[
62
62
  mark,
63
63
  HSL.new(0, 0, 100).html,
@@ -31,7 +31,7 @@ module Coltrane
31
31
 
32
32
  def hint
33
33
  case @flavor
34
- when :marks then ''
34
+ when :marks then ''
35
35
  # when :notes then "(\u266E means the note is natural, not flat nor sharp)"
36
36
  when :intervals
37
37
  <<~DESC
data/lib/coltrane.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'yaml'
3
4
 
4
5
  require 'forwardable'
@@ -10,11 +11,15 @@ require 'coltrane/version'
10
11
  require 'coltrane/errors'
11
12
  require 'coltrane/cadence'
12
13
 
13
- require 'coltrane/note'
14
+ require 'coltrane/frequency'
14
15
  require 'coltrane/pitch'
16
+ require 'coltrane/pitch_class'
17
+ require 'coltrane/note'
15
18
  require 'coltrane/note_set'
16
19
 
17
20
  require 'coltrane/interval'
21
+ require 'coltrane/interval_class'
22
+ require 'coltrane/unordered_interval_class'
18
23
  require 'coltrane/interval_sequence'
19
24
 
20
25
  require 'coltrane/chord_quality'
@@ -31,3 +36,19 @@ require 'coltrane/changes'
31
36
  require 'coltrane/progression'
32
37
 
33
38
  require 'coltrane/mode'
39
+
40
+ # The main module for working with Music Theory
41
+ module Coltrane
42
+ BASE_OCTAVE = 4
43
+ BASE_PITCH_INTEGER = 9
44
+
45
+ def self.tuning=(f)
46
+ @base_tuning = Frequency[f].octave(-4)
47
+ end
48
+
49
+ def self.base_tuning
50
+ @base_tuning
51
+ end
52
+
53
+ @base_tuning = Frequency[440].octave(-4)
54
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coltrane
4
-
5
4
  # Not done yet
6
5
  class Cadence
7
6
  end
@@ -1,9 +1,7 @@
1
- module Changes
2
- def coltrane_changes
3
-
4
- end
1
+ # frozen_string_literal: true
5
2
 
6
- def bird_changes
3
+ module Changes
4
+ def coltrane_changes; end
7
5
 
8
- end
9
- end
6
+ def bird_changes; end
7
+ end
@@ -8,19 +8,19 @@ module Coltrane
8
8
  include ChordSubstitutions
9
9
 
10
10
  def initialize(notes: nil, root_note: nil, quality: nil, name: nil)
11
- if !notes.nil?
11
+ if notes
12
12
  notes = NoteSet[*notes] if notes.is_a?(Array)
13
13
  @notes = notes
14
14
  @root_note = notes.first
15
15
  @quality = ChordQuality.new(notes: notes)
16
- elsif !root_note.nil? && !quality.nil?
16
+ elsif root_note && quality
17
17
  @notes = quality.notes_for(root_note)
18
18
  @root_note = root_note
19
19
  @quality = quality
20
- elsif !name.nil?
20
+ elsif name
21
21
  @root_note, @quality, @notes = parse_from_name(name)
22
22
  else
23
- raise WrongKeywordsError,
23
+ raise WrongKeywordsError,
24
24
  '[notes:] || [root_note:, quality:] || [name:]'
25
25
  end
26
26
  end
@@ -76,13 +76,13 @@ module Coltrane
76
76
  protected
77
77
 
78
78
  def parse_from_name(name)
79
- chord_name, bass = name.split('/')
80
- chord_regex = %r{([A-Z](?:#|b)?)(.*)}
79
+ chord_name, bass = name.match?(/\/9/) ? [name, nil] : name.split('/')
80
+ chord_regex = /([A-Z](?:#|b)?)(.*)/
81
81
  _, root_name, quality_name = chord_name.match(chord_regex).to_a
82
82
  root = Note[root_name]
83
83
  quality = ChordQuality.new(name: quality_name, bass: bass)
84
84
  notes = quality.notes_for(root)
85
- notes << Note[bass] unless bass.nil?
85
+ notes << Note[bass] unless bass.nil?
86
86
  [root, quality, notes]
87
87
  end
88
88
  end
@@ -9,7 +9,7 @@ module Coltrane
9
9
 
10
10
  def self.chord_trie
11
11
  trie = YAML.load_file(
12
- File.expand_path("#{'../'*3}data/qualities.yml", __FILE__)
12
+ File.expand_path("#{'../' * 3}data/qualities.yml", __FILE__)
13
13
  )
14
14
 
15
15
  trie.clone_values from_keys: ['Perfect Unison', 'Major Third'],
@@ -26,7 +26,7 @@ module Coltrane
26
26
  hash ||= chord_trie
27
27
  return quality_names if hash.empty?
28
28
  if hash['name']
29
- quality_names.merge! hash.delete('name') => intervals.map {|n| Interval[n] }
29
+ quality_names[hash.delete('name')] = intervals.map { |n| IntervalClass.new(n) }
30
30
  end
31
31
  hash.reduce(quality_names) do |memo, (interval, values)|
32
32
  memo.merge intervals_per_name(hash: values,
@@ -65,7 +65,7 @@ module Coltrane
65
65
  ints = IntervalSequence.new(intervals: self)
66
66
  chord_sequence.map do |int_sym|
67
67
  next unless interval_name = ints.public_send(int_sym)
68
- ints.delete(Interval[interval_name])
68
+ ints.delete_if { |i| i.cents == IntervalClass.new(interval_name).cents }
69
69
  interval_name
70
70
  end
71
71
  end
@@ -73,31 +73,25 @@ module Coltrane
73
73
  public
74
74
 
75
75
  def get_name
76
- if result = find_chord([*retrieve_chord_intervals].compact)
77
- return result
78
- elsif result = find_chord([*retrieve_chord_intervals(sus2_sequence)].compact)
79
- return result
80
- elsif result = find_chord([*retrieve_chord_intervals(sus4_sequence)].compact)
81
- return result
82
- else
83
- raise ChordNotFoundError
84
- end
76
+ find_chord(retrieve_chord_intervals.compact) ||
77
+ find_chord(retrieve_chord_intervals(sus2_sequence).compact) ||
78
+ find_chord(retrieve_chord_intervals(sus4_sequence).compact) ||
79
+ raise(ChordNotFoundError)
85
80
  end
86
81
 
87
-
88
82
  def suspension_type
89
83
  if has_major_second?
90
84
  'sus2'
91
85
  else has_fourth?
92
- 'sus4'
86
+ 'sus4'
93
87
  end
94
88
  end
95
89
 
96
90
  def initialize(name: nil, notes: nil, bass: nil)
97
- if !name.nil?
91
+ if name
98
92
  @name = bass.nil? ? name : [name, bass].join('/')
99
- super(intervals: NAMES[name])
100
- elsif !notes.nil?
93
+ super(intervals: intervals_from_name(name))
94
+ elsif notes
101
95
  super(notes: notes)
102
96
  @name = get_name
103
97
  else
@@ -106,5 +100,11 @@ module Coltrane
106
100
  end
107
101
 
108
102
  alias to_s name
103
+
104
+ private
105
+
106
+ def intervals_from_name(name)
107
+ NAMES[name] || NAMES["M#{name}"] || raise(ChordNotFoundError)
108
+ end
109
109
  end
110
110
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Coltrane
2
4
  module ChordSubstitutions
3
5
  def tritone_substitution
4
6
  self + Interval.augmented_fourth
5
7
  end
6
8
  end
7
- end
9
+ end
@@ -14,7 +14,7 @@ module Coltrane
14
14
  'Blues Minor' => [3, 2, 1, 1, 3, 2],
15
15
  'Whole Tone' => [2, 2, 2, 2, 2, 2],
16
16
  'Flamenco' => [1, 3, 1, 2, 1, 2, 2],
17
- 'Chromatic' => [1]*12
17
+ 'Chromatic' => [1] * 12
18
18
  }.freeze
19
19
 
20
20
  MODES = {
@@ -49,7 +49,7 @@ module Coltrane
49
49
 
50
50
  # All but the chromatic
51
51
  def standard_scales
52
- SCALES.reject { |k,v| k == 'Chromatic' }
52
+ SCALES.reject { |k, _v| k == 'Chromatic' }
53
53
  end
54
54
 
55
55
  def fetch(name, tone = nil)
@@ -75,17 +75,17 @@ module Coltrane
75
75
 
76
76
  def having_notes(notes)
77
77
  format = { scales: [], results: {} }
78
- OpenStruct.new(
78
+ OpenStruct.new begin
79
79
  standard_scales.each_with_object(format) do |(name, intervals), output|
80
- Note.all.each.map do |tone|
80
+ PitchClass.all.each.map do |tone|
81
81
  scale = new(*intervals, tone: tone, name: scale)
82
82
  output[:results][name] ||= {}
83
- next if output[:results][name].key?(tone.number)
83
+ next if output[:results][name].key?(tone.integer)
84
84
  output[:scales] << scale if scale.include?(notes)
85
- output[:results][name][tone.number] = scale.notes & notes
85
+ output[:results][name][tone.integer] = scale.notes & notes
86
86
  end
87
87
  end
88
- )
88
+ end
89
89
  end
90
90
 
91
91
  def having_chords(*chords)
@@ -21,6 +21,12 @@ module Coltrane
21
21
  end
22
22
  end
23
23
 
24
+ class WrongArgumentsError < BadConstructorError
25
+ def initialize(_msg)
26
+ super 'Wrong argument(s).'
27
+ end
28
+ end
29
+
24
30
  class InvalidNoteError < BadConstructorError
25
31
  def initialize(note)
26
32
  super "#{note} is not a valid note"
@@ -35,7 +41,7 @@ module Coltrane
35
41
 
36
42
  class HasNoNotesError < BadConstructorError
37
43
  def initialize
38
- super "The given object does not respond to :notes, "\
44
+ super 'The given object does not respond to :notes, '\
39
45
  "thereby it can't be used for this operation)"
40
46
  end
41
47
  end
@@ -65,6 +71,25 @@ module Coltrane
65
71
  'https://github.com/pedrozath/coltrane/issues '\
66
72
  end
67
73
  end
74
+
75
+ class InvalidPitchClassError < ColtraneError
76
+ def initialize(arg)
77
+ super "The given frequency(#{arg}) is not considered "\
78
+ 'part of a pitch class'\
79
+ end
80
+ end
81
+
82
+ class InvalidNoteSymbolError < ColtraneError
83
+ def initialize(arg)
84
+ super "The musical notation included an unrecognizable symbol (#{arg})."
85
+ end
86
+ end
87
+
88
+ class InvalidNoteLetterError < ColtraneError
89
+ def initialize(arg)
90
+ super "The musical notation included an unrecognizable letter (#{arg})."
91
+ end
92
+ end
68
93
  end
69
94
 
70
95
  # rubocop:enable Style/Documentation
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coltrane
4
+ class Frequency
5
+ attr_reader :frequency
6
+
7
+ def initialize(frequency)
8
+ @frequency = frequency.to_f
9
+ end
10
+
11
+ class << self
12
+ alias [] new
13
+ end
14
+
15
+ def to_s
16
+ "#{frequency}hz"
17
+ end
18
+
19
+ def to_f
20
+ frequency
21
+ end
22
+
23
+ def octave(n)
24
+ frequency * 2**n
25
+ end
26
+
27
+ def ==(other)
28
+ frequency == (other.is_a?(Frequency) ? other.frequency : other)
29
+ end
30
+
31
+ def octave_up(n = 1)
32
+ octave(n)
33
+ end
34
+
35
+ def octave_down(n = 1)
36
+ octave(-n)
37
+ end
38
+
39
+ def /(other)
40
+ case other
41
+ when Frequency then Interval[1200 * Math.log2(frequency / other.frequency)]
42
+ when Numeric then Frequency[frequency / other]
43
+ end
44
+ end
45
+
46
+ def method_missing(method, *args)
47
+ Frequency[frequency.send(method, args[0].to_f)]
48
+ end
49
+ end
50
+ end
@@ -1,113 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coltrane
4
- # It describes a interval between 2 pitches
4
+ # Interval describe the logarithmic distance between 2 frequencies.
5
+ # It's measured in cents.
5
6
  class Interval
6
- include Multiton
7
- attr_reader :semitones
7
+ attr_reader :cents
8
8
 
9
- INTERVALS = %w[
10
- P1
11
- m2
12
- M2
13
- m3
14
- M3
15
- P4
16
- A4
17
- P5
18
- m6
19
- M6
20
- m7
21
- M7
22
- ].freeze
23
-
24
- def self.split(interval)
25
- interval.scan(/(\w)(\d\d?)/)[0]
26
- end
27
-
28
- def self.full_name(interval)
29
- q,n = split(interval)
30
- "#{q.interval_quality} #{n.to_i.interval_name}"
31
- end
32
-
33
- # Create full names and methods such as major_third? minor_seventh?
34
- # TODO: It's a mess and it really needs a refactor one day
35
- NAMES = INTERVALS.each_with_index.reduce({}) do |memo, (interval, index)|
36
- memo[interval] ||= []
37
- 2.times do |o|
38
- q,i = split(interval)
39
- num = o * 7 + i.to_i
40
- prev_q = split(INTERVALS[(index - 1) % 12])[0]
41
- next_q = split(INTERVALS[(index + 1) % 12])[0]
42
- memo[interval] << full_name("#{q}#{num}")
43
- memo[interval] << full_name("d#{(num - 1 + 1) % 14 + 1}") if next_q.match? /m|P/
44
- next if q == 'A'
45
- memo[interval] << full_name("A#{(num - 1 - 1) % 14 + 1}") if prev_q.match? /M|P/
46
- end
47
- memo
48
- end
49
-
50
- def self.[](arg)
51
- new(case arg
52
- when Interval then arg.semitones
53
- when String then INTERVALS.index(arg) || interval_by_full_name(arg)
54
- when Numeric then arg
55
- end % 12)
9
+ class << self
10
+ alias [] new
56
11
  end
57
12
 
58
- ALL_FULL_NAMES = NAMES.values.flatten
59
-
60
- NAMES.each do |interval_name, full_names|
61
- full_names.each do |the_full_name|
62
- define_method "#{the_full_name.underscore}?" do
63
- name == interval_name
64
- end
65
- self.class.define_method "#{the_full_name.underscore}" do
66
- self[interval_name]
67
- end
68
- end
13
+ def initialize(cents)
14
+ @cents = cents.round
69
15
  end
70
16
 
71
- def initialize(semitones)
72
- @semitones = semitones
17
+ def semitones
18
+ (cents.to_f / 100).round
73
19
  end
74
20
 
75
- private_class_method :new
76
-
77
- def all_full_names
78
- ALL_FULL_NAMES
21
+ def ascending?
22
+ cents < 0
79
23
  end
80
24
 
81
-
82
-
83
- def name
84
- INTERVALS[semitones]
25
+ def descending?
26
+ cents > 0
85
27
  end
86
28
 
87
- def full_name
88
- self.class.full_name(name)
29
+ def ==(other)
30
+ cents == other.cents
89
31
  end
90
32
 
91
- def full_names
92
- NAMES[name]
93
- end
33
+ alias eql? ==
34
+ alias hash cents
94
35
 
95
36
  def +(other)
96
37
  case other
97
- when Numeric then Interval[semitones + other]
98
- when Interval then Interval[semitones + other.semitones]
38
+ when Numeric then Interval[cents + other]
39
+ when Interval then Interval[cents + other.cents]
99
40
  end
100
41
  end
101
42
 
102
- private
103
-
104
- def self.interval_by_full_name(arg)
105
- NAMES.invert.each do |full_names, interval_name|
106
- if full_names.include?(arg)
107
- return INTERVALS.index(interval_name)
108
- end
43
+ def -(other)
44
+ case other
45
+ when Numeric then Interval[cents - other]
46
+ when Interval then Interval[cents - other.cents]
109
47
  end
110
- raise IntervalNotFoundError, arg
111
48
  end
112
49
  end
113
50
  end