Class: Musa::Sequencer::BaseSequencer

Inherits:
Object
  • Object
show all
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 position
    • wait: Schedule relative to current position
    • play: Play series over time
    • every: Repeat at intervals
    • move: 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
  • tick method 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

Examples:

Basic tick-based sequencer

seq = Musa::Sequencer::BaseSequencer.new(4, 24)  # 4/4, 24 ticks/beat

seq.at(1) { puts "Beat 1" }
seq.at(2) { puts "Beat 2" }
seq.at(3.5) { puts "Beat 3.5" }

seq.run  # Executes all scheduled events

Tickless sequencer

seq = Musa::Sequencer::BaseSequencer.new  # Tickless mode

seq.at(1) { puts "Position 1" }
seq.at(1.5) { puts "Position 1.5" }

seq.tick(1)    # Jumps to position 1
seq.tick(1.5)  # Jumps to position 1.5

Playing series

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

pitches = Musa::Series::S(60, 62, 64, 65, 67)
durations = Musa::Series::S(1, 1, 0.5, 0.5, 2)
played_notes = []

seq.play(pitches.zip(durations)) do |pitch, duration|
  played_notes << { pitch: pitch, duration: duration, position: seq.position }
end

seq.run
# Result: played_notes contains [{pitch: 60, duration: 1, position: 0}, ...]

Every and move

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

tick_positions = []
volume_values = []

# Execute every beat
seq.every(1, till: 8) { tick_positions << seq.position }

# Animate value from 0 to 127 over 4 beats
seq.move(every: 1/4r, from: 0, to: 127, duration: 4) do |value|
  volume_values << value.round
end

seq.run
# Result: tick_positions = [0, 1, 2, 3, 4, 5, 6, 7]
# Result: volume_values = [0, 8, 16, ..., 119, 127]

Defined Under Namespace

Modules: TickBasedTiming, TicklessBasedTiming

Instance Attribute Summary collapse

Instance Method Summary collapse

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
  • tick advances by one tick

Tickless: Omit beats_per_bar and ticks_per_beat

  • Continuous rational time
  • tick advances to next scheduled position (without timing quantization)

Examples:

Tick-based 4/4 time

seq = BaseSequencer.new(4, 24)

Tick-based 3/4 time

seq = BaseSequencer.new(3, 24)

Tickless mode

seq = BaseSequencer.new

With offset

seq = BaseSequencer.new(4, 24, offset: 10r)

Parameters:

  • beats_per_bar (Numeric, nil) (defaults to: nil)

    beats per bar (nil for tickless)

  • ticks_per_beat (Numeric, nil) (defaults to: nil)

    ticks per beat (nil for tickless)

  • offset (Rational, nil) (defaults to: nil)

    starting position offset

  • logger (Musa::Logger::Logger, nil) (defaults to: nil)

    custom logger

  • do_log (Boolean, nil) (defaults to: nil)

    enable debug logging

  • do_error_log (Boolean, nil) (defaults to: nil)

    enable error logging

  • log_position_format (Proc, nil) (defaults to: nil)

    custom position formatter for logs

Raises:

  • (ArgumentError)

    if only one of beats_per_bar/ticks_per_beat provided



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(beats_per_bar = nil, ticks_per_beat = nil,
               offset: nil,
               logger: nil,
               do_log: nil, do_error_log: nil, log_position_format: nil)

  unless beats_per_bar && ticks_per_beat || beats_per_bar.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 beats_per_bar && ticks_per_beat
    @beats_per_bar = Rational(beats_per_bar)
    @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

  @everying = []
  @playing = []
  @moving = []

  reset
end

Instance Attribute Details

#beats_per_barRational? (readonly)

Returns beats per bar (tick-based mode only).

Returns:

  • (Rational, nil)

    beats per bar (tick-based mode only)



133
134
135
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 133

def beats_per_bar
  @beats_per_bar
end

#everyingArray<EveryControl> (readonly)

Returns active every loops.

Returns:

  • (Array<EveryControl>)

    active every loops



145
146
147
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 145

def everying
  @everying
end

#loggerMusa::Logger::Logger (readonly)

Returns sequencer logger.

Returns:



154
155
156
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 154

def logger
  @logger
end

#movingArray<MoveControl> (readonly)

Returns active move operations.

Returns:

  • (Array<MoveControl>)

    active move operations



151
152
153
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 151

def moving
  @moving
end

#offsetRational (readonly)

Returns time offset for position calculations.

