coltrane 1.2.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -1
- data/Gemfile +3 -0
- data/Gemfile.lock +48 -3
- data/Rakefile +1 -1
- data/bin/erubis +12 -0
- data/bin/flay +29 -0
- data/bin/gitlab +29 -0
- data/bin/httparty +29 -0
- data/bin/pronto +29 -0
- data/bin/ruby_parse +29 -0
- data/bin/ruby_parse_extract_error +29 -0
- data/bin/thor +12 -0
- data/exe/coltrane +8 -6
- data/lib/cli/guitar.rb +7 -7
- data/lib/cli/representation.rb +1 -1
- data/lib/coltrane.rb +22 -1
- data/lib/coltrane/cadence.rb +0 -1
- data/lib/coltrane/changes.rb +5 -7
- data/lib/coltrane/chord.rb +7 -7
- data/lib/coltrane/chord_quality.rb +17 -17
- data/lib/coltrane/chord_substitutions.rb +3 -1
- data/lib/coltrane/classic_scales.rb +7 -7
- data/lib/coltrane/errors.rb +26 -1
- data/lib/coltrane/frequency.rb +50 -0
- data/lib/coltrane/interval.rb +23 -86
- data/lib/coltrane/interval_class.rb +106 -0
- data/lib/coltrane/interval_sequence.rb +14 -13
- data/lib/coltrane/notable_progressions.rb +8 -3
- data/lib/coltrane/note.rb +44 -73
- data/lib/coltrane/note_set.rb +4 -4
- data/lib/coltrane/pitch.rb +43 -22
- data/lib/coltrane/pitch_class.rb +113 -0
- data/lib/coltrane/progression.rb +6 -9
- data/lib/coltrane/roman_chord.rb +14 -14
- data/lib/coltrane/scale.rb +8 -10
- data/lib/coltrane/unordered_interval_class.rb +7 -0
- data/lib/coltrane/version.rb +1 -1
- data/lib/coltrane_instruments.rb +4 -0
- data/lib/coltrane_instruments/guitar.rb +7 -0
- data/lib/coltrane_instruments/guitar/base.rb +14 -0
- data/lib/coltrane_instruments/guitar/chord.rb +41 -0
- data/lib/coltrane_instruments/guitar/note.rb +8 -0
- data/lib/coltrane_instruments/guitar/string.rb +8 -0
- data/lib/core_ext.rb +16 -27
- metadata +18 -2
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coltrane
|
4
|
+
# Interval class here is not related to the Object Oriented Programming context
|
5
|
+
# but to the fact that there is a class of intervals that can all be categorized
|
6
|
+
# as having the same quality.
|
7
|
+
#
|
8
|
+
# This class in specific still takes into account the order of intervals.
|
9
|
+
# C to D is a major second, but D to C is a minor seventh.
|
10
|
+
class IntervalClass < Interval
|
11
|
+
INTERVALS = %w[P1 m2 M2 m3 M3 P4 A4 P5 m6 M6 m7 M7].freeze
|
12
|
+
|
13
|
+
def self.split(interval)
|
14
|
+
interval.scan(/(\w)(\d\d?)/)[0]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.full_name(interval)
|
18
|
+
q, n = split(interval)
|
19
|
+
"#{q.interval_quality} #{n.to_i.interval_name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create full names and methods such as major_third? minor_seventh?
|
23
|
+
# TODO: It's a mess and it really needs a refactor someday
|
24
|
+
NAMES = INTERVALS.each_with_index.each_with_object({}) do |(interval, index), memo|
|
25
|
+
memo[interval] ||= []
|
26
|
+
2.times do |o|
|
27
|
+
q, i = split(interval)
|
28
|
+
num = o * 7 + i.to_i
|
29
|
+
prev_q = split(INTERVALS[(index - 1) % 12])[0]
|
30
|
+
next_q = split(INTERVALS[(index + 1) % 12])[0]
|
31
|
+
memo[interval] << full_name("#{q}#{num}")
|
32
|
+
memo[interval] << full_name("d#{(num - 1 + 1) % 14 + 1}") if next_q.match? /m|P/
|
33
|
+
next if q == 'A'
|
34
|
+
memo[interval] << full_name("A#{(num - 1 - 1) % 14 + 1}") if prev_q.match? /M|P/
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
ALL_FULL_NAMES = NAMES.values.flatten
|
39
|
+
|
40
|
+
NAMES.each do |interval_name, full_names|
|
41
|
+
full_names.each do |the_full_name|
|
42
|
+
define_method "#{the_full_name.underscore}?" do
|
43
|
+
name == interval_name
|
44
|
+
end
|
45
|
+
IntervalClass.class.define_method the_full_name.underscore.to_s do
|
46
|
+
IntervalClass.new(interval_name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(arg)
|
52
|
+
super case arg
|
53
|
+
when Interval then arg.semitones
|
54
|
+
when String
|
55
|
+
INTERVALS.index(arg) || self.class.interval_by_full_name(arg)
|
56
|
+
when Numeric then arg
|
57
|
+
else raise WrongArgumentsError
|
58
|
+
end % 12 * 100
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.[](semis)
|
62
|
+
new semis
|
63
|
+
end
|
64
|
+
|
65
|
+
def all_full_names
|
66
|
+
self.class.all_full_names
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.all_full_names
|
70
|
+
ALL_FULL_NAMES
|
71
|
+
end
|
72
|
+
|
73
|
+
def ==(other)
|
74
|
+
(cents % 12) == (other.cents % 12)
|
75
|
+
end
|
76
|
+
|
77
|
+
def name
|
78
|
+
INTERVALS[semitones % 12]
|
79
|
+
end
|
80
|
+
|
81
|
+
def full_name
|
82
|
+
self.class.full_name(name)
|
83
|
+
end
|
84
|
+
|
85
|
+
def full_names
|
86
|
+
NAMES[name]
|
87
|
+
end
|
88
|
+
|
89
|
+
def +(other)
|
90
|
+
IntervalClass[semitones + other]
|
91
|
+
end
|
92
|
+
|
93
|
+
def -(other)
|
94
|
+
IntervalClass[semitones - other]
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def self.interval_by_full_name(arg)
|
100
|
+
NAMES.invert.each do |full_names, interval_name|
|
101
|
+
return INTERVALS.index(interval_name) if full_names.include?(arg)
|
102
|
+
end
|
103
|
+
raise IntervalNotFoundError, arg
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -7,11 +7,11 @@ module Coltrane
|
|
7
7
|
attr_reader :intervals
|
8
8
|
|
9
9
|
def_delegators :@intervals, :map, :each, :[], :size,
|
10
|
-
:reduce, :delete
|
10
|
+
:reduce, :delete, :reject!, :delete_if
|
11
11
|
|
12
|
-
|
12
|
+
IntervalClass.all_full_names.each do |full_name|
|
13
13
|
define_method "has_#{full_name.underscore}?" do
|
14
|
-
!!(intervals.detect {|i| i.public_send("#{full_name.underscore}?")})
|
14
|
+
!!(intervals.detect { |i| i.public_send("#{full_name.underscore}?") })
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
@@ -22,7 +22,7 @@ module Coltrane
|
|
22
22
|
return priority unless priority.nil?
|
23
23
|
@intervals.each do |ix|
|
24
24
|
ix.full_names.detect do |ixx|
|
25
|
-
return ixx if ixx.match(/#{i.interval_name}/)
|
25
|
+
return ixx if ixx.match?(/#{i.interval_name}/)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
nil
|
@@ -31,7 +31,7 @@ module Coltrane
|
|
31
31
|
define_method "#{i.interval_name.underscore}!" do
|
32
32
|
@intervals.each do |ix|
|
33
33
|
ix.full_names.detect do |ixx|
|
34
|
-
next if ixx.match(/Diminished|Augmented/)
|
34
|
+
next if ixx.match?(/Diminished|Augmented/)
|
35
35
|
return ixx if ixx.match? /#{i.interval_name}/
|
36
36
|
end
|
37
37
|
end
|
@@ -40,20 +40,21 @@ module Coltrane
|
|
40
40
|
|
41
41
|
# defines methods like :has_fifth?, :has_third?, has_eleventh?:
|
42
42
|
define_method "has_#{i.interval_name.underscore}?" do
|
43
|
-
!!@intervals.detect {|ix| ix.full_name.match(/#{i.interval_name}/) }
|
43
|
+
!!@intervals.detect { |ix| ix.full_name.match(/#{i.interval_name}/) }
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
47
|
def initialize(notes: nil, intervals: nil, distances: nil)
|
48
|
-
if
|
48
|
+
if notes
|
49
49
|
notes = NoteSet[*notes] if notes.is_a?(Array)
|
50
50
|
@intervals = intervals_from_notes(notes)
|
51
|
-
elsif
|
52
|
-
@intervals = intervals.map { |i|
|
53
|
-
elsif
|
51
|
+
elsif intervals
|
52
|
+
@intervals = intervals.map { |i| IntervalClass[i] }
|
53
|
+
elsif distances
|
54
54
|
@distances = distances
|
55
55
|
@intervals = intervals_from_distances(distances)
|
56
56
|
else
|
57
|
+
require 'pry'; binding.pry
|
57
58
|
raise 'Provide: [notes:] || [intervals:] || [distances:]'
|
58
59
|
end
|
59
60
|
end
|
@@ -73,7 +74,7 @@ module Coltrane
|
|
73
74
|
end
|
74
75
|
|
75
76
|
def has?(interval_name)
|
76
|
-
@intervals.include?(
|
77
|
+
@intervals.include?(IntervalClass[interval_name])
|
77
78
|
end
|
78
79
|
|
79
80
|
alias interval_names names
|
@@ -144,13 +145,13 @@ module Coltrane
|
|
144
145
|
private
|
145
146
|
|
146
147
|
def intervals_from_distances(distances)
|
147
|
-
distances[0..-2].reduce([
|
148
|
+
distances[0..-2].reduce([IntervalClass[0]]) do |memo, d|
|
148
149
|
memo + [memo.last + d]
|
149
150
|
end
|
150
151
|
end
|
151
152
|
|
152
153
|
def intervals_from_notes(notes)
|
153
|
-
notes.map { |n| notes.root
|
154
|
+
notes.map { |n| n - notes.root }.sort_by(&:semitones)
|
154
155
|
end
|
155
156
|
end
|
156
157
|
end
|
@@ -1,7 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Coltrane
|
4
|
+
|
5
|
+
# This module takes care of adding to progressions knowledge that is more
|
6
|
+
# based on common standards and practices.
|
2
7
|
module NotableProgressions
|
3
8
|
PROGRESSIONS = {
|
4
|
-
'Pop'
|
9
|
+
'Pop' => ['I-V-vi-IV', :major],
|
5
10
|
'Jazzy Pop' => ['IM7-V7-vi7-IVM7', :major],
|
6
11
|
'Jazz' => ['ii7-V7-I7', :major],
|
7
12
|
'Jazz Minor' => ['ii7-V7-i7', :major],
|
@@ -10,7 +15,7 @@ module Coltrane
|
|
10
15
|
'Fifties' => ['I-vi-IV-V', :major],
|
11
16
|
'Circle' => ['vi-ii-V-I', :major],
|
12
17
|
'Tune Up' => ['ii7-V7-IM7-i7-IV7-IVM7-VIIM7', :minor]
|
13
|
-
}
|
18
|
+
}.freeze
|
14
19
|
|
15
20
|
PROGRESSIONS.each do |name, values|
|
16
21
|
notation, scale_name = values
|
@@ -21,4 +26,4 @@ module Coltrane
|
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
24
|
-
end
|
29
|
+
end
|
data/lib/coltrane/note.rb
CHANGED
@@ -1,98 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Coltrane
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
'
|
20
|
-
'
|
21
|
-
'F#' => 6,
|
22
|
-
'Gb' => 6,
|
23
|
-
'G' => 7,
|
24
|
-
'G#' => 8,
|
25
|
-
'Ab' => 8,
|
26
|
-
'A' => 9,
|
27
|
-
'A#' => 10,
|
28
|
-
'Bb' => 10,
|
29
|
-
'B' => 11
|
4
|
+
# Notes are different ways of calling pitch classes. In the context of equal
|
5
|
+
# tempered scales, they're more like a conceptual subject for
|
6
|
+
# matters of convention than an actual thing.
|
7
|
+
#
|
8
|
+
# Take for example A# and Bb. Those are different notes. Nevertheless, in the
|
9
|
+
# context of equal tempered scales they represent pretty much the same
|
10
|
+
# frequency.
|
11
|
+
#
|
12
|
+
# The theory of notes have changed too much in the course of time, which
|
13
|
+
# lead us with a lot of conventions and strategies when dealing with music.
|
14
|
+
# That's what this class is for.
|
15
|
+
class Note < PitchClass
|
16
|
+
attr_reader :alteration
|
17
|
+
|
18
|
+
ALTERATIONS = {
|
19
|
+
'b' => -1,
|
20
|
+
'#' => 1
|
30
21
|
}.freeze
|
31
22
|
|
32
|
-
def initialize(
|
33
|
-
|
34
|
-
|
23
|
+
def initialize(arg)
|
24
|
+
note_name = case arg
|
25
|
+
when String then arg
|
26
|
+
when PitchClass then arg.true_notation
|
27
|
+
when Numeric, Frequency then PitchClass.new(arg).true_notation
|
28
|
+
else raise(WrongArgumentsError, arg)
|
29
|
+
end
|
30
|
+
|
31
|
+
chars = note_name.chars
|
32
|
+
letter = chars.shift
|
33
|
+
raise InvalidNoteError, arg unless ('A'..'G').cover?(letter)
|
34
|
+
@alteration = chars.reduce(0) do |alt, symbol|
|
35
|
+
raise InvalidNoteError, arg unless ALTERATIONS.include?(symbol)
|
36
|
+
alt + ALTERATIONS[symbol]
|
37
|
+
end
|
38
|
+
super((PitchClass[letter].integer + @alteration) % PitchClass.size)
|
35
39
|
end
|
36
40
|
|
37
|
-
private_class_method :new
|
38
|
-
|
39
41
|
def self.[](arg)
|
40
|
-
|
41
|
-
case arg
|
42
|
-
when Note then return arg
|
43
|
-
when String then find_note(arg)
|
44
|
-
when Numeric then NOTES.key(arg % 12)
|
45
|
-
else
|
46
|
-
raise InvalidNoteError, "Wrong type: #{arg.class}"
|
47
|
-
end
|
48
|
-
|
49
|
-
new(name) || (raise InvalidNoteError, arg.to_s)
|
42
|
+
new(arg)
|
50
43
|
end
|
51
44
|
|
52
|
-
def
|
53
|
-
|
45
|
+
def name
|
46
|
+
"#{base_pitch_class}#{accidents}".gsub(/#b|b#/, '')
|
54
47
|
end
|
55
48
|
|
56
|
-
def
|
57
|
-
|
58
|
-
nil
|
49
|
+
def base_pitch_class
|
50
|
+
PitchClass[integer - alteration]
|
59
51
|
end
|
60
52
|
|
61
|
-
def
|
62
|
-
@
|
53
|
+
def alteration=(a)
|
54
|
+
@alteration = a unless PitchClass[integer - a].accidental?
|
63
55
|
end
|
64
56
|
|
65
|
-
|
66
|
-
|
67
|
-
def accident?
|
68
|
-
[1, 3, 6, 8, 10].include?(number)
|
57
|
+
def alter(x)
|
58
|
+
Note.new(name).tap { |n| n.alteration = x }
|
69
59
|
end
|
70
60
|
|
71
|
-
def
|
72
|
-
|
73
|
-
when Interval then Note[number + other.semitones]
|
74
|
-
when Numeric then Note[number + other]
|
75
|
-
when Note then Chord.new(number + other.number)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def -(other)
|
80
|
-
case other
|
81
|
-
when Numeric then Note[number - other]
|
82
|
-
when Interval then Note[number - other.semitones]
|
83
|
-
when Note then Interval[other.number - number]
|
84
|
-
end
|
61
|
+
def accidents
|
62
|
+
(@alteration > 0 ? '#' : 'b') * alteration.abs
|
85
63
|
end
|
86
64
|
|
87
65
|
def interval_to(note_name)
|
88
66
|
Note[note_name] - self
|
89
67
|
end
|
90
|
-
|
91
|
-
def enharmonic?(other)
|
92
|
-
case other
|
93
|
-
when String then number == Note[other].number
|
94
|
-
when Note then number == other.number
|
95
|
-
end
|
96
|
-
end
|
97
68
|
end
|
98
69
|
end
|
data/lib/coltrane/note_set.rb
CHANGED
@@ -38,14 +38,14 @@ module Coltrane
|
|
38
38
|
case other
|
39
39
|
when Note then NoteSet[*(notes + [other])]
|
40
40
|
when NoteSet then NoteSet[*notes, *other.notes]
|
41
|
-
when Interval then NoteSet[*notes.map {|n| n + other}]
|
41
|
+
when Interval then NoteSet[*notes.map { |n| n + other }]
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
45
|
def -(other)
|
46
46
|
case other
|
47
47
|
when NoteSet then NoteSet[*(notes - other.notes)]
|
48
|
-
when Interval then NoteSet[*notes.map {|n| n - other}]
|
48
|
+
when Interval then NoteSet[*notes.map { |n| n - other }]
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -57,8 +57,8 @@ module Coltrane
|
|
57
57
|
map(&:name)
|
58
58
|
end
|
59
59
|
|
60
|
-
def
|
61
|
-
map(&:
|
60
|
+
def integers
|
61
|
+
map(&:integer)
|
62
62
|
end
|
63
63
|
|
64
64
|
def interval_sequence
|
data/lib/coltrane/pitch.rb
CHANGED
@@ -3,35 +3,56 @@
|
|
3
3
|
module Coltrane
|
4
4
|
# It describes a pitch, like E4 or Bb5. It's like a note, but it has an octave
|
5
5
|
class Pitch
|
6
|
-
attr_reader :
|
7
|
-
|
8
|
-
def initialize(pitch)
|
9
|
-
case pitch
|
10
|
-
when String then @number = number_from_name(pitch)
|
11
|
-
when Numeric then @number = pitch
|
12
|
-
when Pitch then @number = pitch.number
|
13
|
-
end
|
14
|
-
end
|
6
|
+
attr_reader :pitch_class, :octave
|
15
7
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
8
|
+
def initialize(notation_arg = nil,
|
9
|
+
pitch_class: nil,
|
10
|
+
octave: nil,
|
11
|
+
notation: nil,
|
12
|
+
frequency: nil)
|
20
13
|
|
21
|
-
|
22
|
-
|
14
|
+
@pitch_class, @octave =
|
15
|
+
if notation_arg || notation
|
16
|
+
pitch_class_and_octave_from_notation(notation_arg || notation)
|
17
|
+
elsif pitch_class && octave
|
18
|
+
[pitch_class, octave]
|
19
|
+
elsif frequency
|
20
|
+
pitch_class_and_octave_from_frequency(frequency)
|
21
|
+
else
|
22
|
+
raise InvalidArgumentsError
|
23
|
+
end
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
|
-
number / 12
|
27
|
-
end
|
26
|
+
private
|
28
27
|
|
29
|
-
def
|
30
|
-
|
28
|
+
def pitch_class_and_octave_from_notation(name)
|
29
|
+
pitch_class_notation, octaves = *name.match(/(.*)(\d)/)
|
30
|
+
[PitchClass.new(pitch_class_notation), octaves.to_f]
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
34
|
-
|
33
|
+
def pitch_class_and_octave_from_frequency(frequency)
|
34
|
+
[PitchClass[frequency], Math.log(f.to_f / TUNING, 2) / 12]
|
35
35
|
end
|
36
|
+
|
37
|
+
# def number_from_name(pitch_string)
|
38
|
+
# Note[note].number + 12 * octaves.to_i
|
39
|
+
# end
|
40
|
+
|
41
|
+
# def name
|
42
|
+
# "#{note.name}#{octave}"
|
43
|
+
# end
|
44
|
+
|
45
|
+
# def octave
|
46
|
+
# number / 12
|
47
|
+
# end
|
48
|
+
|
49
|
+
# def note
|
50
|
+
# Note[number]
|
51
|
+
# end
|
52
|
+
|
53
|
+
# def +(other)
|
54
|
+
# Pitch.new(number + (other.is_a?(Pitch) ? other.number : other))
|
55
|
+
# end
|
56
|
+
# end
|
36
57
|
end
|
37
58
|
end
|