musa-dsl 0.30.2 → 0.40.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -11,13 +11,167 @@ require_relative 'base-sequencer-tickless-based'
11
11
 
12
12
  module Musa
13
13
  module Sequencer
14
+ # Musical sequencer and scheduler system.
15
+ #
16
+ # Sequencer provides precise timing and scheduling for musical events,
17
+ # supporting both tick-based (quantized) and tickless (continuous) timing
18
+ # modes. Events are scheduled with musical time units (bars, beats, ticks)
19
+ # and executed sequentially.
20
+ #
21
+ # ## Core Concepts
22
+ #
23
+ # - **Position**: Current playback position in beats
24
+ # - **Timeslots**: Scheduled events indexed by time
25
+ # - **Timing Modes**:
26
+ #
27
+ # - **Tick-based**: Quantized to beats_per_bar × ticks_per_beat grid
28
+ # - **Tickless**: Continuous rational time (no quantization)
29
+ #
30
+ # - **Scheduling Methods**:
31
+ #
32
+ # - `at`: Schedule block at absolute position
33
+ # - `wait`: Schedule relative to current position
34
+ # - `play`: Play series over time
35
+ # - `every`: Repeat at intervals
36
+ # - `move`: Animate value over time
37
+ #
38
+ # - **Event Handlers**: Hierarchical event pub/sub system
39
+ # - **Controls**: Objects returned by scheduling methods for lifecycle management
40
+ #
41
+ # ## Tick-based vs Tickless
42
+ #
43
+ # **Tick-based** (beats_per_bar and ticks_per_beat specified):
44
+ #
45
+ # - Positions quantized to tick grid
46
+ # - `tick` method advances by one tick
47
+ # - Suitable for MIDI-like discrete timing
48
+ # - Example: `BaseSequencer.new(4, 24)` → 4/4 time, 24 ticks per beat
49
+ #
50
+ # **Tickless** (no timing parameters):
51
+ #
52
+ # - Continuous rational time
53
+ # - `tick(position)` jumps to arbitrary position
54
+ # - Suitable for score-like continuous timing
55
+ # - Example: `BaseSequencer.new` → tickless mode
56
+ #
57
+ # ## Musical Time Units
58
+ #
59
+ # - **Bar**: Musical measure (defaults to 1.0 in value)
60
+ # - **Beat**: Subdivision of bar (e.g., quarter note in 4/4)
61
+ # - **Tick**: Smallest time quantum in tick-based mode
62
+ # - All times are Rational for precision
63
+ #
64
+ # @example Basic tick-based sequencer
65
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24) # 4/4, 24 ticks/beat
66
+ #
67
+ # seq.at(1) { puts "Beat 1" }
68
+ # seq.at(2) { puts "Beat 2" }
69
+ # seq.at(3.5) { puts "Beat 3.5" }
70
+ #
71
+ # seq.run # Executes all scheduled events
72
+ #
73
+ # @example Tickless sequencer
74
+ # seq = Musa::Sequencer::BaseSequencer.new # Tickless mode
75
+ #
76
+ # seq.at(1) { puts "Position 1" }
77
+ # seq.at(1.5) { puts "Position 1.5" }
78
+ #
79
+ # seq.tick(1) # Jumps to position 1
80
+ # seq.tick(1.5) # Jumps to position 1.5
81
+ #
82
+ # @example Playing series
83
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
84
+ #
85
+ # pitches = Musa::Series::S(60, 62, 64, 65, 67)
86
+ # durations = Musa::Series::S(1, 1, 0.5, 0.5, 2)
87
+ # played_notes = []
88
+ #
89
+ # seq.play(pitches.zip(durations)) do |pitch, duration|
90
+ # played_notes << { pitch: pitch, duration: duration, position: seq.position }
91
+ # end
92
+ #
93
+ # seq.run
94
+ # # Result: played_notes contains [{pitch: 60, duration: 1, position: 0}, ...]
95
+ #
96
+ # @example Every and move
97
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
98
+ #
99
+ # tick_positions = []
100
+ # volume_values = []
101
+ #
102
+ # # Execute every beat
103
+ # seq.every(1, till: 8) { tick_positions << seq.position }
104
+ #
105
+ # # Animate value from 0 to 127 over 4 beats
106
+ # seq.move(every: 1/4r, from: 0, to: 127, duration: 4) do |value|
107
+ # volume_values << value.round
108
+ # end
109
+ #
110
+ # seq.run
111
+ # # Result: tick_positions = [0, 1, 2, 3, 4, 5, 6, 7]
112
+ # # Result: volume_values = [0, 8, 16, ..., 119, 127]
113
+ #
114
+ # @api public
14
115
  class BaseSequencer
