-
Notifications
You must be signed in to change notification settings - Fork 0
/
scales.rb
executable file
·185 lines (158 loc) · 4.06 KB
/
scales.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#! /usr/bin/env ruby
def colored?
($*.include?('--colored') || $*.include?('--color-on')) && !$*.include?('--color-off')
end
class String
def green
"\033[0;32m" + self + "\033[0m"
end
def red
"\033[0;31m" + self + "\033[0m"
end
def underlined(char = '=')
self + "\n" + char * self.length
end
end
class Array
def circular(range)
return at(range % size) if range.is_a?(Fixnum)
range = (range.begin..(size + range.end)) if range.end < range.begin
range.map do |index|
at(index % size)
end
end
end
class Scale
TONES = %w(c c# d d# e f f# g g# a a# b)
STEPS = [2, 2, 1, 2, 2, 2, 1]
MAJMIN = [:major, :minor, :minor, :major, :major, :minor, :diminished]
SPACE = 4
class << self
TONES.each do |tone|
[tone, tone.gsub('#', 'is'), tone.upcase, tone.upcase.gsub('#', 'is')].each do |name|
define_method name do
self.new(tone)
end
end
end
end
def initialize(tonic)
raise "bad tonic: #{tonic.downcase}" unless TONES.include?(tonic.downcase)
@tonic = tonic
@offset = TONES.index(@tonic.downcase)
end
def self.chromatic_tones(basis = 'c', count = 2)
basis = basis.downcase
start = TONES.index(basis)
TONES.circular(start..start - 1)*count
end
def self.all(start_with = 'C')
start = TONES.index(start_with.downcase)
tonality = Scale.new(start_with).tonality
TONES.circular(start..(start - 1)).map do |tonic|
self.new(tonality == :major ? tonic.upcase : tonic.downcase )
end
end
def self.expand(basis = 'C')
Scale.new(basis).tones.map do |tonic|
self.new(tonic) rescue nil
end.compact
end
def tonic; tone(1); end
def subdominant; tone(4); end
def dominant; tone(5); end
def tones(range = 1..7)
range.map do |pos|
tone(pos)
end
end
def step_sum_for_pos(pos)
sum = 0
pos = bound_pos(pos)
(2..pos).each do |i|
if tonality == :major
sum += STEPS.circular(i - 2)
else
sum += STEPS.circular(i + 4)
end
end
sum
end
def pos_to_index(pos)
bound_index(@offset + step_sum_for_pos(pos))
end
def bound_index(index) # ok
index % TONES.size
end
def bound_pos(pos)
((pos - 1) % 7) + 1
end
def tone(pos)
pos = bound_pos(pos)
tone = TONES.circular(pos_to_index(pos))
with_case(tone, pos)
end
def with_case(tone, pos)
look_pos = tonality == :major ? pos - 1 : pos + 4
if MAJMIN.circular(look_pos) == :major
tone.upcase
elsif MAJMIN.circular(look_pos) == :diminished
tone.upcase + '0'
else
tone.downcase
end
end
def tonality
@tonic.upcase == @tonic ? :major : :minor
end
def pos(tone, options = {})
if options[:case_sensitive] == false
tones.map { |t| t.downcase.gsub('0', '') }.index(tone.downcase.gsub('0', '')) + 1 rescue nil
else
tones.index(tone) + 1 rescue nil
end
end
def tone_like?(tone)
p = pos(tone, :case_sensitive => false)
tone(p) if p
end
def to_s(range = 1..7)
tones(range).map do |tone|
tone.ljust(SPACE)
end.join(' ')
end
def matrix(basis)
str = ''
start_output = colored? ? true : nil
base_tones = Scale.new(basis).tones
tones = []
Scale.chromatic_tones(basis, 2).each do |tone|
start_output ||= tone_like?(tone) == tonic
next str << ' '.ljust(SPACE) if !start_output || (!colored? && tones.include?(tone))
if (t = tone_like?(tone)) && start_output
if t == tonic && colored?
str << t.ljust(SPACE).red
elsif base_tones.include?(t) && colored?
str << t.ljust(SPACE).green
else
str << t.ljust(SPACE)
end
else
str << '-'.ljust(SPACE)
end
tones << tone
end
str
end
end
default = 'C'
start = $*.select { |arg| !(arg =~ /^--/) } .last || default
puts "Expanded #{start} scale:".underlined
Scale.expand(start).each do |scale|
puts scale.matrix(start)
end
puts
puts "#{Scale.new(start).tonality.to_s.capitalize} scales:".underlined
Scale.all(start).each do |scale|
puts scale.matrix(start)
end