musa-dsl 0.14.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/Gemfile +20 -0
  4. data/LICENSE.md +157 -0
  5. data/README.md +8 -0
  6. data/lib/musa-dsl/core-ext/array-apply-get.rb +18 -0
  7. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +29 -0
  8. data/lib/musa-dsl/core-ext/array-to-neumas.rb +28 -0
  9. data/lib/musa-dsl/core-ext/array-to-serie.rb +20 -0
  10. data/lib/musa-dsl/core-ext/arrayfy.rb +15 -0
  11. data/lib/musa-dsl/core-ext/as-context-run.rb +44 -0
  12. data/lib/musa-dsl/core-ext/duplicate.rb +134 -0
  13. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +55 -0
  14. data/lib/musa-dsl/core-ext/inspect-nice.rb +28 -0
  15. data/lib/musa-dsl/core-ext/key-parameters-procedure-binder.rb +85 -0
  16. data/lib/musa-dsl/core-ext/proc-nice.rb +13 -0
  17. data/lib/musa-dsl/core-ext/send-nice.rb +21 -0
  18. data/lib/musa-dsl/core-ext/string-to-neumas.rb +27 -0
  19. data/lib/musa-dsl/core-ext.rb +13 -0
  20. data/lib/musa-dsl/datasets/gdv-decorators.rb +221 -0
  21. data/lib/musa-dsl/datasets/gdv.rb +499 -0
  22. data/lib/musa-dsl/datasets/pdv.rb +44 -0
  23. data/lib/musa-dsl/datasets.rb +5 -0
  24. data/lib/musa-dsl/generative/darwin.rb +145 -0
  25. data/lib/musa-dsl/generative/generative-grammar.rb +294 -0
  26. data/lib/musa-dsl/generative/markov.rb +78 -0
  27. data/lib/musa-dsl/generative/rules.rb +282 -0
  28. data/lib/musa-dsl/generative/variatio.rb +331 -0
  29. data/lib/musa-dsl/generative.rb +5 -0
  30. data/lib/musa-dsl/midi/midi-recorder.rb +83 -0
  31. data/lib/musa-dsl/midi/midi-voices.rb +274 -0
  32. data/lib/musa-dsl/midi.rb +2 -0
  33. data/lib/musa-dsl/music/chord-definition.rb +99 -0
  34. data/lib/musa-dsl/music/chord-definitions.rb +13 -0
  35. data/lib/musa-dsl/music/chords.rb +326 -0
  36. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +204 -0
  37. data/lib/musa-dsl/music/scales.rb +584 -0
  38. data/lib/musa-dsl/music.rb +6 -0
  39. data/lib/musa-dsl/neuma/neuma.rb +181 -0
  40. data/lib/musa-dsl/neuma.rb +1 -0
  41. data/lib/musa-dsl/neumalang/neumalang.citrus +294 -0
  42. data/lib/musa-dsl/neumalang/neumalang.rb +179 -0
  43. data/lib/musa-dsl/neumalang.rb +3 -0
  44. data/lib/musa-dsl/repl/repl.rb +143 -0
  45. data/lib/musa-dsl/repl.rb +1 -0
  46. data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +189 -0
  47. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +354 -0
  48. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +382 -0
  49. data/lib/musa-dsl/sequencer/base-sequencer-public.rb +261 -0
  50. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +94 -0
  51. data/lib/musa-dsl/sequencer/sequencer.rb +3 -0
  52. data/lib/musa-dsl/sequencer.rb +1 -0
  53. data/lib/musa-dsl/series/base-series.rb +245 -0
  54. data/lib/musa-dsl/series/hash-serie-splitter.rb +194 -0
  55. data/lib/musa-dsl/series/holder-serie.rb +87 -0
  56. data/lib/musa-dsl/series/main-serie-constructors.rb +726 -0
  57. data/lib/musa-dsl/series/main-serie-operations.rb +1151 -0
  58. data/lib/musa-dsl/series/proxy-serie.rb +69 -0
  59. data/lib/musa-dsl/series/queue-serie.rb +94 -0
  60. data/lib/musa-dsl/series/series.rb +8 -0
  61. data/lib/musa-dsl/series.rb +1 -0
  62. data/lib/musa-dsl/transport/clock.rb +36 -0
  63. data/lib/musa-dsl/transport/dummy-clock.rb +47 -0
  64. data/lib/musa-dsl/transport/external-tick-clock.rb +31 -0
  65. data/lib/musa-dsl/transport/input-midi-clock.rb +124 -0
  66. data/lib/musa-dsl/transport/timer-clock.rb +102 -0
  67. data/lib/musa-dsl/transport/timer.rb +40 -0
  68. data/lib/musa-dsl/transport/transport.rb +137 -0
  69. data/lib/musa-dsl/transport.rb +9 -0
  70. data/lib/musa-dsl.rb +17 -0
  71. data/musa-dsl.gemspec +17 -0
  72. 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,5 @@
1
+ require 'musa-dsl/generative/variatio'
2
+ require 'musa-dsl/generative/darwin'
3
+ require 'musa-dsl/generative/rules'
4
+ require 'musa-dsl/generative/markov'
5
+ require 'musa-dsl/generative/generative-grammar'
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'musa-dsl/midi/midi-voices'
2
+ require 'musa-dsl/midi/midi-recorder'