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, 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.
231 232 233 234 235 236 237 238 |
# File 'lib/musa-dsl/sequencer/base-sequencer-implementation.rb', line 231 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.
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 595 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, &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
693 694 695 696 697 698 699 700 701 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 693 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
1015 1016 1017 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1015 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
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 853 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.after 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
986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 986 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.after do @moving.delete control end control end |
#now { ... } ⇒ EventHandler
Schedules block at current position (immediate execution on next tick).
559 560 561 562 563 564 565 566 567 568 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 559 def now(&block) control = EventHandler.new @event_handlers.last @event_handlers.push control _numeric_at position, control, &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, 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.
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 666 def play(serie, mode: nil, parameter: nil, after_bars: nil, after: nil, context: nil, **mode_args, &block) mode ||= :wait control = PlayControl.new @event_handlers.last, 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.after 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.
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 772 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) control.on_stop do control.do_after.each do |do_after| _numeric_at position + do_after[:bars], control, &do_after[:block] end end @event_handlers.push control _play_timed(timed_serie.instance, at, control, &block) @event_handlers.pop << control control.after 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).
577 578 579 580 581 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 577 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
1019 1020 1021 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1019 def to_s super + ": position=#{position}" end |
#wait(bars_delay, debug: nil) { ... } ⇒ EventHandler
Schedules block relative to current position.
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 |
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 532 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, &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 |