musa-dsl 0.21.0 → 0.22.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/lib/musa-dsl.rb +1 -1
  4. data/lib/musa-dsl/core-ext.rb +1 -0
  5. data/lib/musa-dsl/core-ext/arrayfy.rb +9 -9
  6. data/lib/musa-dsl/core-ext/hashify.rb +42 -0
  7. data/lib/musa-dsl/core-ext/inspect-nice.rb +6 -1
  8. data/lib/musa-dsl/datasets/e.rb +22 -5
  9. data/lib/musa-dsl/datasets/gdv.rb +0 -1
  10. data/lib/musa-dsl/datasets/p.rb +28 -37
  11. data/lib/musa-dsl/datasets/pdv.rb +0 -1
  12. data/lib/musa-dsl/datasets/ps.rb +10 -78
  13. data/lib/musa-dsl/generative/markov.rb +1 -1
  14. data/lib/musa-dsl/logger/logger.rb +4 -3
  15. data/lib/musa-dsl/matrix/matrix.rb +0 -57
  16. data/lib/musa-dsl/midi/midi-voices.rb +4 -0
  17. data/lib/musa-dsl/neumas/string-to-neumas.rb +1 -0
  18. data/lib/musa-dsl/repl/repl.rb +30 -11
  19. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +87 -0
  20. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +439 -0
  21. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +3 -3
  22. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +210 -0
  23. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +178 -0
  24. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +150 -595
  25. data/lib/musa-dsl/sequencer/base-sequencer-public.rb +58 -5
  26. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +5 -9
  27. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +1 -5
  28. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +8 -0
  29. data/lib/musa-dsl/series/base-series.rb +43 -78
  30. data/lib/musa-dsl/series/flattener-timed-serie.rb +61 -0
  31. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +95 -0
  32. data/lib/musa-dsl/series/holder-serie.rb +1 -1
  33. data/lib/musa-dsl/series/main-serie-constructors.rb +29 -83
  34. data/lib/musa-dsl/series/main-serie-operations.rb +60 -215
  35. data/lib/musa-dsl/series/proxy-serie.rb +1 -1
  36. data/lib/musa-dsl/series/quantizer-serie.rb +546 -0
  37. data/lib/musa-dsl/series/queue-serie.rb +1 -1
  38. data/lib/musa-dsl/series/series.rb +7 -2
  39. data/lib/musa-dsl/transport/input-midi-clock.rb +19 -12
  40. data/lib/musa-dsl/transport/transport.rb +25 -12
  41. data/musa-dsl.gemspec +2 -2
  42. metadata +11 -5
  43. data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +0 -216
  44. data/lib/musa-dsl/series/hash-serie-splitter.rb +0 -196
@@ -2,6 +2,10 @@ require 'set'
2
2
  require 'midi-message'
3
3
 
4
4
  require_relative '../core-ext/arrayfy'
5
+ require_relative '../core-ext/array-explode-ranges'
6
+
7
+ using Musa::Extension::Arrayfy
8
+ using Musa::Extension::ExplodeRanges
5
9
 
6
10
  module Musa
7
11
  module MIDIVoices
@@ -24,6 +24,7 @@ module Musa
24
24
  end
25
25
  end
26
26
 
27
+
27
28
  alias_method :neumas, :to_neumas
28
29
  alias_method :n, :to_neumas
29
30
  alias_method :nn, :to_neumas_to_node
@@ -5,9 +5,10 @@ module Musa
5
5
  class REPL
6
6
  @@repl_mutex = Mutex.new
7
7
 
8
- def initialize(binder, port: nil, after_eval: nil)
8
+ def initialize(binder, port: nil, after_eval: nil, logger: nil)
9
9
  port ||= 1327
10
- redirect_stderr ||= false
10
+
11
+ @logger = logger || Musa::Logger::Logger.new
11
12
 
12
13
  @block_source = nil
13
14
 
@@ -15,7 +16,7 @@ module Musa
15
16
  binder.receiver.sequencer.respond_to?(:on_error)
16
17
 
17
18
  binder.receiver.sequencer.on_error do |e|
18
- send_exception e
19
+ send_exception e, output: @connection
19
20
  end
20
21
  end
21
22
 