15
- attr_reader :beats_per_bar, :ticks_per_beat
116
+ # @return [Rational, nil] beats per bar (tick-based mode only)
117
+ attr_reader :beats_per_bar
118
+
119
+ # @return [Rational, nil] ticks per beat (tick-based mode only)
120
+ attr_reader :ticks_per_beat
121
+
122
+ # @return [Rational] time offset for position calculations
16
123
  attr_reader :offset
124
+
125
+ # @return [Rational] current running position
17
126
  attr_reader :running_position
18
- attr_reader :everying, :playing, :moving
127
+
128
+ # @return [Array<EveryControl>] active every loops
129
+ attr_reader :everying
130
+
131
+ # @return [Array<PlayControl, PlayTimedControl>] active play operations
132
+ attr_reader :playing
133
+
134
+ # @return [Array<MoveControl>] active move operations
135
+ attr_reader :moving
136
+
137
+ # @return [Musa::Logger::Logger] sequencer logger
19
138
  attr_reader :logger
20
139
 
140
+ # Creates sequencer with timing configuration.
141
+ #
142
+ # ## Timing Modes
143
+ #
144
+ # **Tick-based**: Provide both beats_per_bar and ticks_per_beat
145
+ #
146
+ # - Position quantized to tick grid
147
+ # - `tick` advances by one tick
148
+ #
149
+ # **Tickless**: Omit beats_per_bar and ticks_per_beat
150
+ #
151
+ # - Continuous rational time
152
+ # - `tick` advances to next scheduled position (without timing quantization)
153
+ #
154
+ # @param beats_per_bar [Numeric, nil] beats per bar (nil for tickless)
155
+ # @param ticks_per_beat [Numeric, nil] ticks per beat (nil for tickless)
156
+ # @param offset [Rational, nil] starting position offset
157
+ # @param logger [Musa::Logger::Logger, nil] custom logger
158
+ # @param do_log [Boolean, nil] enable debug logging
159
+ # @param do_error_log [Boolean, nil] enable error logging
160
+ # @param log_position_format [Proc, nil] custom position formatter for logs
161
+ #
162
+ # @raise [ArgumentError] if only one of beats_per_bar/ticks_per_beat provided
163
+ #
164
+ # @example Tick-based 4/4 time
165
+ # seq = BaseSequencer.new(4, 24)
166
+ #
167
+ # @example Tick-based 3/4 time
168
+ # seq = BaseSequencer.new(3, 24)
169
+ #
170
+ # @example Tickless mode
171
+ # seq = BaseSequencer.new
172
+ #
173
+ # @example With offset
174
+ # seq = BaseSequencer.new(4, 24, offset: 10r)
21
175
  def initialize(beats_per_bar = nil, ticks_per_beat = nil,
22
176
  offset: nil,
23
177
  logger: nil,
@@ -68,6 +222,30 @@ module Musa
68
222
  reset
69
223
  end
70
224
 
225
+ # Resets sequencer to initial state.
226
+ #
227
+ # Clears all scheduled events, active operations, and event handlers.
228
+ # Resets timing to start position.
229
+ #
230
+ # @return [void]
231
+ #
232
+ # @example Resetting sequencer state
233
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
234
+ #
235
+ # # Schedule some events
236
+ # seq.at(1) { puts "Event 1" }
237
+ # seq.at(2) { puts "Event 2" }
238
+ # seq.every(1, till: 8) { puts "Repeating" }
239
+ #
240
+ # puts seq.size # => 2 (scheduled events)
241
+ # puts seq.empty? # => false
242
+ #
243
+ # # Reset clears everything
244
+ # seq.reset
245
+ #
246
+ # puts seq.size # => 0
247
+ # puts seq.empty? # => true
248
+ # puts seq.position # => 0
71
249
  def reset
72
250
  @timeslots.clear
73
251
  @everying.clear
@@ -79,51 +257,278 @@ module Musa
79
257
  _reset_timing
80
258
  end
81
259
 
260
+ # Counts total scheduled events.
261
+ #
262
+ # @return [Integer] number of scheduled events across all timeslots
82
263
  def size
83
264
  @timeslots.values.sum(&:size)
84
265
  end
85
266
 
267
+ # Checks if sequencer has no scheduled events.
268
+ #
269
+ # @return [Boolean] true if no events scheduled
86
270
  def empty?
87
271
  @timeslots.empty?
88
272
  end
89
273
 
274
+ # Quantizes position to tick grid (tick-based mode only).
275
+ #
276
+ # @param position [Rational] position to quantize
277
+ # @param warn [Boolean] emit warning if quantization changes value
278
+ #
279
+ # @return [Rational] quantized position
90
280
  def quantize_position(position, warn: nil)
91
281
  warn ||= false
92
282
  _quantize_position(position, warn: warn)
93
283
  end
94
284
 
285
+ # Executes all scheduled events until empty.
286
+ #
287
+ # Advances time tick by tick (or position by position in tickless mode)
288
+ # until no events remain.
289
+ #
290
+ # @return [void]
291
+ #
292
+ # @example
293
+ # seq.at(1) { puts "Event 1" }
294
+ # seq.at(2) { puts "Event 2" }
295
+ # seq.run # Executes both events
95
296
  def run
96
297
  tick until empty?
97
298
  end
98
299
 
300
+ # Returns current event handler.
301
+ #
302
+ # @return [EventHandler] active event handler
303
+ # @api private
99
304
  def event_handler
100
305
  @event_handlers.last
101
306
  end
102
307
 
308
+ # Registers debug callback for scheduled events.
309
+ #
310
+ # Callback is invoked when debug logging is enabled (see do_log parameter in
311
+ # initialize). Called before executing each scheduled event, allowing inspection
312
+ # of sequencer state at event execution time.
313
+ #
314
+ # @yield debug callback (receives no parameters)
315
+ # @return [void]
316
+ #
317
+ # @example Monitoring event execution
318
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24, do_log: true)
319
+ #
320
+ # debug_calls = []
321
+ #
322
+ # seq.on_debug_at do
323
+ # debug_calls << { position: seq.position, time: Time.now }
324
+ # end
325
+ #
326
+ # seq.at(1) { puts "Event 1" }
327
+ # seq.at(2) { puts "Event 2" }
328
+ #
329
+ # seq.run
330
+ #
331
+ # # debug_calls now contains [{position: 1, time: ...}, {position: 2, time: ...}]
103
332
  def on_debug_at(&block)
