musa-dsl 0.42.2 → 0.42.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12fab469b50ab2d8c92ce166859645d8c30ad655e2ae339aa9b8c0254cc59253
4
- data.tar.gz: 87aa5596a08c21f0b17ed7f6758b708a3473f9e8078020b7cf564b2149f6823a
3
+ metadata.gz: 5b00aaa2474967b2283adc187b2bd8696b96a83ba98b5868c8479f69e1efa195
4
+ data.tar.gz: bf3e363b1160ae7787a9a64d1bd96cd8c2735d80a39449268f01bff1af57d8e9
5
5
  SHA512:
6
- metadata.gz: 38b45b016289a63870ada189d4ea91f9749754997ad4e250b5d24b1ea47c001741696e71eef8ce13dfeb4e6753caf213f970861fbbff2edbcee8f18504a9e03e
7
- data.tar.gz: 6a14e6306e5f3945fa5e5febfa223fd1aece1cbff1d0ed8533a5a54231646bfddf96489a572d8aae1adbeb6b0dbbaee1f3aa1ef9effa8095c051378b145bde9a
6
+ metadata.gz: 19349d6b67aac7e4e00f95d1da0c7b59b1f3d8c4bbb7ea6dd9d92d335f416b4ac0ceebc4647389d8fa901a763e3af2e6443281f5dfa7268f155be1503b2fec2a
7
+ data.tar.gz: e90b0fac158af0990e6fccffbb683001b5f7d641775a5678fe879d16ba905002c5817e56b089977e28632090a54f69be95ddf578f780db3c93d2caf05931dd40
@@ -102,7 +102,11 @@ every 1/4r do ... end
102
102
  at 0.5 do ... end
103
103
  ```
104
104
 
105
- ## Control Callbacks: `on_stop` vs `after`
105
+ ## Control Objects and `.stop`
106
+
107
+ All scheduling methods (`at`, `wait`, `now`, `play`, `play_timed`, `every`, `move`) return a control object that supports `.stop` to cancel execution. Calling `.stop` on the control prevents the associated block from running at its scheduled position, or stops further iterations for series/recurring operations.
108
+
109
+ ### `on_stop` vs `after` callbacks
106
110
 
107
111
  The control objects returned by `every`, `play`, `play_timed`, and `move` support two types of callbacks with different semantics:
108
112
 
@@ -153,6 +157,46 @@ ctrl.on_stop { puts "Fade ended" } # Any reason
153
157
  ctrl.after { launch :next_section } # Only if fade completes
154
158
  ```
155
159
 
160
+ ### Stopping `at`, `wait`, `now` and `play_timed`
161
+
162
+ All scheduling methods return a control object that supports `.stop`:
163
+
164
+ ```ruby
165
+ # Stop a scheduled at/wait/now before it executes
166
+ h = at 5 do
167
+ puts "This won't execute if stopped before bar 5"
168
+ end
169
+
170
+ at 3 do
171
+ h.stop # Cancels the block scheduled at bar 5
172
+ end
173
+ ```
174
+
175
+ ```ruby
176
+ # Stop a series-based at/wait
177
+ h = at [1, 2, 3, 4, 5] do
178
+ puts "Repeating at positions from series"
179
+ end
180
+
181
+ at 3.5 do
182
+ h.stop # No more executions from the series after this point
183
+ end
184
+ ```
185
+
186
+ ```ruby
187
+ # Stop play_timed — same on_stop/after semantics as play/every/move
188
+ ctrl = play_timed(timed_serie) do |values, time:, started_ago:, control:|
189
+ # process values
190
+ end
191
+
192
+ ctrl.on_stop { puts "Stopped (any reason)" }
193
+ ctrl.after { launch :next_section } # Only on natural end
194
+
195
+ at 10 do
196
+ ctrl.stop # on_stop fires, after does NOT
197
+ end
198
+ ```
199
+
156
200
  ### Parameter form
157
201
 
158
202
  `on_stop` and `after` can also be passed as parameters:
@@ -79,7 +79,7 @@ module Musa::Sequencer
79
79
  last_positions,
80
80
  binder, control)
81
81
 
82
- source_next_value = timed_serie.next_value
82
+ source_next_value = control.stopped? ? nil : timed_serie.next_value
83
83
 
84
84
  if source_next_value
85
85
  affected_components = component_ids.select { |_| !source_next_value[:value][_].nil? }
@@ -109,11 +109,13 @@ module Musa::Sequencer
109
109
  end
110
110
 
