coltrane 1.0.26 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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