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