111
111
  _numeric_at _quantize_position(start_position + time, warn: true), control do
112
- binder.call(values,
113
- **extra_attributes,
114
- time: start_position + time,
115
- started_ago: started_ago,
116
- control: control)
112
+ unless control.stopped?
113
+ binder.call(values,
114
+ **extra_attributes,
115
+ time: start_position + time,
116
+ started_ago: started_ago,
117
+ control: control)
118
+ end
117
119
 
118
120
  _play_timed_step(hash_mode,
119
121
  component_ids, extra_attribute_names,
@@ -19,12 +19,21 @@ module Musa::Sequencer
19
19
  #
20
20
  # - Shifts command from queue
21
21
  # - Deletes timeslot if queue becomes empty
22
- # - Pushes parent_control to event handler stack if present
23
- # - Executes command block with parameters (mutex-protected)
22
+ # - Pushes parent_control to event handler stack if present and not stopped
23
+ # - If command has skip_if_stopped and control is stopped, skips block execution
24
+ # - Otherwise executes command block with parameters (mutex-protected)
24
25
  # - Pops parent_control from stack
25
26
  #
26
27
  # 4. Yields to other threads
27
28
  #
29
+ # ## skip_if_stopped
30
+ #
31
+ # Commands scheduled with `skip_if_stopped: true` (via `_numeric_at`) are
32
+ # silently skipped when their control is stopped. Used by `at`, `wait`, `now`
33
+ # and `_serie_at` which have no cleanup logic. Not used by `play`, `every`,
34
+ # `move` or `play_timed` whose blocks mix user callbacks with cleanup/recursion
35
+ # that must execute even when stopped.
36
+ #
28
37
  # @param position_to_run [Rational] position to execute events at
29
38
  #
30
39
  # @return [void]
@@ -43,8 +52,10 @@ module Musa::Sequencer
43
52
 
44
53
  @event_handlers.push(command[:parent_control]) if push_parent
45
54
 
46
- @tick_mutex.synchronize do
47
- command[:block]&.call *command[:value_parameters], **command[:key_parameters]
55
+ unless command[:skip_if_stopped] && command[:parent_control]&.stopped?
56
+ @tick_mutex.synchronize do
57
+ command[:block]&.call *command[:value_parameters], **command[:key_parameters]
58
+ end
48
59
  end
49
60
 
50
61
  @event_handlers.pop if push_parent
@@ -65,7 +76,7 @@ module Musa::Sequencer
65
76
  # @param force_first [Boolean] if true, insert at front of queue
66
77
  # @yield block to execute at position
67
78
  #
68
- # @return [nil]
79
+ # @return [void]
69
80
  #
70
81
  # @example Force execution order
71
82
  # _raw_numeric_at(1r) { puts "second" }
@@ -115,9 +126,23 @@ module Musa::Sequencer
115
126
  # - **Past position**: Warns and ignores
116
127
  # - **nil position** (tickless before first event): Allows scheduling
117
128
  #
129
+ # ## skip_if_stopped
130
+ #
131
+ # When `skip_if_stopped: true`, the block is silently skipped if `control`
132
+ # is stopped at execution time. This applies both for immediate execution
133
+ # (current position) and for future execution (checked in `_tick`).
134
+ # Used by `at`, `wait`, `now` and `_serie_at` for clean cancellation
135
+ # without wrapper procs or extra SmartProcBinder overhead.
136
+ #
137
+ # Not suitable for `play`/`every`/`move`/`play_timed` whose scheduled
138
+ # blocks contain cleanup logic (do_on_stop, do_after) that must run
139
+ # even when stopped.
140
+ #
118
141
  # @param at_position [Rational] position to schedule at
119
142
  # @param control [EventHandler] parent control for hierarchy
120
143
  # @param debug [Boolean, nil] enable debug callbacks
144
+ # @param skip_if_stopped [Boolean, nil] when true, skip block execution
145
+ # if control is stopped. Used by at/wait/now/_serie_at.
121
146
  # @yield block to execute at position (may accept control:)
122
147
  #
123
148
  # @return [nil]
@@ -125,7 +150,7 @@ module Musa::Sequencer
125
150
  # @raise [ArgumentError] if at_position is nil or block not given
126
151
  #
127
152
  # @api private
128
- private def _numeric_at(at_position, control, debug: nil, &block)
153
+ private def _numeric_at(at_position, control, debug: nil, skip_if_stopped: nil, &block)
129
154
  raise ArgumentError, "'at_position' parameter cannot be nil" if at_position.nil?
130
155
  raise ArgumentError, 'Yield block is mandatory' unless block
131
156
 
@@ -140,11 +165,13 @@ module Musa::Sequencer
140
165
  if at_position == @position
141
166
  @on_debug_at.each(&:call) if @logger.sev_threshold >= ::Logger::Severity::DEBUG
142
167
 
143
- begin
144
- locked = @tick_mutex.try_lock
145
- block_key_parameters_binder._call(nil, key_parameters)
146
- ensure
147
- @tick_mutex.unlock if locked
168
+ unless skip_if_stopped && control.stopped?
169
+ begin
170
+ locked = @tick_mutex.try_lock
171
+ block_key_parameters_binder._call(nil, key_parameters)
172
+ ensure
173
+ @tick_mutex.unlock if locked
174
+ end
148
175
  end
149
176
 
150
177
  elsif @position.nil? || at_position > @position
@@ -159,7 +186,8 @@ module Musa::Sequencer
159
186
 
160
187
  @timeslots[at_position] << { parent_control: control,
161
188
  block: block_key_parameters_binder,
162
- key_parameters: key_parameters }
189
+ key_parameters: key_parameters,
190
+ skip_if_stopped: skip_if_stopped }
163
191
  else