Returns:

  • (Rational)

    time offset for position calculations



139
140
141
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 139

def offset
  @offset
end

#playingArray<PlayControl, PlayTimedControl> (readonly)

Returns active play operations.

Returns:

  • (Array<PlayControl, PlayTimedControl>)

    active play operations



148
149
150
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 148

def playing
  @playing
end

#running_positionRational (readonly)

Returns current running position.

Returns:

  • (Rational)

    current running position



142
143
144
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 142

def running_position
  @running_position
end

#ticks_per_beatRational? (readonly)

Returns ticks per beat (tick-based mode only).

Returns:

  • (Rational, nil)

    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.

Parameters:

  • e (Exception)

    exception that occurred



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.full_message(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.

Examples:

Single position

seq.at(4) { puts "At beat 4" }

Series of positions

seq.at([1, 2, 3.5, 4]) { |pos| puts "At #{pos}" }

Cancelling a scheduled at

h = seq.at(8) { puts "won't run" }
h.stop

Parameters:

  • bar_position (Numeric, Series, Array)

    absolute position(s)

  • debug (Boolean) (defaults to: nil)

    enable debug logging

Yields:

  • block to execute at position

Returns:

  • (EventHandler)

    control object (supports .stop to cancel)



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(bar_position, debug: nil, &block)
  debug ||= false

  control = EventHandler.new @event_handlers.last
  @event_handlers.push control

  if bar_position.is_a? Numeric
    _numeric_at bar_position.rationalize, control, debug: debug, skip_if_stopped: true, &block
  else
    bar_position = Series::S(*bar_position) if bar_position.is_a? Array
    bar_position = bar_position.instance if bar_position

    _serie_at bar_position, 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.

Examples:

Logging tick positions

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

tick_log = []

seq.before_tick do |position|
  tick_log << position
end

seq.at(1) { puts "Event" }
seq.at(2) { puts "Event" }

seq.tick  # Executes position 1
seq.tick  # Advances position
seq.tick  # Executes position 2

# tick_log contains [1, 1 + 1/96r, 2, ...]

Conditional event scheduling

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

seq.before_tick do |position|
  # Schedule event only on whole beats
  if position == position.to_i
    seq.now { puts "Beat #{position}" }
  end
end

seq.at(5) { puts "Trigger" }  # Start the sequencer
seq.run

Yields:

  • (position)

    callback receiving the upcoming position

Yield Parameters:

  • position (Rational)

    the position about to be processed



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.

Returns:

  • (Boolean)

    true if no events scheduled



286
287
288
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 286

def empty?
  @timeslots.empty?
end

#event_handlerEventHandler

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.

Returns:

  • (EventHandler)

    active 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:

  1. Execute block at current position
  2. Check stopping conditions
  3. If not stopped, schedule next iteration at start + counter * interval
  4. 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.stop called
  • 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

Examples:

No parameters needed

seq.every(1, till: 8) { puts "Beat at #{seq.position}" }

Accessing the control object (keyword argument)

seq.every(1r, duration: 4r) do |control:|
  puts "Iteration #{control._execution_counter}"
end

Conditional loop

count = 0
sequencer.every(1r, condition: proc { count < 5 }) do
  puts count
  count += 1
end

Parameters:

  • interval (Numeric, nil)

    interval between executions (nil = once)

  • duration (Numeric, nil) (defaults to: nil)

    total duration

  • till (Numeric, nil) (defaults to: nil)

    end position

  • condition (Proc, nil) (defaults to: nil)

    continue while condition true

  • on_stop (Proc, nil) (defaults to: nil)

    callback when loop stops

  • after_bars (Numeric, nil) (defaults to: nil)

    schedule after completion

  • after (Proc, nil) (defaults to: nil)

    block after completion

Yield Parameters:

  • control (EveryControl)

    the loop's control object (keyword, optional)

Returns:

  • (EveryControl)

    control object



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_bars,
                             after: after

  @event_handlers.push control

  _every interval, control, &block

  @event_handlers.pop

  @everying << control

  control.on_stop do
    @everying.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.

Parameters:

  • event (Symbol)

    event name

  • value_parameters (Array)

    positional parameters

  • key_parameters (Hash)

    keyword parameters

See Also:



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 explicit
  • from, to, step, duration/till - Calculates every from steps needed
  • from, to, every, duration/till - Calculates step from duration
  • from, 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

Examples:

Simple pitch glide

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

pitch_values = []

seq.move(from: 60, to: 72, duration: 4r, every: 1/4r) do |pitch|
  pitch_values << { pitch: pitch.round, position: seq.position }
end

seq.run
# Result: pitch_values contains [{pitch: 60, position: 0}, {pitch: 61, position: 0.25}, ...]

Multi-parameter fade

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

controller_values = []

seq.move(
  from: {volume: 0, brightness: 0},
  to: {volume: 127, brightness: 127},
  duration: 8r,
  every: 1/8r
) do |params|
  controller_values << {
    volume: params[:volume].round,
    brightness: params[:brightness].round,
    position: seq.position
  }
end

seq.run
# Result: controller_values contains [{volume: 0, brightness: 0, position: 0}, ...]

Non-linear interpolation

sequencer.move(
  from: 0, to: 100,
  duration: 4r, every: 1/16r,
  function: proc { |ratio| ratio ** 2 }  # Ease-in
) { |value| puts value }

Linear fade (only positional value needed)

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

volume_values = []

seq.move(every: 1/4r, from: 0, to: 127, duration: 4) do |value|
  volume_values << value.round
end

seq.run
# Result: volume_values contains [0, 8, 16, 24, ..., 119, 127]

Using keyword parameters

seq.move(from: 60, to: 72, duration: 4r, every: 1/4r) do |value, next_value, control:, duration:|
  puts "value=#{value.round} next=#{next_value&.round} dur=#{duration}"
end

Parameters:

  • every (Numeric) (defaults to: nil)

    interval between updates

  • from (Numeric) (defaults to: nil)

    starting value

  • to (Numeric) (defaults to: nil)

    ending value

  • step (Numeric, nil) (defaults to: nil)

    value increment per step

  • duration (Numeric, nil) (defaults to: nil)

    total duration

  • till (Numeric, nil) (defaults to: nil)

    end position

  • function (Symbol, Proc, nil) (defaults to: nil)

    interpolation function

  • right_open (Boolean, nil) (defaults to: nil)

    exclude final value

  • on_stop (Proc, nil) (defaults to: nil)

    callback when animation stops

  • after_bars (Numeric, nil) (defaults to: nil)

    schedule after completion

  • after (Proc, nil) (defaults to: nil)

    block after completion

Yields:

  • block executed with interpolated value (via SmartProcBinder — declare only the parameters you need)

Yield Parameters:

  • value (Numeric, Array, Hash)

    current interpolated value(s) (positional)

  • next_value (Numeric, Array, Hash, nil)

    next interpolated value(s), nil at end (positional)

  • control (MoveControl)

    the move control object (keyword, optional)

  • duration (Numeric, Array, Hash)

    interval duration per component (keyword, optional)

  • quantized_duration (Numeric, Array, Hash)

    quantized interval duration (keyword, optional)

  • started_ago (Numeric, Array, Hash, nil)

    time since component last changed (keyword, optional)

  • position_jitter (Numeric, Array, Hash)

    position rounding error (keyword, optional)

  • duration_jitter (Numeric, Array, Hash)

    duration rounding error (keyword, optional)

  • right_open (Boolean, Array, Hash)

    whether final value is excluded (keyword, optional)

Returns:

  • (MoveControl)

    control object



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_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).

