coltrane 1.0.26 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a61ddd2b2b38b4b0432cbe36b72aa0247f3ff438f2a0069fa89684c33251c9e3
4
- data.tar.gz: 28d33ca354479eb0bb183b2d70266a6b4950de79cdfac7b71fc76efbe250526a
3
+ metadata.gz: 9c5035abfa591229d5cb00c1a55f95cbb7904fe4cb6fb02c3da0147ddb28713b
4
+ data.tar.gz: c9d0b5de4c65af3f8bcb59b202720eb84c8ad16ce8f422b03536c6382db4e606
5
5
  SHA512:
6
- metadata.gz: f0f3ccd130860eb0ceed09ff135cde9e4e51d3327650121a263203fc4a4c374c40db38416fff38432b3ee8afe939cdff1aeff6f3fc0e3a1c60cf6063ddb21749
7
- data.tar.gz: 3b717105ee23549585989bd926e6e34ec5b18f0e9863f9dfb110b48b7f7e41adaf7f156c7befc1ddc9a8585bafad692646d30956ce7dd598b07c83fd8bad468c
6
+ metadata.gz: 62c750bd90a06738d6e1cbae6cfd15a2c1b45e0b0771f888aaad11a34a48234d28338315a62cf664afb5ba364589e609c77390f3fe1905c721845ad5e1f686d1
7
+ data.tar.gz: 2dc63fb47d4f39df88daf56b24f0d0e229f274fe2807f57295461c4a26f05659932d5b8599f3285073a2a88cbf2fd4e1c3c6ae49d268900b2f643c7aa6645c13
@@ -10,12 +10,38 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
10
10
  - Refactor notes and add pitch frequencies, pitch classes
11
11
 
12
12
 
13
- ## [1.1.25]
13
+ ## [1.1.0]
14
+
15
+ ### Changes
16
+ - The qualities are now more procedural, less hardcoded as possible. The refactor
17
+ has caused the chord list shorter. That may be an issue for some users, and if
18
+ so, it should be addressed in next versions. However, I believe that reduces
19
+ the spam, since we are focusing on the more relevant chords, as they're the
20
+ basis for building more "exotic" chords.
21
+
22
+ - Intervals are now registered and displayed in the english language order.
23
+ Ex: Minor Third was `3m` and now is `m3`.
24
+
25
+ ### Adds
26
+ - A lot of helper methods for obtaining interval information! `Interval` has
27
+ methods like `#minor_third?`, `#major_second?`, which are pretty self-explanatory.
28
+ `#full_name` and `#full_names` are also added, the latter returning intervals that
29
+ are pretty much the same, such as Major Second and Major Ninth.
30
+ Interval sequences have methods like `#has_minor_third?` and `#third`, which will
31
+ return the third it has, no matter if its major, or minor.
32
+
33
+ - Roman Chords and Chord Progressions are finally here! It's a bit experimental yet,
34
+ the latter definitely needs more specs.
35
+
36
+ ### Fixes
37
+ - The changelog
38
+
39
+ ## [1.0.25]
14
40
 
15
41
  ### Fixes
16
42
  - Ruby version on the gemspec
17
43
 
18
- ## [1.1.25]
44
+ ## [1.0.25]
19
45
 
20
46
  ### Removes
21
47
  - Caching, as after some refactoring it made no sense anymore
@@ -24,7 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
24
50
  - Progressions and Roman Chords
25
51
  - Adds Progression command to the CLI
26
52
 
27
- ## [1.1.24]
53
+ ## [1.0.24]
28
54
 
29
55
  ### Adds
30
56
  - Some coloring on guitar output
@@ -37,7 +63,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
37
63
  - Removes Natural sign ~padding~ for guitar instruments
38
64
 
39
65
 
40
- ## [1.1.23]
66
+ ## [1.0.23]
41
67
 
42
68
  ### Adds
43
69
  - A changelog
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- coltrane (1.0.25)
4
+ coltrane (1.1.0)
5
5
  color (~> 1.8)
6
6
  facets (~> 3.1)
7
7
  mercenary (~> 0.3)
@@ -3,12 +3,7 @@
3
3
  require "bundler/setup"
4
4
  require "coltrane"