164
192
  @logger.warn('BaseSequencer') { "._numeric_at: ignoring past 'at' command for #{at_position}" }
165
193
  end
@@ -201,9 +229,9 @@ module Musa::Sequencer
201
229
  bar_position = position_or_serie.next_value
202
230
 
203
231
  if bar_position
204
- _numeric_at bar_position, control, debug: debug, &block
232
+ _numeric_at bar_position, control, debug: debug, skip_if_stopped: true, &block
205
233
 
206
- _numeric_at bar_position, control, debug: false do
234
+ _numeric_at bar_position, control, debug: false, skip_if_stopped: true do
207
235
  _serie_at position_or_serie, control, debug: debug, &block
208
236
  end
209
237
  else
@@ -522,13 +522,22 @@ module Musa
522
522
 
523
523
  # Schedules block relative to current position.
524
524
  #
525
+ # Returns a control object whose `.stop` method cancels execution:
526
+ # the block will not run if the control is stopped before its scheduled
527
+ # position. For series-based delays, `.stop` also prevents further
528
+ # elements from being scheduled.
529
+ #
525
530
  # @param bars_delay [Numeric, Series, Array] delay from current position
526
531
  # @param debug [Boolean] enable debug logging
527
532
  # @yield block to execute at position + delay
528
- # @return [EventHandler] control object
533
+ # @return [EventHandler] control object (supports .stop to cancel)
529
534
  #
530
- # @example
535
+ # @example Basic wait
531
536
  # seq.wait(2) { puts "2 beats later" }
537
+ #
538
+ # @example Cancelling a scheduled wait
539
+ # h = seq.wait(4) { puts "won't run" }
540
+ # h.stop
532
541
  def wait(bars_delay, debug: nil, &block)
533
542
  debug ||= false
534
543
 
@@ -536,7 +545,7 @@ module Musa
536
545
  @event_handlers.push control
537
546
 
538
547
  if bars_delay.is_a? Numeric
539
- _numeric_at position + bars_delay.rationalize, control, debug: debug, &block
548
+ _numeric_at position + bars_delay.rationalize, control, debug: debug, skip_if_stopped: true, &block
540
549
  else
541
550
  bars_delay = Series::S(*bars_delay) if bars_delay.is_a?(Array)
542
551
  bars_delay = bars_delay.instance if bars_delay
@@ -551,8 +560,12 @@ module Musa
551
560
 
552
561
  # Schedules block at current position (immediate execution on next tick).
553
562
  #
563
+ # Returns a control object whose `.stop` method cancels execution
564
+ # if the block hasn't run yet (e.g., scheduled at current position
565
+ # but not yet reached by the tick loop).
566
+ #
554
567
  # @yield block to execute at current position
555
- # @return [EventHandler] control object
568
+ # @return [EventHandler] control object (supports .stop to cancel)
556
569
  #
557
570
  # @example
558
571
  # seq.now { puts "Executes now" }
@@ -560,7 +573,7 @@ module Musa
560
573
  control = EventHandler.new @event_handlers.last
561
574
  @event_handlers.push control
562
575
 
563
- _numeric_at position, control, &block
576
+ _numeric_at position, control, skip_if_stopped: true, &block
564
577
 
565
578
  @event_handlers.pop
566
579
 
