musa-dsl 0.14.16
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 +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
|