musa-dsl 0.21.1 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/musa-dsl.rb +1 -1
  3. data/lib/musa-dsl/core-ext.rb +1 -0
  4. data/lib/musa-dsl/core-ext/arrayfy.rb +9 -9
  5. data/lib/musa-dsl/core-ext/hashify.rb +42 -0
  6. data/lib/musa-dsl/core-ext/inspect-nice.rb +6 -1
  7. data/lib/musa-dsl/datasets/e.rb +22 -5
  8. data/lib/musa-dsl/datasets/gdv.rb +0 -1
  9. data/lib/musa-dsl/datasets/p.rb +28 -37
  10. data/lib/musa-dsl/datasets/pdv.rb +0 -1
  11. data/lib/musa-dsl/datasets/ps.rb +10 -78
  12. data/lib/musa-dsl/logger/logger.rb +4 -3
  13. data/lib/musa-dsl/matrix/matrix.rb +0 -57
  14. data/lib/musa-dsl/midi/midi-voices.rb +4 -0
  15. data/lib/musa-dsl/repl/repl.rb +30 -11
  16. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +87 -0
  17. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +439 -0
  18. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +3 -3
  19. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +210 -0
  20. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +178 -0
  21. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +150 -595
  22. data/lib/musa-dsl/sequencer/base-sequencer-public.rb +58 -5
  23. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +5 -9
  24. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +1 -5
  25. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +8 -0
  26. data/lib/musa-dsl/series/base-series.rb +43 -78
  27. data/lib/musa-dsl/series/flattener-timed-serie.rb +61 -0
  28. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +95 -0
  29. data/lib/musa-dsl/series/holder-serie.rb +1 -1
  30. data/lib/musa-dsl/series/main-serie-constructors.rb +29 -83
  31. data/lib/musa-dsl/series/main-serie-operations.rb +60 -215
  32. data/lib/musa-dsl/series/proxy-serie.rb +1 -1
  33. data/lib/musa-dsl/series/quantizer-serie.rb +546 -0
  34. data/lib/musa-dsl/series/queue-serie.rb +1 -1
  35. data/lib/musa-dsl/series/series.rb +7 -2
  36. data/lib/musa-dsl/transport/input-midi-clock.rb +19 -12
  37. data/lib/musa-dsl/transport/transport.rb +25 -12
  38. data/musa-dsl.gemspec +2 -2
  39. metadata +10 -4
  40. data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +0 -216
  41. 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
@@ -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