5
5
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
6
+ require "pry"
8
7
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
8
+ include Coltrane
9
+ Pry.start
@@ -0,0 +1,83 @@
1
+ # NOTE: suspended chords are added programatically
2
+
3
+ Perfect Unison:
4
+ Minor Third:
5
+ Diminished Fifth:
6
+ name: dim
7
+ Diminished Seventh:
8
+ name: dim7
9
+ Minor Ninth:
10
+ name: dim9
11
+ Major Ninth:
12
+ name: dim(b9)
13
+ Minor Seventh:
14
+ name: m7b5
15
+ Minor Ninth:
16
+ name: m7b5b9
17
+ Perfect Eleventh:
18
+ name: m7b5b11
19
+ Major Thirteenth:
20
+ name: m7b5b13
21
+ Major Ninth:
22
+ name: m7b5(9)
23
+ Perfect Fifth:
24
+ name: m
25
+ Major Sixth:
26
+ name: m6
27
+ Minor Seventh:
28
+ name: m7
29
+ Minor Ninth:
30
+ name: m9
31
+ Perfect Eleventh:
32
+ name: m11
33
+ Major Seventh:
34
+ name: m(M7)
35
+ Major Ninth:
36
+ name: m(M9)
37
+ Perfect Eleventh:
38
+ name: m(M11)
39
+ Major Thirteenth:
40
+ name: m(M13)
41
+ Major Third:
42
+ Perfect Fifth:
43
+ name: M
44
+ Major Sixth:
45
+ name: M6
46
+ Major Ninth:
47
+ name: 6/9
48
+ Perfect Eleventh:
49
+ name: 6/9(add11)
50
+ Minor Seventh:
51
+ name: "7"
52
+ Major Ninth:
53
+ name: "9"
54
+ Perfect Eleventh:
55
+ name: "11"
56
+ Major Thirteenth:
57
+ name: "13"
58
+ Major Seventh:
59
+ name: M7
60
+ Major Ninth:
61
+ name: M9
62
+ Perfect Eleventh:
63
+ name: M11
64
+ Major Thirteenth:
65
+ name: M13
66
+ Augmented Fifth:
67
+ name: +
68
+ Minor Seventh:
69
+ name: "+7"
70
+ Major Ninth:
71
+ name: "+9"
72
+ Perfect Eleventh:
73
+ name: "+11"
74
+ Major Thirteenth:
75
+ name: "+13"
76
+ Major Seventh:
77
+ name: +M7
78
+ Major Ninth:
79
+ name: +M9
80
+ Perfect Eleventh:
81
+ name: +M11
82
+ Major Thirteenth:
83
+ name: +M13
@@ -17,7 +17,6 @@ module Coltrane
17
17
  end
18
18
 
19
19
  @chords.each do |chord|
20
- raise ChordNotFoundError unless chord.named?
21
20
  desc = "#{chord.name} chord:"
22
21
  Coltrane::Cli::Notes.new chord.notes, on: on,
23
22
  desc: desc,
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'yaml'
2
3
 
3
4
  require 'forwardable'
4
5
  require 'facets/multiton'
@@ -16,7 +17,6 @@ require 'coltrane/note_set'
16
17
  require 'coltrane/interval'
17
18
  require 'coltrane/interval_sequence'
18
19
 
19
- require 'coltrane/qualities'
20
20
  require 'coltrane/chord_quality'
21
21
 
22
22
  require 'coltrane/chord'
@@ -24,21 +24,15 @@ module Coltrane
24
24
  end
25
25
 
26
26
  def name
27
- return @notes.names.join('/') unless named?
28
- "#{root_note.name}#{quality.name}"
27
+ "#{root_note}#{quality}"
29
28
  end
30
29
 
30
+ alias to_s name
31
+
31
32
  def pretty_name
32
- return @notes.names.join('/') unless named?
33
33
  "#{root_note.pretty_name}#{quality.name}"
34
34
  end
35
35
 
36
- def named?
37
- notes.size >= 3 &&
38
- !root_note.nil? &&
39
- !quality&.name.nil?
40
- end
41
-
42
36
  def intervals
43
37
  IntervalSequence.new(NoteSet.new(notes))
44
38
  end
@@ -4,19 +4,108 @@ module Coltrane
4
4
  # It describe the quality of a chord, like maj7 or dim.
5
5
  class ChordQuality < IntervalSequence
6
6
  attr_reader :name
7
- include Qualities
7
+
8
+ private
9
+
10
+ def self.chord_trie
11
+ trie = YAML.load_file(
12
+ File.expand_path("#{'../'*3}data/qualities.yml", __FILE__)
13
+ )
14
+
15
+ trie.clone_values from_keys: ['Perfect Unison', 'Major Third'],
16
+ to_keys: ['Perfect Unison', 'Major Second'],
17
+ suffix: 'sus2'
18
+
19
+ trie.clone_values from_keys: ['Perfect Unison', 'Major Third'],
20
+ to_keys: ['Perfect Unison', 'Perfect Fourth'],
21
+ suffix: 'sus4'
22
+ trie.deep_dup
23
+ end
24
+
25
+ def self.intervals_per_name(quality_names: {}, intervals: [], hash: nil)
26
+ hash ||= chord_trie
27
+ return quality_names if hash.empty?
28
+ if hash['name']
29
+ quality_names.merge! hash.delete('name') => intervals.map {|n| Interval[n] }
30
+ end
31
+ hash.reduce(quality_names) do |memo, (interval, values)|
32
+ memo.merge intervals_per_name(hash: values,
33
+ quality_names: quality_names,
34
+ intervals: intervals + [interval])
35
+ end
36
+ end
37
+
38
+ NAMES = intervals_per_name
39
+
40
+ def find_chord(given_interval_names, trie: self.class.chord_trie, last_name: nil)
41
+ return if trie.nil?
42
+ if given_interval_names.empty?
43
+ @found = true
44
+ return trie['name']
45
+ end
46
+ interval = given_interval_names.shift
47
+ new_trie = trie[interval]
48
+ find_chord given_interval_names, last_name: (trie['name'] || last_name),
49
+ trie: new_trie
50
+ end
51
+
52
+ def normal_sequence
53
+ %i[unison third! fifth sixth! seventh ninth eleventh! thirteenth]
54
+ end
55
+
56
+ def sus2_sequence
57
+ %i[unison second! fifth sixth! seventh ninth eleventh! thirteenth]
58
+ end
59
+
60
+ def sus4_sequence
61
+ %i[unison fourth! fifth sixth! seventh ninth eleventh! thirteenth]
62
+ end
63
+
64
+ def retrieve_chord_intervals(chord_sequence = normal_sequence)
65
+ ints = IntervalSequence.new(intervals: self)
66
+ chord_sequence.map do |int_sym|
67
+ next unless interval_name = ints.public_send(int_sym)
68
+ ints.delete(Interval[interval_name])
69
+ interval_name
70
+ end
71
+ end
72
+
73
+ public
74
+
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
+ binding.pry
84
+ raise ChordNotFoundError
85
+ end
86
+ end
87
+
88
+
89
+ def suspension_type
90
+ if has_major_second?
91
+ 'sus2'
92
+ else has_fourth?
93
+ 'sus4'
94
+ end
95
+ end
8
96
 