104
333
  @on_debug_at << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
105
334
  end
106
335
 
336
+ # Registers error callback.
337
+ #
338
+ # Callback is invoked when an error occurs during event execution. The error
339
+ # is logged and passed to all registered error handlers. Handlers receive the
340
+ # exception object and can process or report it.
341
+ #
342
+ # @yield [error] error callback receiving the exception object
343
+ # @yieldparam error [StandardError, ScriptError] the exception that occurred
344
+ # @return [void]
345
+ #
346
+ # @example Handling errors in scheduled events
347
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24, do_error_log: false)
348
+ #
349
+ # errors = []
350
+ #
351
+ # seq.on_error do |error|
352
+ # errors << { message: error.message, position: seq.position }
353
+ # end
354
+ #
355
+ # seq.at(1) { puts "Normal event" }
356
+ # seq.at(2) { raise "Something went wrong!" }
357
+ # seq.at(3) { puts "This still executes" }
358
+ #
359
+ # seq.run
360
+ #
361
+ # # errors now contains [{message: "Something went wrong!", position: 2}]
362
+ # # All events execute despite the error at position 2
107
363
  def on_error(&block)
108
364
  @on_error << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
109
365
  end
110
366
 
367
+ # Registers fast-forward callback (when jumping over events).
368
+ #
369
+ # Callback is invoked when position is changed directly (via position=), causing
370
+ # the sequencer to skip ahead. Called twice: once with true when fast-forward
371
+ # begins, and once with false when it completes. Events between old and new
372
+ # positions are executed during fast-forward.
373
+ #
374
+ # @yield [is_starting] callback receiving fast-forward state
375
+ # @yieldparam is_starting [Boolean] true when fast-forward begins, false when it ends
376
+ # @return [void]
377
+ #
378
+ # @example Tracking fast-forward operations
379
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
380
+ #
381
+ # ff_state = []
382
+ #
383
+ # seq.on_fast_forward do |is_starting|
384
+ # if is_starting
385
+ # ff_state << "Fast-forward started from position #{seq.position}"
386
+ # else
387
+ # ff_state << "Fast-forward ended at position #{seq.position}"
388
+ # end
389
+ # end
390
+ #
391
+ # seq.at(1) { puts "Event 1" }
392
+ # seq.at(5) { puts "Event 5" }
393
+ #
394
+ # # Jump to position 10 (executes events at 1 and 5 during fast-forward)
395
+ # seq.position = 10
396
+ #
397
+ # # ff_state contains ["Fast-forward started from position 0", "Fast-forward ended at position 10"]
111
398
  def on_fast_forward(&block)