Examples:

seq.now { puts "Executes now" }

Yields:

  • block to execute at current position

Returns:

  • (EventHandler)

    control object (supports .stop to cancel)



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.

Examples:

Basic event pub/sub

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

received_values = []

# Subscribe to custom event
seq.on(:note_played) do |pitch, velocity|
  received_values << { pitch: pitch, velocity: velocity }
end

# Launch event from scheduled block
seq.at(1) do
  seq.launch(:note_played, 60, 100)
end

seq.at(2) do
  seq.launch(:note_played, 64, 80)
end

seq.run

# received_values contains [{pitch: 60, velocity: 100}, {pitch: 64, velocity: 80}]

Hierarchical event handling with control

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

global_events = []
local_events = []

# Global handler (sequencer level)
seq.on(:finished) do |name|
  global_events << name
end

# Local handler (control level)
control = seq.at(1) do |control:|
  control.launch(:finished, "local task")
end

control.on(:finished) do |name|
  local_events << name
end

seq.run

# local_events contains ["local task"]
# global_events is empty (event handled locally, doesn't bubble up)

Parameters:

  • event (Symbol)

    event name

Yields:

  • (*args)

    event handler receiving event parameters



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.

Examples:

Monitoring event execution

seq = Musa::Sequencer::BaseSequencer.new(4, 24, do_log: true)

