Class: Musa::Sequencer::BaseSequencer
- Defined in:
- lib/musa-dsl/sequencer/base-sequencer.rb,
lib/musa-dsl/sequencer/timeslots.rb,
lib/musa-dsl/sequencer/base-sequencer-tick-based.rb,
lib/musa-dsl/sequencer/base-sequencer-implementation.rb,
lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb,
lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb,
lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb,
lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb,
lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb,
lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb
Overview
Musical sequencer and scheduler system.
Sequencer provides precise timing and scheduling for musical events, supporting both tick-based (quantized) and tickless (continuous) timing modes. Events are scheduled with musical time units (bars, beats, ticks) and executed sequentially.
Core Concepts
- Position: Current playback position in beats
- Timeslots: Scheduled events indexed by time
Timing Modes:
- Tick-based: Quantized to beats_per_bar × ticks_per_beat grid
- Tickless: Continuous rational time (no quantization)
Scheduling Methods:
at: Schedule block at absolute positionwait: Schedule relative to current positionplay: Play series over timeevery: Repeat at intervalsmove: Animate value over time
Event Handlers: Hierarchical event pub/sub system
Controls: Objects returned by scheduling methods for lifecycle management
Block Parameter Flexibility (SmartProcBinder)
All scheduling methods (every, play, move, play_timed) pass parameters
to user blocks via SmartProcBinder. This means blocks can declare only the
parameters they need — undeclared parameters are silently ignored.
Keyword parameters (like control:) must be declared as keyword arguments
in the block signature (|control:|), not as positional arguments (|control|).
| Method | Positional params | Keyword params |
|---|---|---|
every |
(none) | control: |
play |
element (+ hash keys as keywords) | control: |
move |
value, next_value | control:, duration:, quantized_duration:, started_ago:, position_jitter:, duration_jitter:, right_open: |
play_timed |
values (+ extra attributes as keywords) | time:, started_ago:, control: |
Tick-based vs Tickless
Tick-based (beats_per_bar and ticks_per_beat specified):
- Positions quantized to tick grid
tickmethod advances by one tick- Suitable for MIDI-like discrete timing
- Example:
BaseSequencer.new(4, 24)→ 4/4 time, 24 ticks per beat
Tickless (no timing parameters):
- Continuous rational time
tick(position)jumps to arbitrary position- Suitable for score-like continuous timing
- Example:
BaseSequencer.new→ tickless mode
Musical Time Units
- Bar: Musical measure (defaults to 1.0 in value)
- Beat: Subdivision of bar (e.g., quarter note in 4/4)
- Tick: Smallest time quantum in tick-based mode
- All times are Rational for precision
Defined Under Namespace
Modules: TickBasedTiming, TicklessBasedTiming
Instance Attribute Summary collapse
-
#beats_per_bar ⇒ Rational?
readonly
Beats per bar (tick-based mode only).
-
#everying ⇒ Array<EveryControl>
readonly
Active every loops.
-
#logger ⇒ Musa::Logger::Logger
readonly
Sequencer logger.
-
#moving ⇒ Array<MoveControl>
readonly
Active move operations.
-
#offset ⇒ Rational
readonly
Time offset for position calculations.
-
#playing ⇒ Array<PlayControl, PlayTimedControl>
readonly
Active play operations.
-
#running_position ⇒ Rational
readonly
Current running position.
-
#ticks_per_beat ⇒ Rational?
readonly
Ticks per beat (tick-based mode only).
Instance Method Summary collapse
-
#_rescue_error(e) ⇒ void
private
Handles errors during event execution.
-
#at(bar_position, debug: nil) { ... } ⇒ EventHandler
Schedules block at absolute position.
-
#before_tick {|position| ... } ⇒ void
Registers callback executed before each tick.
- #continuation_play(parameters) ⇒ Object
- #debug(msg = nil) ⇒ Object
-
#empty? ⇒ Boolean
Checks if sequencer has no scheduled events.
-
#event_handler ⇒ EventHandler
private
Returns current event handler.
-
#every(interval, duration: nil, till: nil, condition: nil, on_stop: nil, after_bars: nil, after: nil) {|control| ... } ⇒ EveryControl
Executes block repeatedly at regular intervals.
-
#initialize(beats_per_bar = nil, ticks_per_beat = nil, offset: nil, logger: nil, do_log: nil, do_error_log: nil, log_position_format: nil) ⇒ BaseSequencer
constructor
Creates sequencer with timing configuration.
-
#launch(event, *value_parameters, **key_parameters) ⇒ void
Launches custom event.
-
#move(every: nil, from: nil, to: nil, step: nil, duration: nil, till: nil, function: nil, right_open: nil, on_stop: nil, after_bars: nil, after: nil) {|value, next_value, control, duration, quantized_duration, started_ago, position_jitter, duration_jitter, right_open| ... } ⇒ MoveControl
Animates value from start to end over time.
-
#now { ... } ⇒ EventHandler
Schedules block at current position (immediate execution on next tick).
-
#on(event) {|*args| ... } ⇒ void
Subscribes to custom event.
-
#on_debug_at { ... } ⇒ void
Registers debug callback for scheduled events.
-
#on_error {|error| ... } ⇒ void
Registers error callback.
-
#on_fast_forward {|is_starting| ... } ⇒ void
Registers fast-forward callback (when jumping over events).
-
#play(serie, mode: nil, parameter: nil, on_stop: nil, after_bars: nil, after: nil, context: nil, **mode_args) {|element, control| ... } ⇒ PlayControl
Plays series over time.
-
#play_timed(timed_serie, at: nil, on_stop: nil, after_bars: nil, after: nil) {|values, time, started_ago, control| ... } ⇒ PlayTimedControl
Plays timed series (series with embedded timing information).
-
#quantize_position(position, warn: nil) ⇒ Rational
Quantizes position to tick grid (tick-based mode only).
-
#raw_at(bar_position, force_first: nil) { ... } ⇒ nil
private
Schedules block at absolute position (low-level, no control object).
-
#reset ⇒ void
Resets sequencer to initial state.
-
#run ⇒ void
Executes all scheduled events until empty.
-
#size ⇒ Integer
Counts total scheduled events.
- #to_s ⇒ Object
-
#wait(bars_delay, debug: nil) { ... } ⇒ EventHandler
Schedules block relative to current position.
Constructor Details
#initialize(beats_per_bar = nil, ticks_per_beat = nil, offset: nil, logger: nil, do_log: nil, do_error_log: nil, log_position_format: nil) ⇒ BaseSequencer
Creates sequencer with timing configuration.
Timing Modes
Tick-based: Provide both beats_per_bar and ticks_per_beat
- Position quantized to tick grid
tickadvances by one tick
Tickless: Omit beats_per_bar and ticks_per_beat
- Continuous rational time
tickadvances to next scheduled position (without timing quantization)
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 191 def initialize( = nil, ticks_per_beat = nil, offset: nil, logger: nil, do_log: nil, do_error_log: nil, log_position_format: nil) unless && ticks_per_beat || .nil? && ticks_per_beat.nil? raise ArgumentError, "'beats_per_bar' and 'ticks_per_beat' parameters should be both nil or both have values" end if logger @logger = logger else @logger = Musa::Logger::Logger.new(sequencer: self, position_format: log_position_format) @logger.fatal! @logger.error! if do_error_log || do_error_log.nil? @logger.debug! if do_log end @offset = offset || 0r if && ticks_per_beat = Rational() @ticks_per_beat = Rational(ticks_per_beat) singleton_class.include TickBasedTiming else singleton_class.include TicklessBasedTiming end _init_timing @on_debug_at = [] @on_error = [] @before_tick = [] @on_fast_forward = [] @tick_mutex = Mutex.new @position_mutex = Mutex.new @timeslots = Timeslots.new = [] = [] @moving = [] reset end |
Instance Attribute Details
#beats_per_bar ⇒ Rational? (readonly)
Returns beats per bar (tick-based mode only).
133 134 135 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 133 def end |
#everying ⇒ Array<EveryControl> (readonly)
Returns active every loops.
145 146 147 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 145 def end |
#logger ⇒ Musa::Logger::Logger (readonly)
Returns sequencer logger.
154 155 156 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 154 def logger @logger end |
#moving ⇒ Array<MoveControl> (readonly)
Returns active move operations.
151 152 153 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 151 def moving @moving end |
#offset ⇒ Rational (readonly)
Returns time offset for position calculations.
139 140 141 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 139 def offset @offset end |
#playing ⇒ Array<PlayControl, PlayTimedControl> (readonly)
Returns active play operations.
148 149 150 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 148 def end |
#running_position ⇒ Rational (readonly)
Returns current running position.
142 143 144 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 142 def running_position @running_position end |
#ticks_per_beat ⇒ Rational? (readonly)
Returns ticks per beat (tick-based mode only).
136 137 138 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 136 def ticks_per_beat @ticks_per_beat end |
Instance Method Details
#_rescue_error(e) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Handles errors during event execution.
Logs error message and full backtrace, then calls all registered on_error callbacks with the exception. Used by SmartProcBinder and direct rescue blocks to centralize error handling.
255 256 257 258 259 260 261 262 |
# File 'lib/musa-dsl/sequencer/base-sequencer-implementation.rb', line 255 def _rescue_error(e) @logger.error('BaseSequencer') { e.to_s } @logger.error('BaseSequencer') { e.(highlight: true, order: :top) } @on_error.each do |block| block.call e end end |
#at(bar_position, debug: nil) { ... } ⇒ EventHandler
Schedules block at absolute position.
Returns a control object whose .stop method cancels execution:
the block will not run if the control is stopped before the scheduled
position. For series-based positions, .stop also prevents further
elements from being scheduled.
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 633 def at(, debug: nil, &block) debug ||= false control = EventHandler.new @event_handlers.last @event_handlers.push control if .is_a? Numeric _numeric_at .rationalize, control, debug: debug, skip_if_stopped: true, &block else = Series::S(*) if .is_a? Array = .instance if _serie_at , control, debug: debug, &block end @event_handlers.pop control end |
#before_tick {|position| ... } ⇒ void
This method returns an undefined value.
Registers callback executed before each tick.
Callback is invoked before processing events at each position. Useful for logging, metrics collection, or performing pre-tick setup. Receives the position about to be executed.
458 459 460 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 458 def before_tick(&block) @before_tick << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#continuation_play(parameters) ⇒ Object
736 737 738 739 740 741 742 743 744 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 736 def continuation_play(parameters) _play parameters[:serie], parameters[:control], parameters[:neumalang_context], mode: parameters[:mode], decoder: parameters[:decoder], __play_eval: parameters[:play_eval], **parameters[:mode_args] end |
#debug(msg = nil) ⇒ Object
1075 1076 1077 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1075 def debug(msg = nil) @logger.debug { msg || '...' } end |
#empty? ⇒ Boolean
Checks if sequencer has no scheduled events.
286 287 288 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 286 def empty? @timeslots.empty? end |
#event_handler ⇒ EventHandler
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns current event handler.
320 321 322 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 320 def event_handler @event_handlers.last end |
#every(interval, duration: nil, till: nil, condition: nil, on_stop: nil, after_bars: nil, after: nil) {|control| ... } ⇒ EveryControl
Executes block repeatedly at regular intervals.
Execution Model
Every loop schedules itself recursively:
- Execute block at current position
- Check stopping conditions
- If not stopped, schedule next iteration at start + counter * interval
- If stopped, call on_stop and after callbacks
This ensures precise timing - iterations are scheduled relative to start position, not accumulated from previous iteration (avoiding drift).
Stopping Conditions
Loop stops when any of these conditions is met:
- manual stop:
control.stopcalled - duration: elapsed time >= duration (in bars)
- till: current position >= till position
- condition: condition block returns false
- nil interval: immediate stop after first execution
Block Parameters (via SmartProcBinder)
The block receives the following keyword parameter via SmartProcBinder. You can declare only the parameters you need — undeclared ones are silently ignored.
- control: [EveryControl] — the control object for the current loop
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 899 def every(interval, duration: nil, till: nil, condition: nil, on_stop: nil, after_bars: nil, after: nil, &block) # nil interval means 'only once' interval = interval.rationalize unless interval.nil? control = EveryControl.new @event_handlers.last, duration: duration, till: till, condition: condition, on_stop: on_stop, after_bars: , after: after @event_handlers.push control _every interval, control, &block @event_handlers.pop << control control.on_stop do .delete control end control end |
#launch(event, *value_parameters, **key_parameters) ⇒ void
This method returns an undefined value.
Launches custom event.
Publishes a custom event to registered handlers. Events bubble up through the handler hierarchy if not handled locally. Supports both positional and keyword parameters.
535 536 537 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 535 def launch(event, *value_parameters, **key_parameters) @event_handlers.last.launch event, *value_parameters, **key_parameters end |
#move(every: nil, from: nil, to: nil, step: nil, duration: nil, till: nil, function: nil, right_open: nil, on_stop: nil, after_bars: nil, after: nil) {|value, next_value, control, duration, quantized_duration, started_ago, position_jitter, duration_jitter, right_open| ... } ⇒ MoveControl
Animates value from start to end over time. Supports single values, arrays, and hashes with flexible parameter combinations for controlling timing and interpolation.
Value Modes
- Single value:
from: 0, to: 100 - Array:
from: [60, 0.5], to: [72, 1.0]- multiple values - Hash:
from: {pitch: 60}, to: {pitch: 72}- named values
Parameter Combinations
Move requires enough information to calculate both step size and iteration interval. Valid combinations:
from, to, step, every- All explicitfrom, to, step, duration/till- Calculates every from steps neededfrom, to, every, duration/till- Calculates step from durationfrom, step, every, duration/till- Open-ended with time limit
Interpolation
- Linear (default):
function: proc { |ratio| ratio } - Ease-in:
function: proc { |ratio| ratio ** 2 } - Ease-out:
function: proc { |ratio| 1 - (1 - ratio) ** 2 } - Custom: Any proc mapping [0..1] to [0..1]
Applications
- Pitch bends and glissandi
- Volume fades and swells
- Filter sweeps and modulation
- Tempo changes and rubato
- Multi-parameter automation
1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1046 def move(every: nil, from: nil, to: nil, step: nil, duration: nil, till: nil, function: nil, right_open: nil, on_stop: nil, after_bars: nil, after: nil, &block) control = _move every: every, from: from, to: to, step: step, duration: duration, till: till, function: function, right_open: right_open, on_stop: on_stop, after_bars: , after: after, &block @moving << control control.on_stop do @moving.delete control end control end |
#now { ... } ⇒ EventHandler
Schedules block at current position (immediate execution on next tick).
Returns a control object whose .stop method cancels execution
if the block hasn't run yet (e.g., scheduled at current position
but not yet reached by the tick loop).
588 589 590 591 592 593 594 595 596 597 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 588 def now(&block) control = EventHandler.new @event_handlers.last @event_handlers.push control _numeric_at position, control, skip_if_stopped: true, &block @event_handlers.pop control end |
#on(event) {|*args| ... } ⇒ void
This method returns an undefined value.
Subscribes to custom event.
Registers a handler for custom events in the sequencer's pub/sub system. Events can be launched from scheduled blocks and handled at the sequencer level or at specific control levels. Supports hierarchical event delegation.
519 520 521 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 519 def on(event, &block) @event_handlers.last.on event, &block end |
#on_debug_at { ... } ⇒ void
This method returns an undefined value.
Registers debug callback for scheduled events.
Callback is invoked when debug logging is enabled (see do_log parameter in initialize). Called before executing each scheduled event, allowing inspection of sequencer state at event execution time.
348 349 350 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 348 def on_debug_at(&block) @on_debug_at << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#on_error {|error| ... } ⇒ void
This method returns an undefined value.
Registers error callback.
Callback is invoked when an error occurs during event execution. The error is logged and passed to all registered error handlers. Handlers receive the exception object and can process or report it.
379 380 381 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 379 def on_error(&block) @on_error << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#on_fast_forward {|is_starting| ... } ⇒ void
This method returns an undefined value.
Registers fast-forward callback (when jumping over events).
Callback is invoked when position is changed directly (via position=), causing the sequencer to skip ahead. Called twice: once with true when fast-forward begins, and once with false when it completes. Events between old and new positions are executed during fast-forward.
414 415 416 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 414 def on_fast_forward(&block) @on_fast_forward << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#play(serie, mode: nil, parameter: nil, on_stop: nil, after_bars: nil, after: nil, context: nil, **mode_args) {|element, control| ... } ⇒ PlayControl
Plays series over time.
Consumes series values sequentially, evaluating each element to determine operation and scheduling continuation. Supports pause/continue, nested plays, parallel plays, and event-driven continuation. Timing determined by mode.
Available Running Modes
- :wait (default): Elements with duration specify wait time before next element
- :at: Elements specify absolute positions via :at key
- :neumalang: Full Neumalang DSL with variables, commands, series, etc.
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 708 def play(serie, mode: nil, parameter: nil, on_stop: nil, after_bars: nil, after: nil, context: nil, **mode_args, &block) mode ||= :wait control = PlayControl.new @event_handlers.last, on_stop: on_stop, after_bars: , after: after @event_handlers.push control _play serie.instance, control, context, mode: mode, parameter: parameter, **mode_args, &block @event_handlers.pop << control control.on_stop do .delete control end control end |
#play_timed(timed_serie, at: nil, on_stop: nil, after_bars: nil, after: nil) {|values, time, started_ago, control| ... } ⇒ PlayTimedControl
Plays timed series (series with embedded timing information).
Similar to play but serie values include timing: each element specifies its
own timing via :time attribute. Unlike regular play which derives timing
from evaluation mode, play_timed uses explicit times from series data.
Timed Series Format
Each element must have:
- :time: Rational time offset from start
- :value: Actual value(s) - Hash or Array
- Optional extra attributes (passed to block)
Value Modes
- Hash mode:
{ time: 0r, value: {pitch: 60, velocity: 96} } - Array mode:
{ time: 0r, value: [60, 96] }
Mode is detected from first element and applied to entire series.
Component Tracking
Tracks last update time per component (hash key or array index) to
calculate started_ago - how long since each component changed.
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 819 def play_timed(timed_serie, at: nil, on_stop: nil, after_bars: nil, after: nil, &block) at ||= position control = PlayTimedControl.new(@event_handlers.last, on_stop: on_stop, after_bars: , after: after) @event_handlers.push control _play_timed(timed_serie.instance, at, control, &block) @event_handlers.pop << control control.on_stop do .delete control end control end |
#quantize_position(position, warn: nil) ⇒ Rational
Quantizes position to tick grid (tick-based mode only).
296 297 298 299 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 296 def quantize_position(position, warn: nil) warn ||= false _quantize_position(position, warn: warn) end |
#raw_at(bar_position, force_first: nil) { ... } ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Schedules block at absolute position (low-level, no control object).
606 607 608 609 610 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 606 def raw_at(, force_first: nil, &block) _raw_numeric_at .rationalize, force_first: force_first, &block nil end |
#reset ⇒ void
This method returns an undefined value.
Resets sequencer to initial state.
Clears all scheduled events, active operations, and event handlers. Resets timing to start position.
265 266 267 268 269 270 271 272 273 274 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 265 def reset @timeslots.clear .clear .clear @moving.clear @event_handlers = [EventHandler.new] _reset_timing end |
#run ⇒ void
This method returns an undefined value.
Executes all scheduled events until empty.
Advances time tick by tick (or position by position in tickless mode) until no events remain.
312 313 314 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 312 def run tick until empty? end |
#size ⇒ Integer
Counts total scheduled events.
279 280 281 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 279 def size @timeslots.values.sum(&:size) end |
#to_s ⇒ Object
1079 1080 1081 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1079 def to_s super + ": position=#{position}" end |
#wait(bars_delay, debug: nil) { ... } ⇒ EventHandler
Schedules block relative to current position.
Returns a control object whose .stop method cancels execution:
the block will not run if the control is stopped before its scheduled
position. For series-based delays, .stop also prevents further
elements from being scheduled.
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 557 def wait(, debug: nil, &block) debug ||= false control = EventHandler.new @event_handlers.last @event_handlers.push control if .is_a? Numeric _numeric_at position + .rationalize, control, debug: debug, skip_if_stopped: true, &block else = Series::S(*) if .is_a?(Array) = .instance if _serie_at .with { |delay| position + delay }, control, debug: debug, &block end @event_handlers.pop control end |