@@ -582,16 +595,25 @@ module Musa
582
595
 
583
596
  # Schedules block at absolute position.
584
597
  #
598
+ # Returns a control object whose `.stop` method cancels execution:
599
+ # the block will not run if the control is stopped before the scheduled
600
+ # position. For series-based positions, `.stop` also prevents further
601
+ # elements from being scheduled.
602
+ #
585
603
  # @param bar_position [Numeric, Series, Array] absolute position(s)
586
604
  # @param debug [Boolean] enable debug logging
587
605
  # @yield block to execute at position
588
- # @return [EventHandler] control object
606
+ # @return [EventHandler] control object (supports .stop to cancel)
589
607
  #
590
608
  # @example Single position
591
609
  # seq.at(4) { puts "At beat 4" }
592
610
  #
593
611
  # @example Series of positions
594
612
  # seq.at([1, 2, 3.5, 4]) { |pos| puts "At #{pos}" }
613
+ #
614
+ # @example Cancelling a scheduled at
615
+ # h = seq.at(8) { puts "won't run" }
616
+ # h.stop
595
617
  def at(bar_position, debug: nil, &block)
596
618
  debug ||= false
597
619
 
@@ -599,7 +621,7 @@ module Musa
599
621
  @event_handlers.push control
600
622
 
601
623
  if bar_position.is_a? Numeric
602
- _numeric_at bar_position.rationalize, control, debug: debug, &block
624
+ _numeric_at bar_position.rationalize, control, debug: debug, skip_if_stopped: true, &block
603
625
  else
604
626
  bar_position = Series::S(*bar_position) if bar_position.is_a? Array
605
627
  bar_position = bar_position.instance if bar_position
@@ -222,7 +222,7 @@ module Musa
222
222
  # @param value_parameters [Array] parameters to pass to block
223
223
  # @param key_parameters [Hash] keyword parameters
224
224
  # @yield block to execute now
225
- # @return [void]
225
+ # @return [EventHandler] control object
226
226
 
227
227
  # @!method at(position, *value_parameters, **key_parameters, &block)
228
228
  # Schedules block to execute at specified position.
@@ -233,7 +233,7 @@ module Musa
233
233
  # @param value_parameters [Array] parameters to pass to block
234
234
  # @param key_parameters [Hash] keyword parameters
235
235
  # @yield block to execute at position
236
- # @return [void]
236
+ # @return [EventHandler] control object
237
237
 
238
238
  # @!method wait(duration, *value_parameters, **key_parameters, &block)
239
239
  # Schedules block after waiting specified duration.
@@ -244,7 +244,7 @@ module Musa
244
244
  # @param value_parameters [Array] parameters to pass to block
245
245
  # @param key_parameters [Hash] keyword parameters
246
246
  # @yield block to execute after wait
247
- # @return [void]
247
+ # @return [EventHandler] control object
248
248
 
249
249
  # @!method play(serie, decoder: nil, mode: nil, **options, &block)
250
250
  # Plays a series using the decoder.
@@ -596,7 +596,7 @@ module Musa
596
596
  # @param value_parameters [Array] parameters to pass to block
597
597
  # @param key_parameters [Hash] keyword parameters
598
598
  # @yield block to execute at current position
599
- # @return [void]
599
+ # @return [EventHandler] control object
600
600
  def now(*value_parameters, **key_parameters, &block)
601
601
  block ||= proc {}
602
602
 
@@ -612,7 +612,7 @@ module Musa
612
612
  # @param value_parameters [Array] parameters (first is position)
613
613
  # @param key_parameters [Hash] keyword parameters
614
614
  # @yield block to execute at position
615
- # @return [void]
615
+ # @return [EventHandler] control object
616
616
  def at(*value_parameters, **key_parameters, &block)
617
617
  block ||= proc {}
618
618
 
@@ -628,7 +628,7 @@ module Musa
628
628
  # @param value_parameters [Array] parameters (first is duration)
629
629
  # @param key_parameters [Hash] keyword parameters
630
630
  # @yield block to execute after wait
631
- # @return [void]
631
+ # @return [EventHandler] control object
632
632
  def wait(*value_parameters, **key_parameters, &block)
633
633
  block ||= proc {}
634
634
  @sequencer.wait *value_parameters, **key_parameters do |*values, **key_values|
@@ -1,3 +1,3 @@
1
1
  module Musa
2
- VERSION = '0.42.2'.freeze
2
+ VERSION = '0.42.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: musa-dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.42.2
4
+ version: 0.42.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Sánchez Yeste