@@ -25,26 +26,33 @@ module Musa
25
26
  @main_thread = Thread.new do
26
27
  @server = TCPServer.new(port)
27
28
  begin
28
- while (connection = @server.accept) && @run
29
+ while (@connection = @server.accept) && @run
29
30
  @client_threads << Thread.new do
30
31
  buffer = nil
31
32
 
32
33
  begin
33
- while (line = connection.gets) && @run
34
+ while (line = @connection.gets) && @run
35
+
36
+ @logger.warn('REPL') { 'input line is nil; will close connection...' } if line.nil?
37
+
34
38
  line.chomp!
35
39
  case line
36
40
  when '#begin'
37
41
  buffer = StringIO.new
42
+
38
43
  when '#end'
39
44
  @@repl_mutex.synchronize do
40
45
  @block_source = buffer.string
41
46
 
42
47
  begin
43
- send_echo @block_source, output: connection
48
+ send_echo @block_source, output: @connection
44
49
  binder.eval @block_source, "(repl)", 1
45
50
 
46
51
  rescue StandardError, ScriptError => e
47
- send_exception e, output: connection
52
+ @logger.warn('REPL') { 'code execution error' }
53
+ @logger.warn('REPL') { e.full_message(highlight: true, order: :top) }
54
+
55
+ send_exception e, output: @connection
48
56
  else
49
57
  after_eval.call @block_source if after_eval
50
58
  end
@@ -53,16 +61,23 @@ module Musa
53
61
  buffer.puts line
54
62
  end
55
63
  end
64
+
56
65
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e
57
- warn e.message
66
+ @logger.warn('REPL') { 'lost connection' }
67
+ @logger.warn('REPL') { e.full_message(highlight: true, order: :top) }
68
+
69
+ ensure
70
+ @logger.debug("REPL") { "closing connection (running #{@run})" }
71
+ @connection.close
58
72
  end
59
73
 
60
- connection.close
61
74
  end
62
75
  end
63
76
  rescue Errno::ECONNRESET, Errno::EPIPE => e
64
- warn e.message
77
+ @logger.warn('REPL') { 'connection failure while getting server port; will retry...' }
78
+ @logger.warn('REPL') { e.full_message(highlight: true, order: :top) }
65
79
  retry
80
+
66
81
  end
67
82
  end
68
83
  end
@@ -71,9 +86,11 @@ module Musa
71
86
  @run = false
72
87
 
73
88
  @main_thread.terminate
74
- @client_threads.each { |t| t.terminate }
89
+ Thread.pass
75
90
 
76
91
  @main_thread = nil
92
+
93
+ @client_threads.each { |t| t.terminate; Thread.pass }
77
94
  @client_threads.clear
78
95
  end
79
96
 
@@ -87,6 +104,8 @@ module Musa
87
104
 
88
105
  def send_exception(e, output:)
89
106
 
107
+ @logger.error('REPL') { e.full_message(highlight: true, order: :top) }
108
+
90
109
  send output: output, command: '//error'
91
110
 
92
111
  selected_backtrace_locations = e.backtrace_locations.select { |bt| bt.path == '(repl)' }