9
97
  def initialize(name: nil, notes: nil)
10
98
  if !name.nil?
11
- raise ChordNotFoundError unless (intervals = CHORD_QUALITIES[name])
12
99
  @name = name
13
- super(intervals: intervals)
100
+ super(intervals: NAMES[name])
14
101
  elsif !notes.nil?
15
102
  super(notes: notes)
16
- @name = CHORD_QUALITIES.key(intervals_semitones)
103
+ @name = get_name
17
104
  else
18
105
  raise WrongKeywordsError, '[name:] || [notes:]'
19
106
  end
20
107
  end
108
+
109
+ alias to_s name
21
110
  end
22
111
  end
@@ -47,6 +47,11 @@ module Coltrane
47
47
  SCALES.keys
48
48
  end
49
49
 
50
+ # All but the chromatic
51
+ def standard_scales
52
+ SCALES.reject { |k,v| k == 'Chromatic' }
53
+ end
54
+
50
55
  def fetch(name, tone = nil)
51
56
  Coltrane::Scale.public_send(name, tone)
52
57
  end
@@ -71,7 +76,7 @@ module Coltrane
71
76
  def having_notes(notes)
72
77
  format = { scales: [], results: {} }
73
78
  OpenStruct.new(
74
- SCALES.each_with_object(format) do |(name, intervals), output|
79
+ standard_scales.each_with_object(format) do |(name, intervals), output|
75
80
  Note.all.each.map do |tone|
76
81
  scale = new(*intervals, tone: tone, name: scale)
77
82
  output[:results][name] ||= {}
@@ -56,6 +56,15 @@ module Coltrane
56
56
  "\n\nA tip tho: always include the letter M for major"
57
57
  end
58
58
  end
59
+
60
+ class IntervalNotFoundError < ColtraneError
61
+ def initialize(arg)
62
+ super "The interval \"#{arg}\" that was provided wasn't found. "\
63
+ "If you're sure this interval exists, "\
64
+ "would you mind to suggest it's inclusion here: "\
65
+ 'https://github.com/pedrozath/coltrane/issues '\
66
+ end
67
+ end
59
68
  end
60
69
 
61
70
  # rubocop:enable Style/Documentation
@@ -3,40 +3,111 @@
3
3
  module Coltrane
4
4
  # It describes a interval between 2 pitches
5
5
  class Interval
6
+ include Multiton
6
7
  attr_reader :semitones
7
8
 
8
- NAMES = %w[
9
- 1P
10
- 2m
11
- 2M
12
- 3m
13
- 3M
14
- 4P
15
- 4A
16
- 5P
17
- 6m
18
- 6M
19
- 7m
20
- 7M
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
21
22
  ].freeze
22
23
 
23
- def initialize(arg)
24
- @semitones = (case arg
25
- when Interval then arg.semitones
26
- when String then NAMES.index(arg)
27
- when Numeric then arg
28
- end) % 12
24
+ def self.split(interval)
25
+ interval.scan(/(\w)(\d\d?)/)[0]
29
26
  end
30
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)
56
+ end
57
+
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
69
+ end
70
+
71
+ def initialize(semitones)
72
+ @semitones = semitones
73
+ end
74
+
75
+ private_class_method :new
76
+
77
+ def all_full_names
78
+ ALL_FULL_NAMES
79
+ end
80
+
81
+
82
+
31
83
  def name
32
- NAMES[semitones]
84
+ INTERVALS[semitones]
85
+ end
86
+
87
+ def full_name
88
+ self.class.full_name(name)
89
+ end
90
+
91
+ def full_names
92
+ NAMES[name]
33
93
  end
