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 advances to the next scheduled position (it takes no argument)
  • 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

The per-method examples further down are written against:

sequencer = BaseSequencer.new(4, 24)

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  # Jumps to position 1 -- tick takes no argument; in tickless
seq.tick  # mode it advances to wherever the next event is scheduled.

Playing series

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

# Series are combined with H, which yields the hashes play consumes;
# there is no `zip`. The :duration of each element is what makes play
# walk time.
notes = Musa::Series::Constructors.H(
  pitch: Musa::Series::Constructors.S(60, 62, 64, 65, 67),
  duration: Musa::Series::Constructors.S(1r, 1r, 1/2r, 1/2r, 2r))
played_notes = []

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

seq.run
played_notes.collect { |n| n[:pitch] }  # => [60, 62, 64, 65, 67]

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 Classes: EventHandler, EveryControl, MoveControl, PlayControl, PlayTimedControl

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



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
240
241
242
243
244
245
246
247
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 199

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)



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

def beats_per_bar
  @beats_per_bar
end

#everyingArray<EveryControl> (readonly)

Returns active every loops.

Returns:



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

def everying
  @everying
end

#loggerMusa::Logger::Logger (readonly)

Returns sequencer logger.

Returns:



162
163
164
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 162

def logger
  @logger
end

#movingArray<MoveControl> (readonly)

Returns active move operations.

Returns:



159
160
161
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 159

def moving
  @moving
end

#offsetRational (readonly)

Returns time offset for position calculations.

Returns:

  • (Rational)

    time offset for position calculations



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

def offset
  @offset
end

#playingArray<PlayControl, PlayTimedControl> (readonly)

Returns active play operations.

Returns:



156
157
158
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 156

def playing
  @playing
end

#running_positionRational (readonly)

Returns current running position.

Returns:

  • (Rational)

    current running position



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

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)



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

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



287
288
289
290
291
292
293
294
# File 'lib/musa-dsl/sequencer/base-sequencer-implementation.rb', line 287

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)



643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 643

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



468
469
470
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 468

def before_tick(&block)
  @before_tick << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
end

#continuation_play(parameters) ⇒ Object



753
754
755
756
757
758
759
760
761
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 753

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



1101
1102
1103
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1101

def debug(msg = nil)
  @logger.debug { msg || '...' }
end

#empty?Boolean

Checks if sequencer has no scheduled events.

Returns:

  • (Boolean)

    true if no events scheduled



296
297
298
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 296

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:



330
331
332
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 330

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:



925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 925

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:



545
546
547
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 545

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:



1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1072

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)



598
599
600
601
602
603
604
605
606
607
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 598

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



529
530
531
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 529

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)



358
359
360
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 358

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



389
390
391
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 389

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



424
425
426
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 424

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::Constructors.H(
  pitch: Musa::Series::Constructors.S(60, 62, 64),
  duration: Musa::Series::Constructors.S(1r, 1r, 2r))
played_notes = []

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

seq.run
played_notes.collect { |n| n[:pitch] }  # => [60, 62, 64]

Parallel plays

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

# Simultaneous voices are simultaneous plays, one per voice: play takes
# a serie, not an array of them.
melody = Musa::Series::Constructors.H(
  pitch: Musa::Series::Constructors.S(60, 62, 64),
  duration: Musa::Series::Constructors.S(1r, 1r, 1r))
harmony = Musa::Series::Constructors.H(
  pitch: Musa::Series::Constructors.S(48, 52, 55),
  duration: Musa::Series::Constructors.S(1r, 1r, 1r))
played_notes = []

[melody, harmony].each do |voice|
  seq.play(voice) { |pitch:, duration:| played_notes << pitch }
end

seq.run
played_notes  # => [60, 48, 62, 52, 64, 55]

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:



725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 725

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::Constructors.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
# played_notes == [{pitch: 60, velocity: 96, time: 95/96r},
#                  {pitch: 64, velocity: 80, time: 191/96r},
#                  {pitch: 67, velocity: 64, time: 287/96r}]
#
# NOTE the yielded `time:` is the sequencer's ABSOLUTE POSITION when the
# event fires, not the `time:` of the serie's element. The sequencer sits
# one tick before bar 1 until it starts (95/96 of a bar, with 24 ticks per
# beat and 4 beats per bar), so that its first tick lands exactly on bar 1;
# the element at serie time 0 fires there. The intervals do match the
# serie: each following event is exactly one bar later.

Array mode with extra attributes

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

timed = Musa::Series::Constructors.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:



845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 845

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:



306
307
308
309
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 306

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)


616
617
618
619
620
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 616

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  # => 95/96
# Back to one tick before bar 1, not to zero: with 4 beats of 24 ticks
# the reset position is 1r - 1/96r, so the next tick lands on bar 1.


275
276
277
278
279
280
281
282
283
284
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 275

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:

Running until nothing is left

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


322
323
324
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 322

def run
  tick until empty?
end

#sizeInteger

Counts total scheduled events.

Returns:

  • (Integer)

    number of scheduled events across all timeslots



289
290
291
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 289

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

#to_sObject



1105
1106
1107
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 1105

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)



567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 567

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