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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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,
|