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
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) {|position| ... } ⇒ 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| ... } ⇒ 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) {|value| ... } ⇒ PlayControl
Plays series over time.
-
#play_timed(timed_serie, at: nil, on_stop: nil, after_bars: nil, after: nil) {|value| ... } ⇒ 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)
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 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 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 175 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).
117 118 119 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 117 def end |
#everying ⇒ Array<EveryControl> (readonly)
Returns active every loops.
129 130 131 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 129 def end |
#logger ⇒ Musa::Logger::Logger (readonly)
Returns sequencer logger.
138 139 140 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 138 def logger @logger end |
#moving ⇒ Array<MoveControl> (readonly)
Returns active move operations.
135 136 137 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 135 def moving @moving end |
#offset ⇒ Rational (readonly)
Returns time offset for position calculations.
123 124 125 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 123 def offset @offset end |
#playing ⇒ Array<PlayControl, PlayTimedControl> (readonly)
Returns active play operations.
132 133 134 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 132 def end |
#running_position ⇒ Rational (readonly)
Returns current running position.
126 127 128 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 126 def running_position @running_position end |
#ticks_per_beat ⇒ Rational? (readonly)
Returns ticks per beat (tick-based mode only).
120 121 122 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 120 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.
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 617 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.
442 443 444 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 442 def before_tick(&block) @before_tick << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#continuation_play(parameters) ⇒ Object
717 718 719 720 721 722 723 724 725 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 717 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
1033 1034 1035 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1033 def debug(msg = nil) @logger.debug { msg || '...' } end |
#empty? ⇒ Boolean
Checks if sequencer has no scheduled events.
270 271 272 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 270 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.
304 305 306 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 304 def event_handler @event_handlers.last end |
#every(interval, duration: nil, till: nil, condition: nil, on_stop: nil, after_bars: nil, after: nil) {|position| ... } ⇒ 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
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 871 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.
519 520 521 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 519 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| ... } ⇒ 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
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1004 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).
572 573 574 575 576 577 578 579 580 581 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 572 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.
503 504 505 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 503 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.
332 333 334 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 332 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.
363 364 365 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 363 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.
398 399 400 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 398 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) {|value| ... } ⇒ 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
- :at: Elements specify absolute positions via :at key
- :wait: Elements with duration specify wait time
- :neumalang: Full Neumalang DSL with variables, commands, series, etc.
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 689 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) {|value| ... } ⇒ 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.
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 796 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).
280 281 282 283 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 280 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).
590 591 592 593 594 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 590 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.
249 250 251 252 253 254 255 256 257 258 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 249 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.
296 297 298 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 296 def run tick until empty? end |
#size ⇒ Integer
Counts total scheduled events.
263 264 265 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 263 def size @timeslots.values.sum(&:size) end |
#to_s ⇒ Object
1037 1038 1039 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1037 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.
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 541 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 |