debug_calls = []

seq.on_debug_at do
  debug_calls << { position: seq.position, time: Time.now }
end

seq.at(1) { puts "Event 1" }
seq.at(2) { puts "Event 2" }

seq.run

# debug_calls now contains [{position: 1, time: ...}, {position: 2, time: ...}]

Yields:

  • debug callback (receives no parameters)



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.

Examples:

Handling errors in scheduled events

seq = Musa::Sequencer::BaseSequencer.new(4, 24, do_error_log: false)

errors = []

seq.on_error do |error|
  errors << { message: error.message, position: seq.position }
end

seq.at(1) { puts "Normal event" }
seq.at(2) { raise "Something went wrong!" }
seq.at(3) { puts "This still executes" }

seq.run

# errors now contains [{message: "Something went wrong!", position: 2}]
# All events execute despite the error at position 2

Yields:

  • (error)

    error callback receiving the exception object

Yield Parameters:

  • error (StandardError, ScriptError)

    the exception that occurred



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.

Examples:

Tracking fast-forward operations

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

ff_state = []

seq.on_fast_forward do |is_starting|
  if is_starting
    ff_state << "Fast-forward started from position #{seq.position}"
  else
    ff_state << "Fast-forward ended at position #{seq.position}"
  end
end

seq.at(1) { puts "Event 1" }
seq.at(5) { puts "Event 5" }

# Jump to position 10 (executes events at 1 and 5 during fast-forward)
seq.position = 10

# ff_state contains ["Fast-forward started from position 0", "Fast-forward ended at position 10"]

Yields:

  • (is_starting)

    callback receiving fast-forward state

Yield Parameters:

  • is_starting (Boolean)

    true when fast-forward begins, false when it ends



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.

Examples:

Playing notes from a series

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

notes = Musa::Series::S(60, 62, 64).zip(Musa::Series::S(1, 1, 2))
played_notes = []

seq.play(notes) do |pitch, duration|
  played_notes << { pitch: pitch, duration: duration, position: seq.position }
end

seq.run
# Result: played_notes contains [{pitch: 60, duration: 1, position: 0}, ...]

Parallel plays

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

melody = Musa::Series::S(60, 62, 64)
harmony = Musa::Series::S(48, 52, 55)
played_notes = []

seq.play([melody, harmony]) do |pitch|
  # pitch will be array [melody_pitch, harmony_pitch]
  played_notes << { melody: pitch[0], harmony: pitch[1], position: seq.position }
end

seq.run
# Result: played_notes contains [{melody: 60, harmony: 48, position: 0}, ...]

Parameters:

  • serie (Series)

    series to play

  • mode (Symbol) (defaults to: nil)

    running mode (:at, :wait, :neumalang). Defaults to :wait

  • parameter (Symbol, nil) (defaults to: nil)

    duration parameter name from serie values

  • on_stop (Proc, nil) (defaults to: nil)

    callback when play stops (any reason, including manual stop)

  • after_bars (Numeric, nil) (defaults to: nil)

    delay for after callback

  • after (Proc, nil) (defaults to: nil)

    callback after play completes naturally (NOT on manual stop)

  • context (Object, nil) (defaults to: nil)

    context for neumalang processing

  • mode_args (Hash)

    additional mode-specific parameters

Yields:

  • block executed for each serie value (via SmartProcBinder — declare only the parameters you need)

Yield Parameters:

  • element (Object)

    the current serie element (positional). When the element is a Hash, its keys are also available as keyword arguments (e.g., |note:, duration:|)

  • control (PlayControl)

    the play control object (keyword, optional)

Returns:

  • (PlayControl)

    control object



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_bars, after: after
  @event_handlers.push control

  _play serie.instance, control, context, mode: mode, parameter: parameter, **mode_args, &block

  @event_handlers.pop

  @playing << control

  control.on_stop do
    @playing.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.

Examples:

Hash mode timed series

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

timed_notes = Musa::Series::S(
  { time: 0r, value: {pitch: 60, velocity: 96} },
  { time: 1r, value: {pitch: 64, velocity: 80} },
  { time: 2r, value: {pitch: 67, velocity: 64} }
)

played_notes = []

seq.play_timed(timed_notes) do |values, time:, started_ago:, control:|
  played_notes << { pitch: values[:pitch], velocity: values[:velocity], time: time }
end

seq.run
# Result: played_notes contains [{pitch: 60, velocity: 96, time: 0r}, ...]