112
399
  @on_fast_forward << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
113
400
  end
114
401
 
402
+ # Registers callback executed before each tick.
403
+ #
404
+ # Callback is invoked before processing events at each position. Useful for
405
+ # logging, metrics collection, or performing pre-tick setup. Receives the
406
+ # position about to be executed.
407
+ #
408
+ # @yield [position] callback receiving the upcoming position
409
+ # @yieldparam position [Rational] the position about to be processed
410
+ # @return [void]
411
+ #
412
+ # @example Logging tick positions
413
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
414
+ #
415
+ # tick_log = []
416
+ #
417
+ # seq.before_tick do |position|
418
+ # tick_log << position
419
+ # end
420
+ #
421
+ # seq.at(1) { puts "Event" }
422
+ # seq.at(2) { puts "Event" }
423
+ #
424
+ # seq.tick # Executes position 1
425
+ # seq.tick # Advances position
426
+ # seq.tick # Executes position 2
427
+ #
428
+ # # tick_log contains [1, 1 + 1/96r, 2, ...]
429
+ #
430
+ # @example Conditional event scheduling
431
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
432
+ #
433
+ # seq.before_tick do |position|
434
+ # # Schedule event only on whole beats
435
+ # if position == position.to_i
436
+ # seq.now { puts "Beat #{position}" }
437
+ # end
438
+ # end
439
+ #
440
+ # seq.at(5) { puts "Trigger" } # Start the sequencer
441
+ # seq.run
115
442
  def before_tick(&block)
116
443
  @before_tick << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
117
444
  end
118
445
 
446
+ # Subscribes to custom event.
447
+ #
448
+ # Registers a handler for custom events in the sequencer's pub/sub system.
449
+ # Events can be launched from scheduled blocks and handled at the sequencer
450
+ # level or at specific control levels. Supports hierarchical event delegation.
451
+ #
452
+ # @param event [Symbol] event name
453
+ # @yield [*args] event handler receiving event parameters
454
+ # @return [void]
455
+ #
456
+ # @example Basic event pub/sub
457
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
458
+ #
459
+ # received_values = []
460
+ #
461
+ # # Subscribe to custom event
462
+ # seq.on(:note_played) do |pitch, velocity|
463
+ # received_values << { pitch: pitch, velocity: velocity }
464
+ # end
465
+ #
466
+ # # Launch event from scheduled block
467
+ # seq.at(1) do
468
+ # seq.launch(:note_played, 60, 100)
469
+ # end
470
+ #
471
+ # seq.at(2) do
472
+ # seq.launch(:note_played, 64, 80)
473
+ # end
474
+ #
475
+ # seq.run
476
+ #
477
+ # # received_values contains [{pitch: 60, velocity: 100}, {pitch: 64, velocity: 80}]
478
+ #
479
+ # @example Hierarchical event handling with control
480
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
481
+ #
482
+ # global_events = []
483
+ # local_events = []
484
+ #
485
+ # # Global handler (sequencer level)
486
+ # seq.on(:finished) do |name|
487
+ # global_events << name
488
+ # end
489
+ #
490
+ # # Local handler (control level)
491
+ # control = seq.at(1) do |control:|
492
+ # control.launch(:finished, "local task")
493
+ # end
494
+ #
495
+ # control.on(:finished) do |name|
496
+ # local_events << name
497
+ # end
498
+ #
499
+ # seq.run
500
+ #
501
+ # # local_events contains ["local task"]
502
+ # # global_events is empty (event handled locally, doesn't bubble up)
119
503
  def on(event, &block)