@@ -0,0 +1,87 @@
1
+ module Musa; module Sequencer
2
+ class BaseSequencer
3
+ private def _every(interval, control, block_procedure_binder: nil, &block)
4
+ block ||= proc {}
5
+
6
+ block_procedure_binder ||= SmartProcBinder.new block, on_rescue: proc { |e| _rescue_error(e) }
7
+
8
+ _numeric_at position, control do
9
+ control._start_position ||= position
10
+ control._execution_counter ||= 0
11
+
12
+ duration_exceeded =
13
+ (control._start_position + control.duration_value - interval) <= position if interval && control.duration_value
14
+
15
+ till_exceeded = control.till_value - interval <= position if interval && control.till_value
16
+
17
+ condition_failed = !control.condition_block.call if control.condition_block
18
+
19
+ unless control.stopped? || condition_failed || till_exceeded
20
+ block_procedure_binder.call(control: control)
21
+ control._execution_counter += 1
22
+ end
23
+
24
+
25
+ unless control.stopped? || duration_exceeded || till_exceeded || condition_failed || interval.nil?
26
+ _numeric_at control._start_position + control._execution_counter * interval, control do
27
+ _every interval, control, block_procedure_binder: block_procedure_binder
28
+ end
29
+
30
+ else
31
+ control.do_on_stop.each(&:call)
32
+
33
+ control.do_after.each do |do_after|
34
+ _numeric_at position + (interval || 0) + do_after[:bars], control, &do_after[:block]
35
+ end
36
+ end
37
+ end
38
+
39
+ nil
40
+ end
41
+
42
+ class EveryControl < EventHandler
43
+ attr_reader :duration_value, :till_value, :condition_block, :do_on_stop, :do_after
44
+
45
+ attr_accessor :_start_position
46
+ attr_accessor :_execution_counter
47
+
48
+ def initialize(parent, duration: nil, till: nil, condition: nil, on_stop: nil, after_bars: nil, after: nil)
49
+ super parent
50
+
51
+ @duration_value = duration
52
+ @till_value = till
53
+ @condition_block = condition
54
+
55
+ @do_on_stop = []
56
+ @do_after = []
57
+
58
+ @do_on_stop << on_stop if on_stop
59
+
60
+ self.after after_bars, &after if after
61
+ end
62
+
63
+ def duration(value)
64
+ @duration_value = value.rationalize
65
+ end
66
+
67
+ def till(value)
68
+ @till_value = value.rationalize
69
+ end
70
+
71
+ def condition(&block)
72
+ @condition_block = block
73
+ end
74
+
75
+ def on_stop(&block)
76
+ @do_on_stop << block
77
+ end
78
+
79
+ def after(bars = nil, &block)
80
+ bars ||= 0
81
+ @do_after << { bars: bars.rationalize, block: block }
82
+ end
83
+ end
84
+
85
+ private_constant :EveryControl
86
+ end
87
+ end; end
@@ -0,0 +1,439 @@
1
+ using Musa::Extension::Hashify
2
+ using Musa::Extension::Arrayfy
3
+
4
+ using Musa::Extension::InspectNice
5
+
6
+ module Musa; module Sequencer
7
+ class BaseSequencer
8
+ private def _move(every: nil,
9
+ from:, to: nil,
10
+ step: nil,
11
+ duration: nil, till: nil,
12
+ function: nil,
13
+ right_open: nil,
14
+ on_stop: nil,
15
+ after_bars: nil, after: nil,
16
+ &block)
17
+
18
+ #
19
+ # Main calling parameters error check
20
+ #
21
+ raise ArgumentError,
22
+ "Cannot use duration: #{duration} and till: #{till} parameters at the same time. " \
23
+ "Use only one of them." if till && duration
24
+
25
+ raise ArgumentError,
26
+ "Invalid use: 'function:' parameter is incompatible with 'step:' parameter" if function && step
27
+ raise ArgumentError,
28
+ "Invalid use: 'function:' parameter needs 'to:' parameter to be not nil" if function && !to
29
+
30
+ #
31
+ # Homogenize mode parameters
32
+ #
33
+ array_mode = from.is_a?(Array)
34
+ hash_mode = from.is_a?(Hash)
35
+
36
+ if array_mode
37
+ from = from.arrayfy
38
+ size = from.size
39
+
40
+ elsif hash_mode
41
+ hash_keys = from.keys
42
+ from = from.values
43
+ size = from.size
44
+
45
+ if every.is_a?(Hash)
46
+ every = hash_keys.collect { |k| every[k] }
47
+ raise ArgumentError,
48
+ "Invalid use: 'every:' parameter should contain the same keys as 'from:' Hash" \
49
+ unless every.all? { |_| _ }
50
+ end
51
+
52
+ if to.is_a?(Hash)
53
+ to = hash_keys.collect { |k| to[k] }
54
+ raise ArgumentError,
55
+ "Invalid use: 'to:' parameter should contain the same keys as 'from:' Hash" unless to.all? { |_| _ }
56
+ end
57
+
58
+ if step.is_a?(Hash)
59
+ step = hash_keys.collect { |k| step[k] }
60
+ end
61
+
62
+ if right_open.is_a?(Hash)
63
+ right_open = hash_keys.collect { |k| right_open[k] }
64
+ end
65
+
66
+ else
67
+ from = from.arrayfy
68
+ size = from.size
69
+ end
70
+
71
+ every = every.arrayfy(size: size)
72
+ to = to.arrayfy(size: size)
73
+ step = step.arrayfy(size: size)
74
+ right_open = right_open.arrayfy(size: size)
75
+
76
+ # from, to, step, every
77
+ # from, to, step, (duration | till)
78
+ # from, to, every, (duration | till)
79
+ # from, step, every, (duration | till)
80
+
81
+ block ||= proc {}
82
+
83
+ step.map!.with_index do |s, i|
84
+ (s && to[i] && ((s > 0 && to[i] < from[i]) || (s < 0 && from[i] < to[i]))) ? -s : s
85
+ end
86
+
87
+ right_open.map! { |v| v || false }
88
+
89
+ function ||= proc { |ratio| ratio }
90
+ function = function.arrayfy(size: size)
91
+
92
+ function_range = 1r.arrayfy(size: size)
93
+ function_offset = 0r.arrayfy(size: size)
94
+
95
+ #
96
+ # Prepare intervals, steps & transformation functions
97
+ #
98
+ start_position = position
99
+
100
+ if duration || till
101
+ effective_duration = duration || till - start_position
102
+
103
+ # Add 1 tick to arrive to final value in duration time (no need to add an extra tick)
104
+ right_open_offset = right_open.collect { |ro| ro ? 0 : 1 }
105
+
106
+ size.times do |i|
107
+ if to[i] && step[i] && !every[i]
108
+
109
+ steps = (to[i] - from[i]) / step[i]
110
+
111
+ # When to == from don't need to do any iteration with every
112
+ if steps + right_open_offset[i] > 0
113
+ every[i] = Rational(effective_duration, steps + right_open_offset[i])
114
+ else
115
+ every[i] = nil
116
+ end
117
+
118
+ elsif to[i] && !step[i] && !every[i]
119
+
120
+ if tick_duration > 0
121
+ function_range[i] = to[i] - from[i]
122
+ function_offset[i] = from[i]
123
+
124
+ from[i] = 0r
125
+ to[i] = 1r
126
+
127
+ step[i] = 1r / (effective_duration * ticks_per_bar - right_open_offset[i])
128
+ every[i] = tick_duration
129
+ else
130
+ raise ArgumentError, "Cannot use sequencer tickless mode without 'step' or 'every' parameter values"
131
+ end
132
+
133
+ elsif to[i] && !step[i] && every[i]
134
+ function_range[i] = to[i] - from[i]
135
+ function_offset[i] = from[i]
136
+
137
+ from[i] = 0r
138
+ to[i] = 1r
139
+
140
+ steps = effective_duration / every[i]
141
+ step[i] = 1r / (steps - right_open_offset[i])
142
+
143
+ elsif !to[i] && step[i] && every[i]
144
+ # ok
145
+ elsif !to[i] && !step[i] && every[i]
146
+ step[i] = 1r
147
+
148
+ else
149
+ raise ArgumentError, 'Cannot use this parameters combination (with \'duration\' or \'till\')'
150
+ end
151
+ end
152
+ else
153
+ size.times do |i|
154
+ if to[i] && step[i] && every[i]
155
+ # ok
156
+ elsif to[i] && !step[i] && every[i]
157
+ size.times do |i|
158
+ step[i] = (to[i] <=> from[i]).to_r
159
+ end
160
+ else
161
+ raise ArgumentError, 'Cannot use this parameters combination'
162
+ end
163
+ end
164
+ end
165
+
166
+ #
167
+ # Prepare yield block, parameters to yield block and coincident moving interval groups
168
+ #
169
+ binder = SmartProcBinder.new(block)
170
+
171
+ every_groups = {}
172
+ group_counter = {}
173
+
174
+ positions = Array.new(size)
175
+ q_durations = Array.new(size)
176
+ position_jitters = Array.new(size)
177
+ duration_jitters = Array.new(size)
178
+
179
+ size.times.each do |i|
180
+ every_groups[every[i]] ||= []
181
+ every_groups[every[i]] << i
182
+ group_counter[every[i]] = 0
183
+ end
184
+
185
+ #
186
+ # Initialize external control object
187
+ #
188
+ control = MoveControl.new(@event_handlers.last,
189
+ duration: duration, till: till,
190
+ on_stop: on_stop, after_bars: after_bars, after: after)
191
+
192
+ control.on_stop do
193
+ control.do_after.each do |do_after|
194
+ _numeric_at position + do_after[:bars], control, &do_after[:block]
195
+ end
196
+ end
197
+
198
+ @event_handlers.push control
199
+
200
+ #
201
+ # Let's go with the loop!
202
+ #
203
+ _numeric_at start_position, control do
204
+ next_values = from.dup
205
+
206
+ values = Array.new(size)
207
+ stop = Array.new(size, false)
208
+ last_position = Array.new(size)
209
+
210
+ #
211
+ # Ok, the loop is here...
212
+ #
213
+ _every _common_interval(every_groups.keys), control.every_control do
214
+ process_indexes = []
215
+
216
+ every_groups.each_pair do |group_interval, affected_indexes|
217
+ group_position = start_position + ((group_interval || 0) * group_counter[group_interval])
218
+
219
+ # We consider a position to be on current tick position when it is inside the interval of one tick
220
+ # centered on the current tick (current tick +- 1/2 tick duration).
221
+ # This allow to round the irregularly timed positions due to every intervals not integer
222
+ # multiples of the tick_duration.
223
+ #
224
+ if tick_duration == 0 && group_position == position ||
225
+ group_position >= position - tick_duration && group_position < position + tick_duration
226
+
227
+ process_indexes << affected_indexes
228
+
229
+ group_counter[group_interval] += 1
230
+
231
+ next_group_position = start_position +
232
+ if group_interval
233
+ (group_interval * group_counter[group_interval])
234
+ else
235
+ effective_duration
236
+ end
237
+
238
+ next_group_q_position = _quantize_position(next_group_position)
239
+
240
+ affected_indexes.each do |i|
241
+ positions[i] = group_position
242
+ q_durations[i] = next_group_q_position - position
243
+
244
+ position_jitters[i] = group_position - position
245
+ duration_jitters[i] = next_group_position - next_group_q_position
246
+ end
247
+ end
248
+ end
249
+
250
+ process_indexes.flatten!
251
+
252
+ #
253
+ # Calculate values and next_values for yield block
254
+ #
255
+ if process_indexes.any?
256
+ process_indexes.each do |i|
257
+ unless stop[i]
258
+ values[i] = next_values[i]
259
+ next_values[i] += step[i]
260
+
261
+ if to[i]
262
+ stop[i] = if right_open[i]
263
+ step[i].positive? ? next_values[i] >= to[i] : next_values[i] <= to[i]
264
+ else
265
+ step[i].positive? ? next_values[i] > to[i] : next_values[i] < to[i]
266
+ end
267
+
268
+ if stop[i]
269
+ if right_open[i]
270
+ next_values[i] = nil if values[i] == to[i]
271
+ else
272
+ next_values[i] = nil
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ #
280
+ # Do we need stop?
281
+ #
282
+ control.stop if stop.all?
283
+
284
+ #
285
+ # Calculate effective values and next_values applying the parameter function
286
+ #
287
+ effective_values = from.clone(freeze: false).map!.with_index do |_, i|
288
+ function[i].call(values[i]) * function_range[i] + function_offset[i] unless values[i].nil?
289
+ end
290
+
291
+ effective_next_values = from.clone(freeze: false).map!.with_index do |_, i|
292
+ function[i].call(next_values[i]) * function_range[i] +
293
+ function_offset[i] unless next_values[i].nil?
294
+ end
295
+
296
+ # TODO add to 'values' and 'next_values' elements the modules of the original from and/or to objects (i.e. GDV).
297
+
298
+ #
299
+ # Adapt values to array/hash/value mode
300
+ #
301
+ value_parameters, key_parameters =
302
+ if array_mode
303
+ binder.apply(effective_values, effective_next_values,
304
+ control: control,
305
+ duration: _durations(every_groups, effective_duration),
306
+ quantized_duration: q_durations.dup,
307
+ started_ago: _started_ago(last_position, position, process_indexes),
308
+ position_jitter: position_jitters.dup,
309
+ duration_jitter: duration_jitters.dup,
310
+ right_open: right_open.dup)
311
+ elsif hash_mode
312
+ binder.apply(_hash_from_keys_and_values(hash_keys, effective_values),
313
+ _hash_from_keys_and_values(hash_keys, effective_next_values),
314
+ control: control,
315
+ duration: _hash_from_keys_and_values(
316
+ hash_keys,
317
+ _durations(every_groups, effective_duration)),
318
+ quantized_duration: _hash_from_keys_and_values(
319
+ hash_keys,
320
+ q_durations),
321
+ started_ago: _hash_from_keys_and_values(
322
+ hash_keys,
323
+ _started_ago(last_position, position, process_indexes)),
324
+ position_jitter: _hash_from_keys_and_values(
325
+ hash_keys,
326
+ position_jitters),
327
+ duration_jitter: _hash_from_keys_and_values(
328
+ hash_keys,
329
+ duration_jitters),
330
+ right_open: _hash_from_keys_and_values(hash_keys, right_open))
331
+ else
332
+ binder.apply(effective_values.first,
333
+ effective_next_values.first,
334
+ control: control,
335
+ duration: _durations(every_groups, effective_duration).first,
336
+ quantized_duration: q_durations.first,
337
+ position_jitter: position_jitters.first,
338
+ duration_jitter: duration_jitters.first,
339
+ started_ago: nil,
340
+ right_open: right_open.first)
341
+ end
342
+
343
+ #
344
+ # Do the REAL thing
345
+ #
346
+ yield *value_parameters, **key_parameters
347
+
348
+ process_indexes.each { |i| last_position[i] = position }
349
+ end
350
+ end
351
+ end
352
+
353
+ @event_handlers.pop
354
+
355
+ control
356
+ end
357
+
358
+ private def _started_ago(last_positions, position, affected_indexes)
359
+ Array.new(last_positions.size).tap do |a|
360
+ last_positions.each_index do |i|
361
+ if last_positions[i] && !affected_indexes.include?(i)
362
+ a[i] = position - last_positions[i]
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ private def _durations(every_groups, largest_duration)
369
+ [].tap do |a|
370
+ if every_groups.any?
371
+ every_groups.each_pair do |every_group, affected_indexes|
372
+ affected_indexes.each do |i|
373
+ a[i] = every_group || largest_duration
374
+ end
375
+ end
376
+ else
377
+ a << largest_duration
378
+ end
379
+ end
380
+ end
381
+
382
+ private def _hash_from_keys_and_values(keys, values)
383
+ {}.tap { |h| keys.each_index { |i| h[keys[i]] = values[i] } }
384
+ end
385
+
386
+ private def _common_interval(intervals)
387
+ intervals = intervals.compact
388
+ return nil if intervals.empty?
389
+
390
+ lcm_denominators = intervals.collect(&:denominator).reduce(1, :lcm)
391
+ numerators = intervals.collect { |i| i.numerator * lcm_denominators / i.denominator }
392
+ gcd_numerators = numerators.reduce(numerators.first, :gcd)
393
+
394
+ #intervals.reduce(1r, :*)
395
+
396
+ Rational(gcd_numerators, lcm_denominators)
397
+ end
398
+
399
+ class MoveControl < EventHandler
400
+ attr_reader :every_control, :do_on_stop, :do_after
401
+
402
+ def initialize(parent, duration: nil, till: nil, on_stop: nil, after_bars: nil, after: nil)
403
+ super parent
404
+
405
+ @every_control = EveryControl.new(self, duration: duration, till: till)
406
+
407
+ @do_on_stop = []
408
+ @do_after = []
409
+
410
+ @do_on_stop << on_stop if on_stop
411
+ self.after after_bars, &after if after
412
+
413
+ @every_control.on_stop do
414
+ @stop = true
415
+ @do_on_stop.each(&:call)
416
+ end
417
+ end
418
+
419
+ def on_stop(&block)
420
+ @do_on_stop << block
421
+ end
422
+
423
+ def after(bars = nil, &block)
424
+ bars ||= 0
425
+ @do_after << { bars: bars.rationalize, block: block }
426
+ end
427
+
428
+ def stop
429
+ @every_control.stop
430
+ end
431
+
432
+ def stopped?
433
+ @stop
434
+ end
435
+ end
436
+
437
+ private_constant :MoveControl
438
+ end
439
+ end; end