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

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)

Raises:

  • (ArgumentError)

    if only one of beats_per_bar/ticks_per_beat provided



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



117
118
119
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 117

def beats_per_bar
  @beats_per_bar
end

#everyingArray<EveryControl> (readonly)

Returns active every loops.



129
130
131
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 129

def everying
  @everying
end

#loggerMusa::Logger::Logger (readonly)

Returns sequencer logger.



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

def logger
  @logger
end

#movingArray<MoveControl> (readonly)

Returns active move operations.



135
136
137
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 135

def moving
  @moving
end

#offsetRational (readonly)

Returns time offset for position calculations.



123
124
125
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 123

def offset
  @offset
end

#playingArray<PlayControl, PlayTimedControl> (readonly)

Returns active play operations.



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

def playing
  @playing
end

#running_positionRational (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_beatRational? (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.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.

Examples:

Single position

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

Series of positions

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

Yields:

  • block to execute at 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(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, &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



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



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:

  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

Examples:

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

Every 4 beats for 16 bars

sequencer.every(1r, duration: 4r) { puts "tick" }
# Executes at 1r, 2r, 3r, 4r, 5r (5 times total)

Every beat until position 10

sequencer.every(1r, till: 10r) { |control| puts control.position }

Conditional loop

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

Yields:

  • (position)

    block executed each interval



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

  @event_handlers.push control

  _every interval, control, &block

  @event_handlers.pop

  @everying << control

  control.after 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.

See Also:



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

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]

Yields:

  • (value)

    block executed with interpolated value



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

Examples:

seq.now { puts "Executes now" }

Yields:

  • block to execute at current position



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.

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)

Yields:

  • (*args)

    event handler receiving event parameters



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.

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)



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.

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



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.

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



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.

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}, ...]

Yields:

  • (value)

    block executed for each serie value



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_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.after do
    @playing.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.

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}, ...]

Yields:

  • (value)

    block for each value



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

  @playing << control

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

Yields:

  • block to execute



577
578
579
580
581
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 577

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


249
250
251
252
253
254
255
256
257
258
# File 'lib/musa-dsl/sequencer/base-sequencer.rb', line 249

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


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

def run
  tick until empty?
end

#sizeInteger

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_sObject



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.

Examples:

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

Yields:

  • block to execute at position + delay



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(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, &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