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
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