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,331 @@
|
|
1
|
+
require 'musa-dsl/core-ext/key-parameters-procedure-binder'
|
2
|
+
require 'musa-dsl/core-ext/arrayfy'
|
3
|
+
|
4
|
+
# TODO: permitir definir un variatio a través de llamadas a métodos y/o atributos, además de a través del block del constructor
|
5
|
+
|
6
|
+
module Musa
|
7
|
+
class Variatio
|
8
|
+
def initialize(instance_name, &block)
|
9
|
+
raise ArgumentError, 'instance_name should be a symbol' unless instance_name.is_a?(Symbol)
|
10
|
+
raise ArgumentError, 'block is needed' unless block
|
11
|
+
|
12
|
+
@instance_name = instance_name
|
13
|
+
|
14
|
+
main_context = MainContext.new block
|
15
|
+
|
16
|
+
@constructor = main_context._constructor
|
17
|
+
@fieldset = main_context._fieldset
|
18
|
+
@finalize = main_context._finalize
|
19
|
+
end
|
20
|
+
|
21
|
+
def on(**values)
|
22
|
+
constructor_binder = KeyParametersProcedureBinder.new @constructor
|
23
|
+
finalize_binder = KeyParametersProcedureBinder.new @finalize if @finalize
|
24
|
+
|
25
|
+
run_fieldset = @fieldset.clone # TODO: verificar que esto no da problemas
|
26
|
+
|
27
|
+
run_fieldset.components.each do |component|
|
28
|
+
if values.key? component.name
|
29
|
+
component.options = values[component.name].arrayfy.explode_ranges
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
tree_A = generate_eval_tree_A run_fieldset
|
34
|
+
tree_B = generate_eval_tree_B run_fieldset
|
35
|
+
|
36
|
+
parameters_set = tree_A.calc_parameters
|
37
|
+
|
38
|
+
combinations = []
|
39
|
+
|
40
|
+
parameters_set.each do |parameters_with_depth|
|
41
|
+
instance = @constructor.call constructor_binder.apply(parameters_with_depth)
|
42
|
+
|
43
|
+
tree_B.run parameters_with_depth, @instance_name => instance
|
44
|
+
|
45
|
+
if @finalize
|
46
|
+
finalize_parameters = finalize_binder.apply parameters_with_depth
|
47
|
+
finalize_parameters[@instance_name] = instance
|
48
|
+
|
49
|
+
@finalize.call finalize_parameters
|
50
|
+
end
|
51
|
+
|
52
|
+
combinations << instance
|
53
|
+
end
|
54
|
+
|
55
|
+
combinations
|
56
|
+
end
|
57
|
+
|
58
|
+
def run
|
59
|
+
on
|
60
|
+
end
|
61
|
+
|
62
|
+
module Helper
|
63
|
+
module_function
|
64
|
+
|
65
|
+
def list_of_hashes_product(list_of_hashes_1, list_of_hashes_2)
|
66
|
+
result = []
|
67
|
+
|
68
|
+
list_of_hashes_1.each do |hash1|
|
69
|
+
list_of_hashes_2.each do |hash2|
|
70
|
+
result << hash1.merge(hash2)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private_constant :Helper
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def generate_eval_tree_A(fieldset)
|
83
|
+
root = nil
|
84
|
+
current = nil
|
85
|
+
|
86
|
+
fieldset.components.each do |component|
|
87
|
+
if component.is_a? Field
|
88
|
+
a = A1.new component.name, component.options
|
89
|
+
elsif component.is_a? Fieldset
|
90
|
+
a = A2.new component.name, component.options, generate_eval_tree_A(component)
|
91
|
+
end
|
92
|
+
|
93
|
+
current.inner = a if current
|
94
|
+
root ||= a
|
95
|
+
|
96
|
+
current = a
|
97
|
+
end
|
98
|
+
|
99
|
+
root
|
100
|
+
end
|
101
|
+
|
102
|
+
def generate_eval_tree_B(fieldset)
|
103
|
+
affected_field_names = []
|
104
|
+
inner = []
|
105
|
+
|
106
|
+
fieldset.components.each do |component|
|
107
|
+
if component.is_a? Fieldset
|
108
|
+
inner << generate_eval_tree_B(component)
|
109
|
+
elsif component.is_a? Field
|
110
|
+
affected_field_names << component.name
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
B.new fieldset.name, fieldset.options, affected_field_names, inner, fieldset.with_attributes
|
115
|
+
end
|
116
|
+
|
117
|
+
class A
|
118
|
+
attr_reader :parameter_name, :options
|
119
|
+
attr_accessor :inner
|
120
|
+
|
121
|
+
def initialize(parameter_name, options)
|
122
|
+
@parameter_name = parameter_name
|
123
|
+
@options = options
|
124
|
+
@inner = nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def calc_parameters
|
128
|
+
unless @calc_parameters
|
129
|
+
if inner
|
130
|
+
@calc_parameters = Helper.list_of_hashes_product(calc_own_parameters, @inner.calc_parameters)
|
131
|
+
else
|
132
|
+
@calc_parameters = calc_own_parameters
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
@calc_parameters
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
private_constant :A
|
141
|
+
|
142
|
+
class A1 < A
|
143
|
+
def initialize(parameter_name, options)
|
144
|
+
super parameter_name, options
|
145
|
+
|
146
|
+
@own_parameters = @options.collect { |option| { @parameter_name => option } }
|
147
|
+
end
|
148
|
+
|
149
|
+
def calc_own_parameters
|
150
|
+
@own_parameters
|
151
|
+
end
|
152
|
+
|
153
|
+
def inspect
|
154
|
+
"A1 name: #{@parameter_name}, options: #{@options}, inner: #{@inner || 'nil'}"
|
155
|
+
end
|
156
|
+
|
157
|
+
alias to_s inspect
|
158
|
+
end
|
159
|
+
|
160
|
+
private_constant :A1
|
161
|
+
|
162
|
+
class A2 < A
|
163
|
+
def initialize(parameter_name, options, subcomponent)
|
164
|
+
super parameter_name, options
|
165
|
+
|
166
|
+
@subcomponent = subcomponent
|
167
|
+
|
168
|
+
sub_parameters_set = @subcomponent.calc_parameters
|
169
|
+
result = nil
|
170
|
+
|
171
|
+
@options.each do |option|
|
172
|
+
if result.nil?
|
173
|
+
result = sub_parameters_set.collect { |v| { option => v } }
|
174
|
+
else
|
175
|
+
result = Helper.list_of_hashes_product result, sub_parameters_set.collect { |v| { option => v } }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
result = result.collect { |v| { @parameter_name => v } }
|
180
|
+
|
181
|
+
@own_parameters = result
|
182
|
+
end
|
183
|
+
|
184
|
+
def calc_own_parameters
|
185
|
+
@own_parameters
|
186
|
+
end
|
187
|
+
|
188
|
+
def inspect
|
189
|
+
"A2 name: #{@parameter_name}, options: #{@options}, subcomponent: #{@subcomponent}, inner: #{@inner || 'nil'}"
|
190
|
+
end
|
191
|
+
|
192
|
+
alias to_s inspect
|
193
|
+
end
|
194
|
+
|
195
|
+
private_constant :A2
|
196
|
+
|
197
|
+
class B
|
198
|
+
attr_reader :parameter_name, :options, :affected_field_names, :blocks, :inner
|
199
|
+
|
200
|
+
def initialize(parameter_name, options, affected_field_names, inner, blocks)
|
201
|
+
@parameter_name = parameter_name
|
202
|
+
@options = options
|
203
|
+
@affected_field_names = affected_field_names
|
204
|
+
@inner = inner
|
205
|
+
|
206
|
+
@procedures = blocks.collect { |proc| KeyParametersProcedureBinder.new proc }
|
207
|
+
end
|
208
|
+
|
209
|
+
def run(parameters_with_depth, parent_parameters = nil)
|
210
|
+
parent_parameters ||= {}
|
211
|
+
|
212
|
+
@options.each do |option|
|
213
|
+
base = @parameter_name == :_maincontext ? parameters_with_depth : parameters_with_depth[@parameter_name][option]
|
214
|
+
|
215
|
+
parameters = base.select { |k, _v| @affected_field_names.include? k }.merge(parent_parameters)
|
216
|
+
parameters[@parameter_name] = option
|
217
|
+
|
218
|
+
@procedures.each do |procedure_binder|
|
219
|
+
procedure_binder.call parameters
|
220
|
+
end
|
221
|
+
|
222
|
+
if @parameter_name == :_maincontext
|
223
|
+
@inner.each do |inner|
|
224
|
+
inner.run parameters_with_depth, parameters
|
225
|
+
end
|
226
|
+
else
|
227
|
+
@inner.each do |inner|
|
228
|
+
inner.run parameters_with_depth[@parameter_name][option], parameters
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def inspect
|
235
|
+
"B name: #{@parameter_name}, options: #{@options}, affected_field_names: #{@affected_field_names}, blocks_size: #{@blocks.size}, inner: #{@inner}"
|
236
|
+
end
|
237
|
+
|
238
|
+
alias to_s inspect
|
239
|
+
|
240
|
+
private
|
241
|
+
end
|
242
|
+
|
243
|
+
class FieldsetContext
|
244
|
+
attr_reader :_fieldset
|
245
|
+
|
246
|
+
def initialize(name, options = nil, block)
|
247
|
+
@_fieldset = Fieldset.new name, options.arrayfy.explode_ranges
|
248
|
+
|
249
|
+
_as_context_run block
|
250
|
+
end
|
251
|
+
|
252
|
+
def field(name, options = nil)
|
253
|
+
@_fieldset.components << Field.new(name, options.arrayfy.explode_ranges)
|
254
|
+
end
|
255
|
+
|
256
|
+
def fieldset(name, options = nil, &block)
|
257
|
+
fieldset_context = FieldsetContext.new name, options, block
|
258
|
+
@_fieldset.components << fieldset_context._fieldset
|
259
|
+
end
|
260
|
+
|
261
|
+
def with_attributes(&block)
|
262
|
+
@_fieldset.with_attributes << block
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
private_constant :FieldsetContext
|
267
|
+
|
268
|
+
class MainContext < FieldsetContext
|
269
|
+
attr_reader :_constructor, :_finalize
|
270
|
+
|
271
|
+
def initialize(block)
|
272
|
+
@_constructor = nil
|
273
|
+
@_finalize = nil
|
274
|
+
|
275
|
+
super :_maincontext, [nil], block
|
276
|
+
end
|
277
|
+
|
278
|
+
def constructor(&block)
|
279
|
+
@_constructor = block
|
280
|
+
end
|
281
|
+
|
282
|
+
def finalize(&block)
|
283
|
+
@_finalize = block
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
private_constant :MainContext
|
288
|
+
|
289
|
+
class Field
|
290
|
+
attr_reader :name
|
291
|
+
attr_accessor :options
|
292
|
+
|
293
|
+
def inspect
|
294
|
+
"Field #{@name} options: #{@options}"
|
295
|
+
end
|
296
|
+
|
297
|
+
alias to_s inspect
|
298
|
+
|
299
|
+
private
|
300
|
+
|
301
|
+
def initialize(name, options)
|
302
|
+
@name = name
|
303
|
+
@options = options
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
private_constant :Field
|
308
|
+
|
309
|
+
class Fieldset
|
310
|
+
attr_reader :name, :with_attributes, :components
|
311
|
+
attr_accessor :options
|
312
|
+
|
313
|
+
def inspect
|
314
|
+
"Fieldset #{@name} options: #{@options} components: #{@components}"
|
315
|
+
end
|
316
|
+
|
317
|
+
alias to_s inspect
|
318
|
+
|
319
|
+
private
|
320
|
+
|
321
|
+
def initialize(name, options)
|
322
|
+
@name = name
|
323
|
+
@options = options || [nil]
|
324
|
+
@components = []
|
325
|
+
@with_attributes = []
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
private_constant :Fieldset
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'nibbler'
|
2
|
+
|
3
|
+
module Musa
|
4
|
+
class MIDIRecorder
|
5
|
+
def initialize(sequencer)
|
6
|
+
@sequencer = sequencer
|
7
|
+
@nibbler = Nibbler.new
|
8
|
+
|
9
|
+
clear
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear
|
13
|
+
@messages = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def record(midi_bytes)
|
17
|
+
m = @nibbler.parse midi_bytes
|
18
|
+
m = [m] unless m.is_a? Array
|
19
|
+
|
20
|
+
m.each do |mm|
|
21
|
+
@messages << Message.new(@sequencer.position, mm)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def raw
|
26
|
+
@messages
|
27
|
+
end
|
28
|
+
|
29
|
+
def transcription
|
30
|
+
note_on = {}
|
31
|
+
last_note = {}
|
32
|
+
|
33
|
+
notes = []
|
34
|
+
|
35
|
+
@messages.each do |m|
|
36
|
+
mm = m.message
|
37
|
+
|
38
|
+
if mm.is_a?(MIDIMessage::NoteOn)
|
39
|
+
|
40
|
+
if last_note[mm.channel]
|
41
|
+
notes << { position: last_note[mm.channel], channel: mm.channel, pitch: :silence, duration: m.position - last_note[mm.channel] }
|
42
|
+
last_note.delete mm.channel
|
43
|
+
end
|
44
|
+
|
45
|
+
note = { position: m.position, channel: mm.channel, pitch: mm.note, velocity: mm.velocity }
|
46
|
+
|
47
|
+
note_on[mm.channel] ||= {}
|
48
|
+
note_on[mm.channel][mm.note] = note
|
49
|
+
|
50
|
+
notes << note
|
51
|
+
|
52
|
+
elsif mm.is_a?(MIDIMessage::NoteOff)
|
53
|
+
|
54
|
+
note_on[mm.channel] ||= {}
|
55
|
+
|
56
|
+
note = note_on[mm.channel][mm.note]
|
57
|
+
|
58
|
+
if note
|
59
|
+
note_on[mm.channel].delete mm.note
|
60
|
+
|
61
|
+
note[:duration] = m.position - note[:position]
|
62
|
+
note[:velocity_off] = mm.velocity
|
63
|
+
end
|
64
|
+
|
65
|
+
last_note[mm.channel] = m.position
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
notes
|
70
|
+
end
|
71
|
+
|
72
|
+
class Message
|
73
|
+
attr_accessor :position, :message
|
74
|
+
|
75
|
+
def initialize(position, message)
|
76
|
+
@position = position
|
77
|
+
@message = message
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private_constant :Message
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'midi-message'
|
3
|
+
|
4
|
+
require 'musa-dsl/core-ext/array-apply-get'
|
5
|
+
require 'musa-dsl/core-ext/arrayfy'
|
6
|
+
|
7
|
+
module Musa
|
8
|
+
class MIDIVoices
|
9
|
+
attr_accessor :log
|
10
|
+
|
11
|
+
def initialize(sequencer:, output:, channels:, do_log: nil)
|
12
|
+
do_log ||= false
|
13
|
+
|
14
|
+
@sequencer = sequencer
|
15
|
+
@output = output
|
16
|
+
@channels = channels.arrayfy.explode_ranges
|
17
|
+
@do_log = do_log
|
18
|
+
|
19
|
+
reset
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset
|
23
|
+
@voices = @channels.collect { |channel| MIDIVoice.new sequencer: @sequencer, output: @output, channel: channel, log: @do_log }.freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :voices
|
27
|
+
|
28
|
+
def fast_forward=(enabled)
|
29
|
+
@voices.apply :fast_forward=, enabled
|
30
|
+
end
|
31
|
+
|
32
|
+
def panic(reset: nil)
|
33
|
+
reset ||= false
|
34
|
+
|
35
|
+
@voices.each(&:all_notes_off)
|
36
|
+
|
37
|
+
@output.puts MIDIMessage::SystemRealtime.new(0xff) if reset
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
class MIDIVoice
|
44
|
+
attr_accessor :name, :do_log
|
45
|
+
attr_reader :sequencer, :output, :channel, :active_pitches, :tick_duration
|
46
|
+
|
47
|
+
def initialize(sequencer:, output:, channel:, name: nil, log: nil)
|
48
|
+
log ||= false
|
49
|
+
|
50
|
+
@sequencer = sequencer
|
51
|
+
@output = output
|
52
|
+
@channel = channel
|
53
|
+
@name = name
|
54
|
+
@do_log = log
|
55
|
+
|
56
|
+
@tick_duration = Rational(1, @sequencer.ticks_per_bar)
|
57
|
+
|
58
|
+
@controllers_control = ControllersControl.new(@output, @channel)
|
59
|
+
|
60
|
+
@active_pitches = []
|
61
|
+
fill_active_pitches @active_pitches
|
62
|
+
|
63
|
+
log 'Warning: voice without output' unless @output
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def fast_forward=(enabled)
|
69
|
+
if @fast_forward && !enabled
|
70
|
+
(0..127).each do |pitch|
|
71
|
+
@output.puts MIDIMessage::NoteOn.new(@channel, pitch, @active_pitches[pitch][:velocity]) unless @active_pitches[pitch][:note_controls].empty?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@fast_forward = enabled
|
76
|
+
end
|
77
|
+
|
78
|
+
def fast_forward?
|
79
|
+
@fast_forward
|
80
|
+
end
|
81
|
+
|
82
|
+
def note(pitchvalue = nil, pitch: nil, velocity: nil, duration: nil, duration_offset: nil, effective_duration: nil, velocity_off: nil)
|
83
|
+
pitch ||= pitchvalue
|
84
|
+
|
85
|
+
if pitch
|
86
|
+
velocity ||= 63
|
87
|
+
|
88
|
+
duration_offset ||= -@tick_duration
|
89
|
+
effective_duration ||= [0, duration + duration_offset].max
|
90
|
+
|
91
|
+
velocity_off ||= 63
|
92
|
+
|
93
|
+
NoteControl.new(self, pitch: pitch, velocity: velocity, duration: effective_duration, velocity_off: velocity_off).note_on
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def controller
|
98
|
+
@controllers_control
|
99
|
+
end
|
100
|
+
|
101
|
+
def sustain_pedal=(value)
|
102
|
+
@controllers_control[:sustain_pedal] = value
|
103
|
+
end
|
104
|
+
|
105
|
+
def sustain_pedal
|
106
|
+
@controllers_control[:sustain_pedal]
|
107
|
+
end
|
108
|
+
|
109
|
+
def all_notes_off
|
110
|
+
@active_pitches.clear
|
111
|
+
fill_active_pitches @active_pitches
|
112
|
+
|
113
|
+
@output.puts MIDIMessage::ChannelMessage.new(0xb, @channel, 0x7b, 0)
|
114
|
+
end
|
115
|
+
|
116
|
+
def log(msg)
|
117
|
+
@sequencer.log "voice #{name || @channel}: #{msg}" if @do_log
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_s
|
121
|
+
"voice #{@name} output: #{@output} channel: #{@channel}"
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def fill_active_pitches(pitches)
|
127
|
+
(0..127).each do |pitch|
|
128
|
+
pitches[pitch] = { note_controls: Set[], velocity: 0 }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class ControllersControl
|
133
|
+
def initialize(output, channel)
|
134
|
+
@output = output
|
135
|
+
@channel = channel
|
136
|
+
|
137
|
+
@controller_map = { sustain_pedal: 0x40 }
|
138
|
+
@controller = []
|
139
|
+
end
|
140
|
+
|
141
|
+
def []=(controller_number_or_symbol, value)
|
142
|
+
number = number_of(controller_number_or_symbol)
|
143
|
+
value ||= 0
|
144
|
+
|
145
|
+
@controller[number] = [[0, value].max, 0xff].min
|
146
|
+
@output.puts MIDIMessage::ChannelMessage.new(0xb, @channel, number, @controller[number])
|
147
|
+
end
|
148
|
+
|
149
|
+
def [](controller_number_or_symbol)
|
150
|
+
@controller[number_of(controller_number_or_symbol)]
|
151
|
+
end
|
152
|
+
|
153
|
+
def number_of(controller_number_or_symbol)
|
154
|
+
case controller_number_or_symbol
|
155
|
+
when Numeric
|
156
|
+
controller_number_or_symbol.to_i
|
157
|
+
when Symbol
|
158
|
+
@controller_map[controller_number_or_symbol]
|
159
|
+
else
|
160
|
+
raise ArgumentError, "#{controller_number_or_symbol} is not a Numeric nor a Symbol. Only MIDI controller numbers are allowed"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private_constant :ControllersControl
|
166
|
+
|
167
|
+
class NoteControl
|
168
|
+
attr_reader :start_position, :end_position
|
169
|
+
|
170
|
+
def initialize(voice, pitch:, velocity: nil, duration: nil, velocity_off: nil)
|
171
|
+
raise ArgumentError, "MIDIVoice: note duration should be nil or Numeric: #{duration} (#{duration.class})" unless duration.nil? || duration.is_a?(Numeric)
|
172
|
+
|
173
|
+
@voice = voice
|
174
|
+
|
175
|
+
@pitch = pitch.arrayfy.explode_ranges
|
176
|
+
|
177
|
+
@velocity = velocity.arrayfy.explode_ranges
|
178
|
+
@velocity_off = velocity_off.arrayfy.explode_ranges
|
179
|
+
|
180
|
+
@duration = duration
|
181
|
+
|
182
|
+
@do_on_stop = []
|
183
|
+
@do_after = []
|
184
|
+
|
185
|
+
@start_position = @end_position = nil
|
186
|
+
end
|
187
|
+
|
188
|
+
def note_on
|
189
|
+
@start_position = @voice.sequencer.position
|
190
|
+
@end_position = nil
|
191
|
+
|
192
|
+
@pitch.each_index do |i|
|
193
|
+
pitch = @pitch[i]
|
194
|
+
velocity = @velocity[i % @velocity.size]
|
195
|
+
|
196
|
+
if !silence?(pitch)
|
197
|
+
@voice.active_pitches[pitch][:note_controls] << self
|
198
|
+
@voice.active_pitches[pitch][:velocity] = velocity
|
199
|
+
|
200
|
+
msg = MIDIMessage::NoteOn.new(@voice.channel, pitch, velocity)
|
201
|
+
@voice.log "#{msg.verbose_name} velocity: #{velocity} duration: #{@duration}"
|
202
|
+
@voice.output.puts msg if @voice.output && !@voice.fast_forward?
|
203
|
+
else
|
204
|
+
@voice.log "silence duration: #{@duration}"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
return self unless @duration
|
209
|
+
|
210
|
+
this = self
|
211
|
+
@voice.sequencer.wait @duration do
|
212
|
+
this.note_off velocity: @velocity_off
|
213
|
+
end
|
214
|
+
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
def note_off(velocity: nil)
|
219
|
+
velocity ||= @velocity_off
|
220
|
+
|
221
|
+
velocity = velocity.arrayfy.explode_ranges
|
222
|
+
|
223
|
+
@pitch.each_index do |i|
|
224
|
+
pitch = @pitch[i]
|
225
|
+
velocity_off = velocity[i % velocity.size]
|
226
|
+
|
227
|
+
next if silence?(pitch)
|
228
|
+
|
229
|
+
@voice.active_pitches[pitch][:note_controls].delete self
|
230
|
+
|
231
|
+
next unless @voice.active_pitches[pitch][:note_controls].empty?
|
232
|
+
|
233
|
+
msg = MIDIMessage::NoteOff.new(@voice.channel, pitch, velocity_off)
|
234
|
+
@voice.log msg.verbose_name.to_s
|
235
|
+
@voice.output.puts msg if @voice.output && !@voice.fast_forward?
|
236
|
+
end
|
237
|
+
|
238
|
+
@end_position = @voice.sequencer.position
|
239
|
+
|
240
|
+
@do_on_stop.each do |do_on_stop|
|
241
|
+
@voice.sequencer.wait 0, &do_on_stop
|
242
|
+
end
|
243
|
+
|
244
|
+
@do_after.each do |do_after|
|
245
|
+
@voice.sequencer.wait @voice.tick_duration + do_after[:bars], &do_after[:block]
|
246
|
+
end
|
247
|
+
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
|
251
|
+
def active?
|
252
|
+
@start_position && !@end_position
|
253
|
+
end
|
254
|
+
|
255
|
+
def on_stop(&block)
|
256
|
+
@do_on_stop << block
|
257
|
+
nil
|
258
|
+
end
|
259
|
+
|
260
|
+
def after(bars = 0, &block)
|
261
|
+
@do_after << { bars: bars.rationalize, block: block }
|
262
|
+
nil
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
def silence?(pitch)
|
268
|
+
pitch.nil? || pitch == :silence
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
private_constant :NoteControl
|
273
|
+
end
|
274
|
+
end
|