120
504
  @event_handlers.last.on event, &block
121
505
  end
122
506
 
507
+ # Launches custom event.
508
+ #
509
+ # Publishes a custom event to registered handlers. Events bubble up through
510
+ # the handler hierarchy if not handled locally. Supports both positional and
511
+ # keyword parameters.
512
+ #
513
+ # @param event [Symbol] event name
514
+ # @param value_parameters [Array] positional parameters
515
+ # @param key_parameters [Hash] keyword parameters
516
+ # @return [void]
517
+ #
518
+ # @see #on
123
519
  def launch(event, *value_parameters, **key_parameters)
124
520
  @event_handlers.last.launch event, *value_parameters, **key_parameters
125
521
  end
126
522
 
523
+ # Schedules block relative to current position.
524
+ #
525
+ # @param bars_delay [Numeric, Series, Array] delay from current position
526
+ # @param debug [Boolean] enable debug logging
527
+ # @yield block to execute at position + delay
528
+ # @return [EventHandler] control object
529
+ #
530
+ # @example
531
+ # seq.wait(2) { puts "2 beats later" }
127
532
  def wait(bars_delay, debug: nil, &block)
128
533
  debug ||= false
129
534
 
@@ -144,6 +549,13 @@ module Musa
144
549
  control
145
550
  end
146
551
 
552
+ # Schedules block at current position (immediate execution on next tick).
553
+ #
554
+ # @yield block to execute at current position
555
+ # @return [EventHandler] control object
556
+ #
557
+ # @example
558
+ # seq.now { puts "Executes now" }
147
559
  def now(&block)
148
560
  control = EventHandler.new @event_handlers.last
149
561
  @event_handlers.push control
@@ -155,12 +567,31 @@ module Musa
155
567
  control
156
568
  end
157
569
 
570
+ # Schedules block at absolute position (low-level, no control object).
571
+ #
572
+ # @param bar_position [Numeric] absolute position
573
+ # @param force_first [Boolean] force execution before other events at same time
574
+ # @yield block to execute
575
+ # @return [nil]
576
+ # @api private
158
577
  def raw_at(bar_position, force_first: nil, &block)
159
578
  _raw_numeric_at bar_position.rationalize, force_first: force_first, &block
160
579
 
161
580
  nil
162
581
  end
163
582
 
583
+ # Schedules block at absolute position.
584
+ #
585
+ # @param bar_position [Numeric, Series, Array] absolute position(s)
586
+ # @param debug [Boolean] enable debug logging
587
+ # @yield block to execute at position
588
+ # @return [EventHandler] control object
589
+ #
590
+ # @example Single position
591
+ # seq.at(4) { puts "At beat 4" }
592
+ #
593
+ # @example Series of positions
594
+ # seq.at([1, 2, 3.5, 4]) { |pos| puts "At #{pos}" }
164
595
  def at(bar_position, debug: nil, &block)
165
596
  debug ||= false
166
597
 
@@ -181,6 +612,57 @@ module Musa
181
612
  control
182
613
  end
183
614
 
