musa-dsl 0.14.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +20 -0
- data/LICENSE.md +157 -0
- data/README.md +8 -0
- data/lib/musa-dsl/core-ext/array-apply-get.rb +18 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +29 -0
- data/lib/musa-dsl/core-ext/array-to-neumas.rb +28 -0
- data/lib/musa-dsl/core-ext/array-to-serie.rb +20 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +15 -0
- data/lib/musa-dsl/core-ext/as-context-run.rb +44 -0
- data/lib/musa-dsl/core-ext/duplicate.rb +134 -0
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +55 -0
- data/lib/musa-dsl/core-ext/inspect-nice.rb +28 -0
- data/lib/musa-dsl/core-ext/key-parameters-procedure-binder.rb +85 -0
- data/lib/musa-dsl/core-ext/proc-nice.rb +13 -0
- data/lib/musa-dsl/core-ext/send-nice.rb +21 -0
- data/lib/musa-dsl/core-ext/string-to-neumas.rb +27 -0
- data/lib/musa-dsl/core-ext.rb +13 -0
- data/lib/musa-dsl/datasets/gdv-decorators.rb +221 -0
- data/lib/musa-dsl/datasets/gdv.rb +499 -0
- data/lib/musa-dsl/datasets/pdv.rb +44 -0
- data/lib/musa-dsl/datasets.rb +5 -0
- data/lib/musa-dsl/generative/darwin.rb +145 -0
- data/lib/musa-dsl/generative/generative-grammar.rb +294 -0
- data/lib/musa-dsl/generative/markov.rb +78 -0
- data/lib/musa-dsl/generative/rules.rb +282 -0
- data/lib/musa-dsl/generative/variatio.rb +331 -0
- data/lib/musa-dsl/generative.rb +5 -0
- data/lib/musa-dsl/midi/midi-recorder.rb +83 -0
- data/lib/musa-dsl/midi/midi-voices.rb +274 -0
- data/lib/musa-dsl/midi.rb +2 -0
- data/lib/musa-dsl/music/chord-definition.rb +99 -0
- data/lib/musa-dsl/music/chord-definitions.rb +13 -0
- data/lib/musa-dsl/music/chords.rb +326 -0
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +204 -0
- data/lib/musa-dsl/music/scales.rb +584 -0
- data/lib/musa-dsl/music.rb +6 -0
- data/lib/musa-dsl/neuma/neuma.rb +181 -0
- data/lib/musa-dsl/neuma.rb +1 -0
- data/lib/musa-dsl/neumalang/neumalang.citrus +294 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +179 -0
- data/lib/musa-dsl/neumalang.rb +3 -0
- data/lib/musa-dsl/repl/repl.rb +143 -0
- data/lib/musa-dsl/repl.rb +1 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +189 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +354 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +382 -0
- data/lib/musa-dsl/sequencer/base-sequencer-public.rb +261 -0
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +94 -0
- data/lib/musa-dsl/sequencer/sequencer.rb +3 -0
- data/lib/musa-dsl/sequencer.rb +1 -0
- data/lib/musa-dsl/series/base-series.rb +245 -0
- data/lib/musa-dsl/series/hash-serie-splitter.rb +194 -0
- data/lib/musa-dsl/series/holder-serie.rb +87 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +726 -0
- data/lib/musa-dsl/series/main-serie-operations.rb +1151 -0
- data/lib/musa-dsl/series/proxy-serie.rb +69 -0
- data/lib/musa-dsl/series/queue-serie.rb +94 -0
- data/lib/musa-dsl/series/series.rb +8 -0
- data/lib/musa-dsl/series.rb +1 -0
- data/lib/musa-dsl/transport/clock.rb +36 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +47 -0
- data/lib/musa-dsl/transport/external-tick-clock.rb +31 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +124 -0
- data/lib/musa-dsl/transport/timer-clock.rb +102 -0
- data/lib/musa-dsl/transport/timer.rb +40 -0
- data/lib/musa-dsl/transport/transport.rb +137 -0
- data/lib/musa-dsl/transport.rb +9 -0
- data/lib/musa-dsl.rb +17 -0
- data/musa-dsl.gemspec +17 -0
- metadata +174 -0
@@ -0,0 +1,584 @@
|
|
1
|
+
module Musa
|
2
|
+
module Scales
|
3
|
+
class << self
|
4
|
+
def register(scale_system, default: nil)
|
5
|
+
@scale_systems ||= {}
|
6
|
+
@scale_systems[scale_system.id] = scale_system
|
7
|
+
|
8
|
+
@default_scale_system = scale_system if default
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](id)
|
13
|
+
raise KeyError, "Scale system :#{id} not found" unless @scale_systems.key?(id)
|
14
|
+
|
15
|
+
@scale_systems[id]
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_system
|
19
|
+
@default_scale_system
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def method_missing(method_name, *args, **key_args, &block)
|
25
|
+
if @scale_systems.key?(method_name) && args.empty? && key_args.empty? && !block
|
26
|
+
self[method_name]
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to_missing?(method_name, include_private)
|
33
|
+
@scale_systems.key?(method_name) || super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ScaleSystem
|
39
|
+
class << self
|
40
|
+
# @abstract Subclass is expected to implement names
|
41
|
+
# @!method id
|
42
|
+
# @return [Symbol] the id of the ScaleSystem as a symbol
|
43
|
+
#
|
44
|
+
def id
|
45
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
46
|
+
end
|
47
|
+
|
48
|
+
# @abstract Subclass is expected to implement notes_in_octave
|
49
|
+
# @!method notes_in_octave
|
50
|
+
# @return [Integer] the number of notes in one octave in the ScaleSystem
|
51
|
+
#
|
52
|
+
def notes_in_octave
|
53
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
54
|
+
end
|
55
|
+
|
56
|
+
# @abstract Subclass is expected to implement part_of_tone_size
|
57
|
+
# @!method part_of_tone_size
|
58
|
+
# @return [Integer] the size inside the ScaleSystem of the smaller part of a tone; used for calculate sharp and flat notes
|
59
|
+
#
|
60
|
+
def part_of_tone_size
|
61
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
62
|
+
end
|
63
|
+
|
64
|
+
# @abstract Subclass is expected to implement intervals
|
65
|
+
# @!method intervals
|
66
|
+
# @return [Hash] the intervals of the ScaleSystem as { name: semitones#, ... }
|
67
|
+
#
|
68
|
+
def intervals
|
69
|
+
# TODO: implementar intérvalos sinónimos (p.ej, m3 = A2)
|
70
|
+
# TODO: implementar identificación de intérvalos, teniendo en cuenta no sólo los semitonos sino los grados de separación
|
71
|
+
# TODO: implementar inversión de intérvalos
|
72
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
73
|
+
end
|
74
|
+
|
75
|
+
# @abstract Subclass is expected to implement frequency_of_pitch
|
76
|
+
# @!method frequency_of_pitch
|
77
|
+
# @param pitch [Number] The pitch (MIDI note numbers based) of the note to get the fundamental frequency
|
78
|
+
# @param root_pitch [Number] The pitch (MIDI note numbers based) of the root note of the scale (needed for not equally tempered scales)
|
79
|
+
# @param a_frequency [Number] The reference frequency of the mid A note
|
80
|
+
# @return [Number] the frequency of the fundamental tone of the pitch
|
81
|
+
#
|
82
|
+
def frequency_of_pitch(pitch, root_pitch, a_frequency)
|
83
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
84
|
+
end
|
85
|
+
|
86
|
+
# @abstract Subclass can implement default_a_frequency. If subclass doesn't implement default_a_frequency 440.0 Hz is assumed.
|
87
|
+
# @!method default_a_frequency
|
88
|
+
# @return [Number] the frequency A by default
|
89
|
+
#
|
90
|
+
def default_a_frequency
|
91
|
+
440.0
|
92
|
+
end
|
93
|
+
|
94
|
+
def [](a_frequency)
|
95
|
+
a_frequency = a_frequency.to_f
|
96
|
+
|
97
|
+
@a_tunings ||= {}
|
98
|
+
@a_tunings[a_frequency] = ScaleSystemTuning.new self, a_frequency unless @a_tunings.key?(a_frequency)
|
99
|
+
|
100
|
+
@a_tunings[a_frequency]
|
101
|
+
end
|
102
|
+
|
103
|
+
def offset_of_interval(name)
|
104
|
+
intervals[name]
|
105
|
+
end
|
106
|
+
|
107
|
+
def default_tuning
|
108
|
+
self[default_a_frequency]
|
109
|
+
end
|
110
|
+
|
111
|
+
def register(scale_kind_class)
|
112
|
+
@scale_kind_classes ||= {}
|
113
|
+
@scale_kind_classes[scale_kind_class.id] = scale_kind_class
|
114
|
+
if scale_kind_class.chromatic?
|
115
|
+
@chromatic_scale_kind_class = scale_kind_class
|
116
|
+
end
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def scale_kind_class(id)
|
121
|
+
raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
|
122
|
+
|
123
|
+
@scale_kind_classes[id]
|
124
|
+
end
|
125
|
+
|
126
|
+
def scale_kind_class?(id)
|
127
|
+
@scale_kind_classes.key? id
|
128
|
+
end
|
129
|
+
|
130
|
+
def chromatic_class
|
131
|
+
raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
|
132
|
+
|
133
|
+
@chromatic_scale_kind_class
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def ==(other)
|
138
|
+
self.class == other.class
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class ScaleSystemTuning
|
143
|
+
extend Forwardable
|
144
|
+
|
145
|
+
def initialize(scale_system, a_frequency)
|
146
|
+
@scale_system = scale_system
|
147
|
+
@a_frequency = a_frequency
|
148
|
+
@scale_kinds = {}
|
149
|
+
|
150
|
+
@chromatic_scale_kind = self[@scale_system.chromatic_class.id]
|
151
|
+
end
|
152
|
+
|
153
|
+
# TODO: allow scales not based in octaves but in other intervals (like fifths or other ratios). Possibly based on intervals definition of ScaleSystem plus a "generator interval" attribute
|
154
|
+
|
155
|
+
def_delegators :@scale_system, :notes_in_octave, :offset_of_interval
|
156
|
+
|
157
|
+
attr_reader :a_frequency, :scale_system
|
158
|
+
|
159
|
+
def [](scale_kind_class_id)
|
160
|
+
@scale_kinds[scale_kind_class_id] ||= @scale_system.scale_kind_class(scale_kind_class_id).new self
|
161
|
+
end
|
162
|
+
|
163
|
+
def chromatic
|
164
|
+
@chromatic_scale_kind
|
165
|
+
end
|
166
|
+
|
167
|
+
def frequency_of_pitch(pitch, root)
|
168
|
+
@scale_system.frequency_of_pitch(pitch, root, @a_frequency)
|
169
|
+
end
|
170
|
+
|
171
|
+
def ==(other)
|
172
|
+
self.class == other.class &&
|
173
|
+
@scale_system == other.scale_system &&
|
174
|
+
@a_frequency == other.a_frequency
|
175
|
+
end
|
176
|
+
|
177
|
+
def inspect
|
178
|
+
"<ScaleSystemTuning: scale_system = #{@scale_system} a_frequency = #{@a_frequency}>"
|
179
|
+
end
|
180
|
+
|
181
|
+
alias to_s inspect
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def method_missing(method_name, *args, **key_args, &block)
|
186
|
+
if @scale_system.scale_kind_class?(method_name) && args.empty? && key_args.empty? && !block
|
187
|
+
self[method_name]
|
188
|
+
else
|
189
|
+
super
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def respond_to_missing?(method_name, include_private)
|
194
|
+
@scale_system.scale_kind_class?(method_name) || super
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class ScaleKind
|
199
|
+
extend Forwardable
|
200
|
+
|
201
|
+
def initialize(tuning)
|
202
|
+
@tuning = tuning
|
203
|
+
@scales = {}
|
204
|
+
end
|
205
|
+
|
206
|
+
attr_reader :tuning
|
207
|
+
|
208
|
+
def [](root_pitch)
|
209
|
+
@scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
|
210
|
+
@scales[root_pitch]
|
211
|
+
end
|
212
|
+
|
213
|
+
def absolut
|
214
|
+
self[0]
|
215
|
+
end
|
216
|
+
|
217
|
+
def ==(other)
|
218
|
+
self.class == other.class && @tuning == other.tuning
|
219
|
+
end
|
220
|
+
|
221
|
+
def inspect
|
222
|
+
"<#{self.class.name}: tuning = #{@tuning}>"
|
223
|
+
end
|
224
|
+
|
225
|
+
alias to_s inspect
|
226
|
+
|
227
|
+
class << self
|
228
|
+
# @abstract Subclass is expected to implement id
|
229
|
+
# @!method id
|
230
|
+
# @return [Symbol] the id of the ScaleKind as a symbol
|
231
|
+
def id
|
232
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
233
|
+
end
|
234
|
+
|
235
|
+
# @abstract Subclass is expected to implement pitches
|
236
|
+
# @!method pitches
|
237
|
+
# @return [Array] the pitches array of the ScaleKind as [ { functions: [ <symbol>, ...], pitch: <Number> }, ... ]
|
238
|
+
def pitches
|
239
|
+
raise 'Method not implemented. Should be implemented in subclass.'
|
240
|
+
end
|
241
|
+
|
242
|
+
# @abstract Subclass is expected to implement chromatic?. Only one of the subclasses should return true.
|
243
|
+
# @!method chromatic?
|
244
|
+
# @return [Boolean] wether the scales is a full scale (with all the notes in the ScaleSystem), sorted and to be considered canonical. I.e. a chromatic 12 semitones uprising serie in a 12 tone tempered ScaleSystem.
|
245
|
+
def chromatic?
|
246
|
+
false
|
247
|
+
end
|
248
|
+
|
249
|
+
# @abstract Subclass is expected to implement grades when the ScaleKind is defining more pitches than notes by octave has the scale. This can happen when there are pitches defined on upper octaves (i.e., to define XII grade, as a octave + fifth)
|
250
|
+
# @!method grades
|
251
|
+
# @return [Integer] Number of grades inside of a octave of the scale
|
252
|
+
def grades
|
253
|
+
pitches.length
|
254
|
+
end
|
255
|
+
|
256
|
+
def find_index(symbol)
|
257
|
+
init unless @index
|
258
|
+
@index[symbol]
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
def init
|
265
|
+
@index = {}
|
266
|
+
pitches.each_index do |i|
|
267
|
+
pitches[i][:functions].each do |function|
|
268
|
+
@index[function] = i
|
269
|
+
end
|
270
|
+
end
|
271
|
+
self
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
class Scale
|
277
|
+
extend Forwardable
|
278
|
+
|
279
|
+
def initialize(kind, root_pitch:)
|
280
|
+
@notes_by_grade = {}
|
281
|
+
@notes_by_pitch = {}
|
282
|
+
|
283
|
+
@kind = kind
|
284
|
+
|
285
|
+
@root_pitch = root_pitch
|
286
|
+
end
|
287
|
+
|
288
|
+
def_delegators :@kind, :a_tuning
|
289
|
+
|
290
|
+
attr_reader :kind, :root_pitch
|
291
|
+
|
292
|
+
def root
|
293
|
+
self[0]
|
294
|
+
end
|
295
|
+
|
296
|
+
def chromatic
|
297
|
+
@kind.tuning.chromatic[@root_pitch]
|
298
|
+
end
|
299
|
+
|
300
|
+
def absolut
|
301
|
+
@kind[0]
|
302
|
+
end
|
303
|
+
|
304
|
+
def octave(octave)
|
305
|
+
raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
|
306
|
+
|
307
|
+
@kind[@root_pitch + octave * @kind.class.grades]
|
308
|
+
end
|
309
|
+
|
310
|
+
def [](grade_or_symbol)
|
311
|
+
|
312
|
+
raise ArgumentError, "grade_or_symbol '#{grade_or_symbol}' should be a Numeric, String or Symbol" unless grade_or_symbol.is_a?(Symbol) || grade_or_symbol.is_a?(String) || grade_or_symbol.is_a?(Integer)
|
313
|
+
|
314
|
+
wide_grade, sharps = grade_of(grade_or_symbol)
|
315
|
+
|
316
|
+
unless @notes_by_grade.key?(wide_grade)
|
317
|
+
|
318
|
+
octave = wide_grade / @kind.class.grades
|
319
|
+
grade = wide_grade % @kind.class.grades
|
320
|
+
|
321
|
+
pitch = @root_pitch +
|
322
|
+
octave * @kind.tuning.notes_in_octave +
|
323
|
+
@kind.class.pitches[grade][:pitch]
|
324
|
+
|
325
|
+
note = NoteInScale.new self, grade, octave, pitch
|
326
|
+
|
327
|
+
@notes_by_grade[wide_grade] = @notes_by_pitch[pitch] = note
|
328
|
+
end
|
329
|
+
|
330
|
+
|
331
|
+
@notes_by_grade[wide_grade].sharp(sharps)
|
332
|
+
end
|
333
|
+
|
334
|
+
def grade_of(grade_or_string_or_symbol)
|
335
|
+
sign, name, wide_grade, accidentals = Musa::Datasets::GDVd::Parser.parse_grade(grade_or_string_or_symbol)
|
336
|
+
|
337
|
+
raise ArgumentError, "Cannot parse sign on #{grade_or_string_or_symbol}" if sign
|
338
|
+
|
339
|
+
grade = @kind.class.find_index name if name
|
340
|
+
|
341
|
+
octave = wide_grade / @kind.class.grades if wide_grade
|
342
|
+
grade = wide_grade % @kind.class.grades if wide_grade
|
343
|
+
|
344
|
+
octave ||= 0
|
345
|
+
|
346
|
+
return octave * @kind.class.grades + grade, accidentals
|
347
|
+
end
|
348
|
+
|
349
|
+
def note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil)
|
350
|
+
allow_chromatic ||= false
|
351
|
+
allow_nearest ||= false
|
352
|
+
|
353
|
+
note = @notes_by_pitch[pitch]
|
354
|
+
|
355
|
+
unless note
|
356
|
+
pitch_offset = pitch - @root_pitch
|
357
|
+
|
358
|
+
pitch_offset_in_octave = pitch_offset % @kind.tuning.scale_system.notes_in_octave
|
359
|
+
pitch_offset_octave = pitch_offset / @kind.tuning.scale_system.notes_in_octave
|
360
|
+
|
361
|
+
grade = @kind.class.pitches.find_index { |pitch_definition| pitch_definition[:pitch] == pitch_offset_in_octave }
|
362
|
+
|
363
|
+
if grade
|
364
|
+
wide_grade = pitch_offset_octave * @kind.class.grades + grade
|
365
|
+
note = self[wide_grade]
|
366
|
+
|
367
|
+
elsif allow_nearest
|
368
|
+
sharps = 0
|
369
|
+
|
370
|
+
until note
|
371
|
+
note = note_of_pitch(pitch - (sharps += 1) * @kind.tuning.scale_system.part_of_tone_size)
|
372
|
+
note ||= note_of_pitch(pitch + sharps * @kind.tuning.scale_system.part_of_tone_size)
|
373
|
+
end
|
374
|
+
|
375
|
+
elsif allow_chromatic
|
376
|
+
nearest = note_of_pitch(pitch, allow_nearest: true)
|
377
|
+
|
378
|
+
note = chromatic.note_of_pitch(pitch).with_background(scale: self, grade: nearest.grade, octave: nearest.octave, sharps: (pitch - nearest.pitch) / @kind.tuning.scale_system.part_of_tone_size)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
note
|
383
|
+
end
|
384
|
+
|
385
|
+
def offset_of_interval(interval_name)
|
386
|
+
@kind.tuning.offset_of_interval(interval_name)
|
387
|
+
end
|
388
|
+
|
389
|
+
def chord_of(*grades_or_symbols)
|
390
|
+
Chord.new(notes: grades_or_symbols.collect { |g| self[g] })
|
391
|
+
end
|
392
|
+
|
393
|
+
def ==(other)
|
394
|
+
self.class == other.class &&
|
395
|
+
@kind == other.kind &&
|
396
|
+
@root_pitch == other.root_pitch
|
397
|
+
end
|
398
|
+
|
399
|
+
def inspect
|
400
|
+
"<Scale: kind = #{@kind} root_pitch = #{@root_pitch}>"
|
401
|
+
end
|
402
|
+
|
403
|
+
alias to_s inspect
|
404
|
+
|
405
|
+
private
|
406
|
+
|
407
|
+
def method_missing(method_name, *args, **key_args, &block)
|
408
|
+
if @kind.class.find_index(method_name) && args.empty? && key_args.empty? && !block
|
409
|
+
self[method_name]
|
410
|
+
else
|
411
|
+
super
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def respond_to_missing?(method_name, include_private)
|
416
|
+
@kind.class.find_index(method_name) || super
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
class NoteInScale
|
421
|
+
|
422
|
+
# @param scale [Scale]
|
423
|
+
# @param grade []
|
424
|
+
# @param octave [Integer]
|
425
|
+
# @param pitch [Number] pitch of the note, based on MIDI note numbers. Can be Integer, Rational or Float to express fractions of a semitone
|
426
|
+
#
|
427
|
+
def initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil)
|
428
|
+
@scale = scale
|
429
|
+
@grade = grade
|
430
|
+
@octave = octave
|
431
|
+
@pitch = pitch
|
432
|
+
|
433
|
+
@background_scale = background_scale
|
434
|
+
@background_grade = background_grade
|
435
|
+
@background_octave = background_octave
|
436
|
+
@background_sharps = background_sharps
|
437
|
+
end
|
438
|
+
|
439
|
+
attr_reader :grade, :pitch
|
440
|
+
|
441
|
+
def functions
|
442
|
+
@scale.kind.class.pitches[grade][:functions]
|
443
|
+
end
|
444
|
+
|
445
|
+
def octave(octave = nil)
|
446
|
+
if octave.nil?
|
447
|
+
@octave
|
448
|
+
else
|
449
|
+
raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
|
450
|
+
|
451
|
+
@scale[@grade + (@octave + octave) * @scale.kind.class.grades]
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def with_background(scale:, grade: nil, octave: nil, sharps: nil)
|
456
|
+
NoteInScale.new(@scale, @grade, @octave, @pitch,
|
457
|
+
background_scale: scale,
|
458
|
+
background_grade: grade,
|
459
|
+
background_octave: octave,
|
460
|
+
background_sharps: sharps)
|
461
|
+
end
|
462
|
+
|
463
|
+
attr_reader :background_scale
|
464
|
+
|
465
|
+
def background_note
|
466
|
+
@background_scale[@background_grade + (@background_octave || 0) * @background_scale.kind.class.grades] if @background_grade
|
467
|
+
end
|
468
|
+
|
469
|
+
attr_reader :background_sharps
|
470
|
+
|
471
|
+
def wide_grade
|
472
|
+
@grade + @octave * @scale.kind.class.grades
|
473
|
+
end
|
474
|
+
|
475
|
+
def up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil)
|
476
|
+
|
477
|
+
sign ||= 1
|
478
|
+
|
479
|
+
if interval_name_or_interval.is_a?(Numeric)
|
480
|
+
natural_or_chromatic ||= :natural
|
481
|
+
else
|
482
|
+
natural_or_chromatic = :chromatic
|
483
|
+
end
|
484
|
+
|
485
|
+
if natural_or_chromatic == :chromatic
|
486
|
+
interval = if interval_name_or_interval.is_a?(Symbol)
|
487
|
+
@scale.kind.tuning.offset_of_interval(interval_name_or_interval)
|
488
|
+
else
|
489
|
+
interval_name_or_interval
|
490
|
+
end
|
491
|
+
|
492
|
+
calculate_note_of_pitch(@pitch, sign * interval)
|
493
|
+
else
|
494
|
+
@scale[@grade + sign * interval_name_or_interval]
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
def calculate_note_of_pitch(in_scale_pitch, sharps)
|
499
|
+
pitch = in_scale_pitch + sharps * @scale.kind.tuning.scale_system.part_of_tone_size
|
500
|
+
|
501
|
+
if pitch == @pitch
|
502
|
+
self
|
503
|
+
else
|
504
|
+
note = @scale.note_of_pitch(pitch, allow_chromatic: true)
|
505
|
+
if @background_scale
|
506
|
+
note.on(@background_scale) || note
|
507
|
+
else
|
508
|
+
note
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
private :calculate_note_of_pitch
|
514
|
+
|
515
|
+
def down(interval_name_or_interval, natural_or_chromatic = nil)
|
516
|
+
up(interval_name_or_interval, natural_or_chromatic, sign: -1)
|
517
|
+
end
|
518
|
+
|
519
|
+
def sharp(count = nil)
|
520
|
+
count ||= 1
|
521
|
+
calculate_note_of_pitch(@pitch, count)
|
522
|
+
end
|
523
|
+
|
524
|
+
def flat(count = nil)
|
525
|
+
count ||= 1
|
526
|
+
sharp(-count)
|
527
|
+
end
|
528
|
+
|
529
|
+
def frequency
|
530
|
+
@scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root)
|
531
|
+
end
|
532
|
+
|
533
|
+
def scale(kind_id_or_kind = nil)
|
534
|
+
if kind_id_or_kind.nil?
|
535
|
+
@scale
|
536
|
+
else
|
537
|
+
if kind_id_or_kind.is_a? ScaleKind
|
538
|
+
kind_id_or_kind[@pitch]
|
539
|
+
else
|
540
|
+
@scale.kind.tuning[kind_id_or_kind][@pitch]
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def on(scale)
|
546
|
+
scale.note_of_pitch @pitch
|
547
|
+
end
|
548
|
+
|
549
|
+
def chord(*feature_values, allow_chromatic: nil, **features_hash)
|
550
|
+
features = { size: :triad } if feature_values.empty? && features_hash.empty?
|
551
|
+
features ||= ChordDefinition.features_from(feature_values, features_hash)
|
552
|
+
|
553
|
+
Musa::Chord.new(root: self, allow_chromatic: allow_chromatic, features: features)
|
554
|
+
end
|
555
|
+
|
556
|
+
def ==(other)
|
557
|
+
self.class == other.class &&
|
558
|
+
@scale == other.scale &&
|
559
|
+
@grade == other.grade &&
|
560
|
+
@octave == other.octave &&
|
561
|
+
@pitch == other.pitch
|
562
|
+
end
|
563
|
+
|
564
|
+
def inspect
|
565
|
+
"<NoteInScale: grade = #{@grade} octave = #{@octave} pitch = #{@pitch} scale = (#{@scale.kind.class.name} on #{scale.root_pitch})>"
|
566
|
+
end
|
567
|
+
|
568
|
+
alias to_s inspect
|
569
|
+
|
570
|
+
private
|
571
|
+
|
572
|
+
def method_missing(method_name, *args, **key_args, &block)
|
573
|
+
if @scale.kind.tuning[method_name] && args.empty? && key_args.empty? && !block
|
574
|
+
scale(method_name)
|
575
|
+
else
|
576
|
+
super
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
def respond_to_missing?(method_name, include_private)
|
581
|
+
@scale.kind.tuning[method_name] || super
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|