34
94
 
35
95
  def +(other)
36
96
  case other
37
- when Numeric then Interval.new(semitones + other)
38
- when Interval then Interval.new(semitones + other.semitones)
97
+ when Numeric then Interval[semitones + other]
98
+ when Interval then Interval[semitones + other.semitones]
99
+ end
100
+ end
101
+
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
39
109
  end
110
+ raise IntervalNotFoundError, arg
40
111
  end
41
112
  end
42
113
  end
@@ -6,14 +6,50 @@ module Coltrane
6
6
  extend Forwardable
7
7
  attr_reader :intervals
8
8
 
9
- def_delegators :@intervals, :map, :each, :[], :size, :reduce
9
+ def_delegators :@intervals, :map, :each, :[], :size,
10
+ :reduce, :delete
11
+
12
+ Interval::ALL_FULL_NAMES.each do |full_name|
13
+ define_method "has_#{full_name.underscore}?" do
14
+ !!(intervals.detect {|i| i.public_send("#{full_name.underscore}?")})
15
+ end
16
+ end
17
+
18
+ (1..15).each do |i|
19
+ # defines methods like :fifth, :third, eleventh:
20
+ define_method i.interval_name.underscore do
21
+ priority = send("#{i.interval_name.underscore}!")
22
+ return priority unless priority.nil?
23
+ @intervals.each do |ix|
24
+ ix.full_names.detect do |ixx|
25
+ return ixx if ixx.match(/#{i.interval_name}/)
26
+ end
27
+ end
28
+ nil
29
+ end
30
+
31
+ define_method "#{i.interval_name.underscore}!" do
32
+ @intervals.each do |ix|
33
+ ix.full_names.detect do |ixx|
34
+ next if ixx.match(/Diminished|Augmented/)
35
+ return ixx if ixx.match? /#{i.interval_name}/
36
+ end
37
+ end
38
+ nil
39
+ end
40
+
41
+ # defines methods like :has_fifth?, :has_third?, has_eleventh?:
42
+ define_method "has_#{i.interval_name.underscore}?" do
43
+ !!@intervals.detect {|ix| ix.full_name.match(/#{i.interval_name}/) }
44
+ end
45
+ end
10
46
 
11
47
  def initialize(notes: nil, intervals: nil, distances: nil)
12
48
  if !notes.nil?
13
49
  notes = NoteSet[*notes] if notes.is_a?(Array)
14
50
  @intervals = intervals_from_notes(notes)
15
51
  elsif !intervals.nil?
16
- @intervals = intervals.map { |i| Interval.new(i) }
52
+ @intervals = intervals.map { |i| Interval[i] }
17
53
  elsif !distances.nil?
18
54
  @distances = distances
19
55
  @intervals = intervals_from_distances(distances)
@@ -32,6 +68,16 @@ module Coltrane
32
68
  end + [12 - intervals_semitones.last]
33
69
  end
34
70
 
71
+ def names
72
+ intervals.map(&:name)
73
+ end
74
+
75
+ def has?(interval_name)
76
+ @intervals.include?(Interval[interval_name])
77
+ end
78
+
79
+ alias interval_names names
80
+
35
81
  def all
36
82
  intervals
37
83
  end
@@ -68,8 +114,6 @@ module Coltrane
68
114
  end
69
115
  end
70
116
 
71
- def quality; end
72
-
73
117
  def intervals_semitones
74
118
  map(&:semitones)
75
119
  end
@@ -78,6 +122,10 @@ module Coltrane
78
122
  map(&:name)
79
123
  end
80
124
 
125
+ def full_names
126
+ map(&:full_name)
127
+ end
128
+
81
129
  def notes_for(root_note)
82
130
  NoteSet[
83
131
  *intervals.reduce([]) do |memo, interval|
@@ -86,10 +134,17 @@ module Coltrane
86
134
  ]
87
135
  end
88
136
 
137
+ def &(other)
138
+ case other
139
+ when Array then intervals & other
140
+ when IntervalSequence then intervals & other.semitones
141
+ end
142
+ end
143
+
89
144
  private
90
145
 
91
146
  def intervals_from_distances(distances)
92
- distances[0..-2].reduce([Interval.new(0)]) do |memo, d|
147
+ distances[0..-2].reduce([Interval[0]]) do |memo, d|
93
148
  memo + [memo.last + d]
94
149
  end
95
150
  end
@@ -7,6 +7,7 @@ module Coltrane
7
7
 
8
8
  attr_reader :name, :number
9
9
  alias id number
10
+ alias to_s name
10
11
 
11
12
  NOTES = {
12
13
  'C' => 0,
@@ -78,7 +79,7 @@ module Coltrane
78
79
  def -(other)
79
80
  case other
80
81
  when Numeric then Note[number - other]
81
- when Note then Interval.new(other.number - number)
82
+ when Note then Interval[other.number - number]
82
83
  end
83
84
  end
84
85
 
@@ -21,7 +21,7 @@ module Coltrane
21
21
  @notes =
22
22
  case arg
23
23
  when NoteSet then arg.notes
24
- when Array then arg.map { |n| n.is_a?(Note) ? n : Note[n] }
24
+ when Array then arg.map { |n| n.is_a?(Note) ? n : Note[n] }.uniq
25
25
  else raise InvalidNotesError, arg
26
26
  end
27
27
  end
@@ -46,6 +46,10 @@ module Coltrane
46
46
  map(&:name)
47
47
  end
48
48
 
49
+ def numbers
50
+ map(&:number)
51
+ end
52
+
49
53
  def transpose_to(note_name)
50
54
  transpose_by(root_note.interval_to(note_name).number)
51
55
  end
@@ -6,12 +6,54 @@ module Coltrane
6
6
  # Ex: Progression.new('I-IV-V', key: 'Am')
7
7
  class Progression
8
8
  extend ClassicProgressions
9
- attr_reader :scale, :chords
9
+ attr_reader :scale, :chords, :notation
10
+
11
+ def self.find(*chords)
12
+ # chords.map! { |c| Chord.new(name: c) } if chords[0].is_a?(String)
13
+ # scales = Scale.having_chords(*chords).scales.map(&:pretty_name)
14
+ # scales.reduce([]) do |memo, scale|
15
+ # memo
16
+ # end
17
+ end
18
+
19
+ def initialize(notation=nil, chords: nil, roman_chords: nil, key: nil, scale: nil)
20
+ if notation.nil? && chords.nil? && roman_chords.nil? || key.nil? && scale.nil?
21
+ raise WrongKeywordsError,
22
+ '[chords:, [scale: || key:]] '\
23
+ '[roman_chords:, [scale: || key:]] '\
24
+ '[notation:, [scale: || key:]] '\
25
+ end
10
26
 
11
- def initialize(roman_notation, roman_chords: [], key: nil, scale: nil)
12
27
  @scale = scale || Scale.from_key(key)
13
- rchords = roman_chords.any? ? roman_chords : roman_notation.split('-')
14
- @chords = rchords.map {|c| RomanChord.new(c, scale: @scale).chord }
28
+ @chords =
29
+ if !chords.nil?
30
+ chords
31
+ elsif !roman_chords.nil?
32
+ roman_chords.map(&:chord)
33
+ elsif !notation.nil?
34
+ @notation = notation
35
+ notation.split('-').map {|c| RomanChord.new(c, scale: @scale).chord }
36
+ end
37
+ end
38
+
39
+ def interval_sequence
40
+ @interval_sequence ||= IntervalSequence(notes: @root_notes)
41
+ end
42
+
43
+ def root_notes
44
+ @root_notes ||= @chords.map(&:root_note)
45
+ end
46
+
47
+ def roman_chords
48
+ @roman_chords ||= RomanChord.new()
49
+ end
50
+
51
+ def notation
52
+ @notation || @scale
53
+ end
54
+
55
+ def rotate
56
+
15
57
  end
16
58
  end
17
59
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coltrane
4
- attr_reader :degree, :quality
5
-
6
4
  # This class deals with chords in roman notation. Ex: IVº.
7
5
  class RomanChord
8
6
  DIGITS = %w[I II III IV V VI VII].freeze
@@ -17,52 +15,92 @@ module Coltrane
17
15
  %w[ø m7b5]
18
16
  ]
19
17
 
20
- def initialize(notation, key: nil, scale: nil)
21
- @scale = scale || Scale.from_key(key)
22
- notation = notation.match(NOTATION_REGEX).named_captures
23
- notation['quality'] =
24
- NOTATION_SUBSTITUTIONS.reduce(notation['quality']) do |memo, subs|
25
- break memo if memo.empty?
26
- memo.gsub(*subs)
27
- end
28
-
29
- @notation = notation
18
+ def initialize(notation=nil, chord: nil, key: nil, scale: nil)
19
+ if notation.nil? && chord.nil? || key.nil? && scale.nil?
20
+ raise WrongKeywordsError,
21
+ '[notation, [scale: || key:]] '\
22
+ '[chord:, [scale: || key:]] '\
23
+ end
24
+ @scale = scale || Scale.from_key(key)
25
+ if !notation.nil?
26
+ @notation = notation
27
+ elsif !chord.nil?
28
+ @chord = chord.is_a?(String) ? Chord.new(name: chord) : chord
29
+ end
30
30
  end
31
31
 
32
32
  def degree
33
- d = @notation['degree']
33
+ return @scale.degree_of_note(root_note) unless @chord.nil?
34
+ d = regexed_notation['degree']
34
35
  @flats = d.count('b')
35
36
  d = d.delete('b')
36
37
  @degree ||= DIGITS.index(d.upcase) + 1
37
38
  end
38
39
 
39
- def quality_name
40
- [
41
- minor_notation,
42
- @notation['quality']
43
- ].join
44
- end
45
-
46
- def minor_notation
47
- return 'm' if !@notation['quality'].match?((/dim|m7b5/)) && !upcase?
40
+ def roman_numeral
41
+ r = DIGITS[degree]
42
+ minor? ? r.downcase : r
48
43
  end
49
44
 
50
45
  def upcase?
51
- !!(@notation['degree'][0].match /[[:upper:]]/)
46
+ !!(regexed_notation['degree'][0].match /[[:upper:]]/)
52
47
  end
53
48
 
54
49
  def chord
55
- Chord.new root_note: root_note,
56
- quality: quality
50
+ @chord ||= Chord.new root_note: root_note,
51
+ quality: quality
52
+ end
53
+
54
+ def minor?
55
+ quality.has_minor_third?
56
+ end
57
+
58
+ def major?
59
+ quality.has_major_third?
60
+ end
61
+
62
+ def quality_name
63
+ return @chord.quality.name unless @chord.nil?
64
+ q = normalize_quality_name(regexed_notation['quality'])
65
+ minor = 'm' if !q.match?((/dim|m7b5/)) && !upcase?
66
+ q = [minor, q].join
67
+ q.empty? ? 'M' : q
57
68
  end
58
69
 
59
70
  def quality
60
- q = quality_name
61
- ChordQuality.new(name: (q.size.zero? ? 'M' : q))
71
+ return @chord.quality unless @chord.nil?
72
+ ChordQuality.new(name: quality_name) if quality_name
62
73
  end
63
74
 
64
75
  def root_note
76
+ return @chord.root_note unless @chord.nil?
65
77
  @scale[degree] - @flats
66
78
  end
79
+
80
+ def notation
81
+ q = case quality_name
82
+ when 'm', 'M' then ''
83
+ when 'm7', 'M' then '7'
84
+ else quality_name
85
+ end
86
+
87
+ @notation ||= [
88
+ roman_numeral,
89
+ q,
90
+ ].join
91
+ end
92
+
93
+ private
94
+
95
+ def regexed_notation
96
+ @regexed_notation ||= @notation.match(NOTATION_REGEX).named_captures
97
+ end
98
+
99
+ def normalize_quality_name(quality_name)
100
+ NOTATION_SUBSTITUTIONS.reduce(quality_name) do |memo, subs|
101
+ break memo if memo.empty?
102
+ memo.gsub(*subs)
103
+ end
104
+ end
67
105
  end
68
106
  end
@@ -59,8 +59,7 @@ module Coltrane
59
59
  end
60
60
 
61
61
  def degree_of_note(note)
62
- note = notes.map(&:name).index(note.name)
63
- return note + 1 unless note.nil?
62
+ notes.index(note)
64
63
  end
65
64
 
66
65
  def &(other)
@@ -90,8 +89,13 @@ module Coltrane
90
89
  def tertians(n = 3)
91
90
  degrees.size.times.reduce([]) do |memo, d|
92
91
  ns = NoteSet[ *Array.new(n) { |i| notes[(d + (i * 2)) % size] } ]
93
- chord = Chord.new(notes: ns)
94
- chord.named? ? memo + [chord] : memo
92
+ begin
93
+ chord = Chord.new(notes: ns)
94
+ rescue ChordNotFoundError
95
+ memo
96
+ else
97
+ memo + [chord]
98
+ end
95
99
  end
96
100
  end
97
101
 
@@ -112,16 +116,17 @@ module Coltrane
112
116
  end
113
117
 
114
118
  def all_chords
115
- (3..size).reduce([]) { |memo, s| memo + chords(s) }
119
+ chords
116
120
  end
117
121
 
118
- def chords(size)
122
+ def chords(size = 3..12)
123
+ size = (size..size) if size.is_a?(Integer)
119
124
  included_names = []
120
- scale_rotations = interval_sequence.inversions.map(&:intervals_semitones)
121
- ChordQuality::CHORD_QUALITIES.reduce([]) do |memo1, (qname, qintervals)|
122
- next memo1 if qintervals.size != size
125
+ scale_rotations = interval_sequence.inversions
126
+ ChordQuality.intervals_per_name.reduce([]) do |memo1, (qname, qintervals)|
127
+ next memo1 unless size.include?(qintervals.size)
123
128
  memo1 + scale_rotations.each_with_index.reduce([]) do |memo2, (rot, index)|
124
- if (rot & qintervals).size == size
129
+ if (rot & qintervals).size == qintervals.size
125
130
  memo2 + [ Chord.new(root_note: degree(index+1),
126
131
  quality: ChordQuality.new(name: qname)) ]
127
132
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coltrane
4
- VERSION = '1.0.26'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -12,4 +12,91 @@ class String
12
12
  word.downcase!
13
13
  word
14
14
  end
15
+
16
+ def interval_quality
17
+ {
18
+ 'P' => 'Perfect',
19
+ 'm' => 'Minor',
20
+ 'M' => 'Major',
21
+ 'A' => 'Augmented',
22
+ 'd' => 'Diminished'
23
+ }[self]
24
+ end
25
+ end
26
+
27
+ class Integer
28
+ def interval_name
29
+ {
30
+ 1 => 'Unison',
31
+ 2 => 'Second',
32
+ 3 => 'Third',
33
+ 4 => 'Fourth',
34
+ 5 => 'Fifth',
35
+ 6 => 'Sixth',
36
+ 7 => 'Seventh',
37
+ 8 => 'Octave',
38
+ 9 => 'Ninth',
39
+ 10 => 'Tenth',
40
+ 11 => 'Eleventh',
41
+ 12 => 'Twelfth',
42
+ 13 => 'Thirteenth',
43
+ 14 => 'Fourteenth',
44
+ 15 => 'Double Octave'
45
+ }[self % 16]
46
+ end
15
47
  end
48
+
49
+ class Hash
50
+ def clone_values(from_keys: nil, to_keys: nil, suffix: nil, branch_a: nil, branch_b: nil)
51
+ branch_a ||= self.dig(*from_keys)
52
+ if branch_b.nil?
53
+ self.create_branch!(*to_keys)
54
+ branch_b = self.dig(*to_keys)
55
+ end
56
+
57
+ branch_a.each do |key, val|
58
+ if val.is_a?(Hash)
59
+ clone_values branch_a: branch_a[key],
60
+ branch_b: (branch_b[key] ||= {}),
61
+ suffix: suffix
62
+ else
63
+ branch_b[key] = "#{val.dup}#{suffix}"
64
+ end
65
+ end
66
+ end
67
+
68
+ def create_branch!(*keys)
69
+ return nil if keys.empty?
70
+ key = keys.shift
71
+ (self[key] ||= {}).create_branch!(*keys)
72
+ end
73
+
74
+ def deep_dup
75
+ dup_hash = {}
76
+ self.each do |k,v|
77
+ if v.is_a?(Hash)
78
+ dup_hash[k] = v.deep_dup
79
+ else
80
+ dup_hash[k] = v.dup
81
+ end
82
+ end
83
+ dup_hash
84
+ end
85
+ end
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coltrane
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.26
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Maciel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-02-08 00:00:00.000000000 Z
11
+ date: 2018-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: facets
@@ -146,6 +146,7 @@ files:
146
146
  - bin/thor
147
147
  - bin/tilt
148
148
  - coltrane.gemspec
149
+ - data/qualities.yml
149
150
  - db/cache.sqlite3
150
151
  - db/cache_test.sqlite3
151
152
  - db/config.yml
@@ -189,7 +190,6 @@ files:
189
190
  - lib/coltrane/piano_representation.rb
190
191
  - lib/coltrane/pitch.rb
191
192
  - lib/coltrane/progression.rb
192
- - lib/coltrane/qualities.rb
193
193
  - lib/coltrane/roman_chord.rb
194
194
  - lib/coltrane/scale.rb
195
195
  - lib/coltrane/version.rb
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Coltrane
4
- module Qualities
5
- CHORD_QUALITIES = {
6
- '5' => [0, 7],
7
- 'Msus2' => [0, 2, 7],
8
- 'dim' => [0, 3, 6],
9
- 'm' => [0, 3, 7],
10
- 'm#5' => [0, 3, 8],
11
- 'Mb5' => [0, 4, 6],
12
- 'M' => [0, 4, 7],
13
- 'M#5' => [0, 4, 8],
14
- 'Mb6' => [0, 4, 8],
15
- '7ndim5' => [0, 4, 10],
16
- 'Msus4' => [0, 5, 7],
17
- 'mb6b9' => [0, 1, 3, 8],
18
- 'addb9' => [0, 1, 4, 7],
19
- 'madd9' => [0, 2, 3, 7],
20
- 'Madd9' => [0, 2, 4, 7],
21
- 'sus24' => [0, 2, 5, 7],
22
- 'M#5add9' => [0, 2, 4, 8],
23
- '9ndim5' => [0, 2, 4, 10],
24
- '+add#9' => [0, 3, 4, 8],
25
- 'madd4' => [0, 3, 5, 7],
26
- '4' => [0, 3, 5, 10],
27
- 'dim7' => [0, 3, 6, 9],
28
- 'm7' => [0, 3, 7, 10],
29
- 'mM7' => [0, 3, 7, 11],
30
- 'm7#5' => [0, 3, 8, 10],
31
- '7b5' => [0, 4, 6, 10],
32
- '7' => [0, 4, 7, 10],
33
- '7#5' => [0, 4, 8, 10],
34
- '7b13' => [0, 4, 8, 10],
35
- 'M7#5' => [0, 4, 8, 11],
36
- 'M7b6' => [0, 4, 8, 11],
37
- 'M7b5' => [0, 4, 6, 11],
38
- 'M7#5sus4' => [0, 5, 8, 11],
39
- '7sus4' => [0, 5, 7, 10],
40
- '7#5sus4' => [0, 5, 8, 10],
41
- 'M7sus4' => [0, 5, 7, 11],
42
- 'M6' => [0, 4, 7, 9],
43
- 'm7b5' => [0, 3, 6, 10],
44
- 'M7' => [0, 4, 7, 11],
45
- 'maj7' => [0, 4, 7, 11],
46
- 'mb6M7' => [0, 3, 8, 11],
47
- 'dimM7' => [0, 3, 6, 11],
48
- 'm6' => [0, 3, 5, 7, 9],
49
- 'm6/9' => [0, 2, 3, 7, 9],
50
- 'M6/9' => [0, 2, 4, 7, 9],
51
- 'M6#11' => [0, 4, 6, 7, 9],
52
- 'm7add11' => [0, 3, 5, 7, 10],
53
- 'dim7M7' => [0, 3, 6, 9, 11],
54
- 'm9' => [0, 2, 3, 7, 10],
55
- 'm9#5' => [0, 2, 3, 8, 10],
56
- 'm9b5' => [0, 2, 3, 6, 10],
57
- 'mM7b6' => [0, 3, 7, 8, 11],
58
- 'mM9' => [0, 2, 3, 7, 11],
59
- 'M7b9' => [0, 1, 4, 7, 11],
60
- 'M9' => [0, 2, 4, 7, 11],
61
- 'M9#5' => [0, 2, 4, 8, 11],
62
- 'M9#5sus4' => [0, 2, 5, 8, 11],
63
- 'M9b5' => [0, 2, 4, 6, 11],
64
- 'M9sus4' => [0, 2, 5, 7, 11],
65
- 'M7#11' => [0, 4, 6, 7, 11],
66
- '7#9' => [0, 3, 4, 7, 10],
67
- '7#11' => [0, 4, 6, 7, 10],
68
- '11' => [0, 2, 5, 7, 10],
69
- '11b9' => [0, 1, 5, 7, 10],
70
- '13ndim5' => [0, 2, 4, 9, 10],
71
- '7#5#9' => [0, 3, 4, 8, 10],
72
- '7#5b9' => [0, 1, 4, 8, 10],
73
- '7add6' => [0, 4, 7, 9, 10],
74
- '7b6' => [0, 4, 7, 8, 10],
75
- '7b9' => [0, 1, 4, 7, 10],
76
- '7sus4b9' => [0, 1, 5, 7, 10],
77
- '9' => [0, 2, 4, 7, 10],
78
- '9#5' => [0, 2, 4, 8, 10],
79
- '9b13' => [0, 2, 4, 8, 10],
80
- '9b5' => [0, 2, 4, 6, 10],
81
- '9sus4' => [0, 2, 5, 7, 10],
82
- 'M6/9#11' => [0, 2, 4, 6, 7, 9],
83
- '6/9#11' => [0, 2, 4, 6, 7, 9],
84
- '9#5#11' => [0, 2, 4, 6, 8, 10],
85
- 'Blues' => [0, 3, 5, 6, 7, 10],
86
- 'm11' => [0, 2, 3, 5, 7, 10],
87
- 'm11#5' => [0, 2, 3, 5, 8, 10],
88
- 'm11b5' => [0, 2, 3, 5, 6, 10],
89
- '13b5' => [0, 2, 4, 6, 9, 10],
90
- '13b9' => [0, 1, 4, 7, 9, 10],
91
- '13sus4' => [0, 2, 5, 7, 9, 10],
92
- '7#11b13' => [0, 4, 6, 7, 8, 10],
93
- '7#5b9#11' => [0, 1, 4, 6, 8, 10],
94
- '7#9#11' => [0, 3, 4, 6, 7, 10],
95
- '7#9b13' => [0, 3, 4, 7, 8, 10],
96
- '13#9' => [0, 3, 4, 7, 9, 10],
97
- '13' => [0, 2, 4, 7, 9, 10],
98
- '7b9#11' => [0, 1, 4, 6, 7, 10],
99
- '7b9#9' => [0, 1, 3, 4, 7, 10],
100
- '7b9b13' => [0, 1, 4, 7, 8, 10],
101
- '7sus4b9b13' => [0, 1, 5, 7, 8, 10],
102
- '9#11' => [0, 2, 4, 6, 7, 10],
103
- 'M13' => [0, 2, 4, 7, 9, 11],
104
- 'M7#9#11' => [0, 3, 4, 6, 7, 11],
105
- 'M7add13' => [0, 2, 4, 7, 9, 11],
106
- 'M9#11' => [0, 2, 4, 6, 7, 11],
107
- 'mM9b6' => [0, 2, 3, 7, 8, 11],
108
- '7b9b13#11' => [0, 1, 4, 6, 7, 8, 10],
109
- '13b9#11' => [0, 1, 4, 6, 7, 9, 10],
110
- '9#11b13' => [0, 2, 4, 6, 7, 8, 10],
111
- '13#11' => [0, 2, 4, 6, 7, 9, 10],
112
- 'M13#11' => [0, 2, 4, 6, 7, 9, 11],
113
- 'm13' => [0, 2, 3, 5, 7, 9, 10],
114
- '13#9#11' => [0, 3, 4, 6, 7, 9, 10],
115
- '7#9#11b13' => [0, 3, 4, 6, 7, 8, 10]
116
- }.freeze
117
- end
118
- end