615
+ # Plays series over time.
616
+ #
617
+ # Consumes series values sequentially, evaluating each element to determine
618
+ # operation and scheduling continuation. Supports pause/continue,
619
+ # nested plays, parallel plays, and event-driven continuation.
620
+ # Timing determined by mode.
621
+ #
622
+ # @param serie [Series] series to play
623
+ # @param mode [Symbol] running mode (:at, :wait, :neumalang)
624
+ # @param parameter [Symbol, nil] duration parameter name from serie values
625
+ # @param after_bars [Numeric, nil] schedule block after play finishes
626
+ # @param after [Proc, nil] block to execute after play finishes
627
+ # @param context [Object, nil] context for neumalang processing
628
+ # @param mode_args [Hash] additional mode-specific parameters
629
+ # @yield [value] block executed for each serie value
630
+ # @return [PlayControl] control object
631
+ #
632
+ # ## Available Running Modes
633
+ #
634
+ # - **:at**: Elements specify absolute positions via :at key
635
+ # - **:wait**: Elements with duration specify wait time
636
+ # - **:neumalang**: Full Neumalang DSL with variables, commands, series, etc.
637
+ #
638
+ #
639
+ # @example Playing notes from a series
640
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
641
+ #
642
+ # notes = Musa::Series::S(60, 62, 64).zip(Musa::Series::S(1, 1, 2))
643
+ # played_notes = []
644
+ #
645
+ # seq.play(notes) do |pitch, duration|
646
+ # played_notes << { pitch: pitch, duration: duration, position: seq.position }
647
+ # end
648
+ #
649
+ # seq.run
650
+ # # Result: played_notes contains [{pitch: 60, duration: 1, position: 0}, ...]
651
+ #
652
+ # @example Parallel plays
653
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
654
+ #
655
+ # melody = Musa::Series::S(60, 62, 64)
656
+ # harmony = Musa::Series::S(48, 52, 55)
657
+ # played_notes = []
658
+ #
659
+ # seq.play([melody, harmony]) do |pitch|
660
+ # # pitch will be array [melody_pitch, harmony_pitch]
661
+ # played_notes << { melody: pitch[0], harmony: pitch[1], position: seq.position }
662
+ # end
663
+ #
664
+ # seq.run
665
+ # # Result: played_notes contains [{melody: 60, harmony: 48, position: 0}, ...]
184
666
  def play(serie,
185
667
  mode: nil,
186
668
  parameter: nil,
@@ -204,19 +686,88 @@ module Musa
204
686
  control.after do
205
687
  @playing.delete control
206
688
  end
207
-
689
+
208
690
  control
209
691
  end
210
-
692
+
211
693
  def continuation_play(parameters)
212
694
  _play parameters[:serie],
213
- parameters[:control],
214
- parameters[:neumalang_context],
215
- mode: parameters[:mode],
216
- decoder: parameters[:decoder],
217
- __play_eval: parameters[:play_eval],
218
- **parameters[:mode_args]
695
+ parameters[:control],
696
+ parameters[:neumalang_context],
697
+ mode: parameters[:mode],
698
+ decoder: parameters[:decoder],
699
+ __play_eval: parameters[:play_eval],
700
+ **parameters[:mode_args]
219
701
  end
702
+
703
+ # Plays timed series (series with embedded timing information).
704
+ #
705
+ # Similar to play but serie values include timing: each element specifies its
706
+ # own timing via `:time` attribute. Unlike regular `play` which derives timing
707
+ # from evaluation mode, play_timed uses explicit times from series data.
708
+ #
709
+ # ## Timed Series Format
710
+ #
711
+ # Each element must have:
712
+ #
713
+ # - **:time**: Rational time offset from start
714
+ # - **:value**: Actual value(s) - Hash or Array
715
+ # - Optional extra attributes (passed to block)
716
+ #
717
+ # ## Value Modes
718
+ #
719
+ # - **Hash mode**: `{ time: 0r, value: {pitch: 60, velocity: 96} }`
720
+ # - **Array mode**: `{ time: 0r, value: [60, 96] }`
721
+ #
722
+ # Mode is detected from first element and applied to entire series.
723
+ #
724
+ # ## Component Tracking
725
+ #
726
+ # Tracks last update time per component (hash key or array index) to
727
+ # calculate `started_ago` - how long since each component changed.
728
+ #
729
+ # @param timed_serie [Series] timed series
730
+ # @param at [Rational, nil] starting position
731
+ # @param on_stop [Proc, nil] callback when playback stops
732
+ # @param after_bars [Numeric, nil] schedule after completion
733
+ # @param after [Proc, nil] block after completion
734
+ # @yield [value] block for each value
735
+ # @return [PlayTimedControl] control object
736
+ #
737
+ # @example Hash mode timed series
738
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
739
+ #
740
+ # timed_notes = Musa::Series::S(
741
+ # { time: 0r, value: {pitch: 60, velocity: 96} },
742
+ # { time: 1r, value: {pitch: 64, velocity: 80} },
743
+ # { time: 2r, value: {pitch: 67, velocity: 64} }
744
+ # )
745
+ #
746
+ # played_notes = []
747
+ #
748
+ # seq.play_timed(timed_notes) do |values, time:, started_ago:, control:|
749
+ # played_notes << { pitch: values[:pitch], velocity: values[:velocity], time: time }
750
+ # end
751
+ #
752
+ # seq.run
753
+ # # Result: played_notes contains [{pitch: 60, velocity: 96, time: 0r}, ...]
754
+ #
755
+ # @example Array mode with extra attributes
756
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
757
+ #
758
+ # timed = Musa::Series::S(
759
+ # { time: 0r, value: [60, 96], channel: 0 },
760
+ # { time: 1r, value: [64, 80], channel: 1 }
761
+ # )
762
+ #
763
+ # played_notes = []
764
+ #
765
+ # seq.play_timed(timed) do |values, channel:, time:, started_ago:, control:|
766
+ # played_notes << { pitch: values[0], velocity: values[1], channel: channel, time: time }
767
+ # end
768
+ #
769
+ # seq.run
770
+ # # Result: played_notes contains [{pitch: 60, velocity: 96, channel: 0, time: 0r}, ...]
220
771
 
221
772
  def play_timed(timed_serie,
222
773
  at: nil,
@@ -250,6 +801,55 @@ module Musa
250
801
  control
251
802
  end
252
803
 
804
+ # Executes block repeatedly at regular intervals.
805
+ #
806
+ # ## Execution Model
807
+ #
808
+ # Every loop schedules itself recursively:
809
+ # 1. Execute block at current position
810
+ # 2. Check stopping conditions
811
+ # 3. If not stopped, schedule next iteration at start + counter * interval
812
+ # 4. If stopped, call on_stop and after callbacks
813
+ #
814
+ # This ensures precise timing - iterations are scheduled relative to start
815
+ # position, not accumulated from previous iteration (avoiding drift).
816
+ #
817
+ # ## Stopping Conditions
818
+ #
819
+ # Loop stops when any of these conditions is met:
820
+ #
821
+ # - **manual stop**: `control.stop` called
822
+ # - **duration**: elapsed time >= duration (in bars)
823
+ # - **till**: current position >= till position
824
+ # - **condition**: condition block returns false
825
+ # - **nil interval**: immediate stop after first execution
826
+ #
827
+ # @param interval [Numeric, nil] interval between executions (nil = once)
828
+ # @param duration [Numeric, nil] total duration
829
+ # @param till [Numeric, nil] end position
830
+ # @param condition [Proc, nil] continue while condition true
831
+ # @param on_stop [Proc, nil] callback when loop stops
832
+ # @param after_bars [Numeric, nil] schedule after completion
833
+ # @param after [Proc, nil] block after completion
834
+ # @yield [position] block executed each interval
835
+ # @return [EveryControl] control object
836
+ #
837
+ # @example
838
+ # seq.every(1, till: 8) { |pos| puts "Beat #{pos}" }
839
+ #
840
+ # @example Every 4 beats for 16 bars
841
+ # sequencer.every(1r, duration: 4r) { puts "tick" }
842
+ # # Executes at 1r, 2r, 3r, 4r, 5r (5 times total)
843
+ #
844
+ # @example Every beat until position 10
845
+ # sequencer.every(1r, till: 10r) { |control| puts control.position }
846
+ #
847
+ # @example Conditional loop
848
+ # count = 0
849
+ # sequencer.every(1r, condition: proc { count < 5 }) do
850
+ # puts count
851
+ # count += 1
852
+ # end
253
853
  def every(interval,
254
854
  duration: nil, till: nil,
255
855
  condition: nil,
@@ -283,6 +883,106 @@ module Musa
283
883
  control
284
884
  end
285
885
 
886
+ # Animates value from start to end over time.
887
+ # Supports single values, arrays, and hashes
888
+ # with flexible parameter combinations for controlling timing and interpolation.
889
+ #
890
+ # ## Value Modes
891
+ #
892
+ # - **Single value**: `from: 0, to: 100`
893
+ # - **Array**: `from: [60, 0.5], to: [72, 1.0]` - multiple values
894
+ # - **Hash**: `from: {pitch: 60}, to: {pitch: 72}` - named values
895
+ #
896
+ # ## Parameter Combinations
897
+ #
898
+ # Move requires enough information to calculate both step size and iteration
899
+ # interval. Valid combinations:
900
+ #
901
+ # - `from, to, step, every` - All explicit
902
+ # - `from, to, step, duration/till` - Calculates every from steps needed
903
+ # - `from, to, every, duration/till` - Calculates step from duration
904
+ # - `from, step, every, duration/till` - Open-ended with time limit
905
+ #
906
+ # ## Interpolation
907
+ #
908
+ # - **Linear** (default): `function: proc { |ratio| ratio }`
909
+ # - **Ease-in**: `function: proc { |ratio| ratio ** 2 }`
910
+ # - **Ease-out**: `function: proc { |ratio| 1 - (1 - ratio) ** 2 }`
911
+ # - **Custom**: Any proc mapping [0..1] to [0..1]
912
+ #
913
+ # ## Applications
914
+ #
915
+ # - Pitch bends and glissandi
916
+ # - Volume fades and swells
917
+ # - Filter sweeps and modulation
918
+ # - Tempo changes and rubato
919
+ # - Multi-parameter automation
920
+ #
921
+ # @param every [Numeric] interval between updates
922
+ # @param from [Numeric] starting value
923
+ # @param to [Numeric] ending value
924
+ # @param step [Numeric, nil] value increment per step
925
+ # @param duration [Numeric, nil] total duration
926
+ # @param till [Numeric, nil] end position
927
+ # @param function [Symbol, Proc, nil] interpolation function
928
+ # @param right_open [Boolean, nil] exclude final value
929
+ # @param on_stop [Proc, nil] callback when animation stops
930
+ # @param after_bars [Numeric, nil] schedule after completion
931
+ # @param after [Proc, nil] block after completion
932
+ # @yield [value] block executed with interpolated value
933
+ # @return [MoveControl] control object
934
+ #
935
+ # @example Simple pitch glide
936
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
937
+ #
938
+ # pitch_values = []
939
+ #
940
+ # seq.move(from: 60, to: 72, duration: 4r, every: 1/4r) do |pitch|
941
+ # pitch_values << { pitch: pitch.round, position: seq.position }
942
+ # end
943
+ #
944
+ # seq.run
945
+ # # Result: pitch_values contains [{pitch: 60, position: 0}, {pitch: 61, position: 0.25}, ...]
946
+ #
947
+ # @example Multi-parameter fade
948
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
949
+ #
950
+ # controller_values = []
951
+ #
952
+ # seq.move(
953
+ # from: {volume: 0, brightness: 0},
954
+ # to: {volume: 127, brightness: 127},
955
+ # duration: 8r,
956
+ # every: 1/8r
957
+ # ) do |params|
958
+ # controller_values << {
959
+ # volume: params[:volume].round,
960
+ # brightness: params[:brightness].round,
961
+ # position: seq.position
962
+ # }
963
+ # end
964
+ #
965
+ # seq.run
966
+ # # Result: controller_values contains [{volume: 0, brightness: 0, position: 0}, ...]
967
+ #
968
+ # @example Non-linear interpolation
969
+ # sequencer.move(
970
+ # from: 0, to: 100,
971
+ # duration: 4r, every: 1/16r,
972
+ # function: proc { |ratio| ratio ** 2 } # Ease-in
973
+ # ) { |value| puts value }
974
+ #
975
+ # @example Linear fade
976
+ # seq = Musa::Sequencer::BaseSequencer.new(4, 24)
977
+ #
978
+ # volume_values = []
979
+ #
980
+ # seq.move(every: 1/4r, from: 0, to: 127, duration: 4) do |value|
981
+ # volume_values << value.round
982
+ # end
983
+ #
984
+ # seq.run
985
+ # # Result: volume_values contains [0, 8, 16, 24, ..., 119, 127]
286
986
  def move(every: nil,
287
987
  from: nil, to: nil, step: nil,
288
988
  duration: nil, till: nil,