Array mode with extra attributes

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

timed = Musa::Series::S(
  { time: 0r, value: [60, 96], channel: 0 },
  { time: 1r, value: [64, 80], channel: 1 }
)

played_notes = []

seq.play_timed(timed) do |values, channel:, time:, started_ago:, control:|
  played_notes << { pitch: values[0], velocity: values[1], channel: channel, time: time }
end

seq.run
# Result: played_notes contains [{pitch: 60, velocity: 96, channel: 0, time: 0r}, ...]

Parameters:

  • timed_serie (Series)

    timed series

  • at (Rational, nil) (defaults to: nil)

    starting position

  • on_stop (Proc, nil) (defaults to: nil)

    callback when playback stops

  • after_bars (Numeric, nil) (defaults to: nil)

    schedule after completion

  • after (Proc, nil) (defaults to: nil)

    block after completion

Yields:

  • block for each timed value (via SmartProcBinder — declare only the parameters you need)

Yield Parameters:

  • values (Hash, Array)

    current component values (positional). Hash in hash mode, Array in array mode

  • time (Rational)

    absolute position of this event (keyword, optional)

  • started_ago (Hash, Array)

    time since each component's last update (keyword, optional)

  • control (PlayTimedControl)

    the play_timed control object (keyword, optional)

Returns:

  • (PlayTimedControl)

    control object



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_bars, after: after)

  @event_handlers.push control

  _play_timed(timed_serie.instance, at, control, &block)

  @event_handlers.pop

  @playing << control

  control.on_stop do
    @playing.delete control
  end

  control
end

#quantize_position(position, warn: nil) ⇒ Rational

Quantizes position to tick grid (tick-based mode only).

Parameters:

  • position (Rational)

    position to quantize

  • warn (Boolean) (defaults to: nil)

    emit warning if quantization changes value

Returns:



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).

Parameters:

  • bar_position (Numeric)

    absolute position

  • force_first (Boolean) (defaults to: nil)

    force execution before other events at same time

Yields:

  • block to execute

Returns:

  • (nil)


606
607
608
609
610
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 606

def raw_at(bar_position, force_first: nil, &block)
  _raw_numeric_at bar_position.rationalize, force_first: force_first, &block

  nil
end

#resetvoid

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.

Examples:

Resetting sequencer state

seq = Musa::Sequencer::BaseSequencer.new(4, 24)

# Schedule some events
seq.at(1) { puts "Event 1" }
seq.at(2) { puts "Event 2" }
seq.every(1, till: 8) { puts "Repeating" }

puts seq.size  # => 2 (scheduled events)
puts seq.empty?  # => false

# Reset clears everything
seq.reset

puts seq.size  # => 0
puts seq.empty?  # => true
puts seq.position  # => 0


265
266
267
268
269
270
271
272
273
274
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 265

def reset
  @timeslots.clear
  @everying.clear
  @playing.clear
  @moving.clear

  @event_handlers = [EventHandler.new]

  _reset_timing
end

#runvoid

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.

Examples:

seq.at(1) { puts "Event 1" }
seq.at(2) { puts "Event 2" }
seq.run  # Executes both events


312
313
314
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 312

def run
  tick until empty?
end

#sizeInteger

Counts total scheduled events.

Returns:

  • (Integer)

    number of scheduled events across all timeslots



279
280
281
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 279

def size
  @timeslots.values.sum(&:size)
end

#to_sObject



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.

Examples:

Basic wait

seq.wait(2) { puts "2 beats later" }

Cancelling a scheduled wait

h = seq.wait(4) { puts "won't run" }
h.stop

Parameters:

  • bars_delay (Numeric, Series, Array)

    delay from current position

  • debug (Boolean) (defaults to: nil)

    enable debug logging

Yields:

  • block to execute at position + delay

Returns:

  • (EventHandler)

    control object (supports .stop to cancel)



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(bars_delay, debug: nil, &block)
  debug ||= false

  control = EventHandler.new @event_handlers.last
  @event_handlers.push control

  if bars_delay.is_a? Numeric
    _numeric_at position + bars_delay.rationalize, control, debug: debug, skip_if_stopped: true, &block
  else
    bars_delay = Series::S(*bars_delay) if bars_delay.is_a?(Array)
    bars_delay = bars_delay.instance if bars_delay

    _serie_at bars_delay.with { |delay| position + delay }, control, debug: debug, &block
  end

  @event_handlers.pop

  control
end