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.
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'