Class: Musa::Transport::Transport
- Defined in:
- lib/musa-dsl/transport/transport.rb
Overview
Main transport class connecting clocks to sequencers with lifecycle management.
Transport acts as the bridge between a clock (timing source) and a sequencer (event scheduler). It manages the playback lifecycle, including initialization, start/stop, and position changes, with support for callbacks at each stage.
Architecture
Clock --ticks--> Transport --tick()--> Sequencer --events--> Music
Lifecycle Phases
- before_begin: Run once before first start (initialization)
- on_start: Run each time transport starts
- Running: Clock generates ticks → sequencer processes events
- on_position_change: Run when position jumps/seeks
- after_stop: Run when transport stops
Position Management
Transport handles three position formats:
- bars: Musical position in bars (Rational)
- beats: Position in beats
- midi_beats: Position in MIDI beats (for MIDI Clock sync)
Use Cases
- Standalone compositions with internal timing
- DAW-synchronized playback via MIDI Clock
- Testing with dummy/external clocks
- Live coding with dynamic tempo changes
Instance Attribute Summary collapse
-
#sequencer ⇒ Sequencer::Sequencer
readonly
The sequencer instance managing event scheduling.
Instance Method Summary collapse
-
#after_stop {|sequencer| ... } ⇒ void
Registers a callback to run when the transport stops.
-
#before_begin {|sequencer| ... } ⇒ void
Registers a callback to run once before the first start.
-
#change_position_to(bars: nil, beats: nil, midi_beats: nil) ⇒ void
Changes the playback position (seek/jump).
-
#initialize(clock, beats_per_bar = nil, ticks_per_beat = nil, offset: nil, sequencer: nil, before_begin: nil, on_start: nil, after_stop: nil, on_position_change: nil, logger: nil, do_log: nil) ⇒ Transport
constructor
Creates a new transport connecting a clock to a sequencer.
-
#logger ⇒ Logger
Returns the transport's logger.
-
#on_change_position {|sequencer| ... } ⇒ void
Registers a callback for position changes.
-
#on_start {|sequencer| ... } ⇒ void
Registers a callback to run each time the transport starts.
-
#start ⇒ void
Starts the transport and begins playback.
-
#stop ⇒ void
Stops the transport.
Constructor Details
#initialize(clock, beats_per_bar = nil, ticks_per_beat = nil, offset: nil, sequencer: nil, before_begin: nil, on_start: nil, after_stop: nil, on_position_change: nil, logger: nil, do_log: nil) ⇒ Transport
Creates a new transport connecting a clock to a sequencer.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/musa-dsl/transport/transport.rb', line 110 def initialize(clock, = nil, ticks_per_beat = nil, offset: nil, sequencer: nil, before_begin: nil, on_start: nil, after_stop: nil, on_position_change: nil, logger: nil, do_log: nil) ||= 4 ticks_per_beat ||= 24 offset ||= 0r do_log ||= false @clock = clock @before_begin = [] @before_begin << Musa::Extension::SmartProcBinder::SmartProcBinder.new(before_begin) if before_begin @on_start = [] @on_start << Musa::Extension::SmartProcBinder::SmartProcBinder.new(on_start) if on_start @on_change_position = [] @on_change_position << Musa::Extension::SmartProcBinder::SmartProcBinder.new(on_position_change) if on_position_change @after_stop = [] @after_stop << Musa::Extension::SmartProcBinder::SmartProcBinder.new(after_stop) if after_stop @do_log = do_log @sequencer = sequencer @sequencer ||= Musa::Sequencer::Sequencer.new , ticks_per_beat, offset: offset, logger: logger, do_log: @do_log @clock.on_start do do_on_start end @clock.on_stop do do_stop end @clock.on_change_position do |bars: nil, beats: nil, midi_beats: nil| change_position_to bars: , beats: beats, midi_beats: midi_beats end # TODO: Consider adding block/DSL support for cleaner initialization syntax. # # Future enhancement could yield a DSL context that provides direct access to # transport methods without requiring the transport variable. This would enable: # # Transport.new(clock, 4, 24) do # before_begin { setup_instruments } # on_start { start_recording } # after_stop { save_recording } # end # # Instead of current approach: # # transport = Transport.new(clock, 4, 24) # transport.before_begin { setup_instruments } # transport.on_start { start_recording } # transport.after_stop { save_recording } # # Implementation would require a DSL context object that delegates methods to # the transport instance, similar to the sequencer DSL pattern. end |
Instance Attribute Details
#sequencer ⇒ Sequencer::Sequencer (readonly)
The sequencer instance managing event scheduling.
84 85 86 |
# File 'lib/musa-dsl/transport/transport.rb', line 84 def sequencer @sequencer end |
Instance Method Details
#after_stop {|sequencer| ... } ⇒ void
This method returns an undefined value.
Registers a callback to run when the transport stops.
after_stop callbacks run when the clock stops, before the sequencer is reset.
230 231 232 |
# File 'lib/musa-dsl/transport/transport.rb', line 230 def after_stop(&block) @after_stop << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#before_begin {|sequencer| ... } ⇒ void
This method returns an undefined value.
Registers a callback to run once before the first start.
before_begin callbacks are run only once, before the very first start. They're ideal for one-time setup like loading samples or initializing state.
After a stop, before_begin runs again before the next start.
197 198 199 |
# File 'lib/musa-dsl/transport/transport.rb', line 197 def before_begin(&block) @before_begin << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#change_position_to(bars: nil, beats: nil, midi_beats: nil) ⇒ void
Backward seeks trigger stop/restart cycle
Calls on_change_position callbacks at new position
Calls sequencer.on_fast_forward callbacks during forward seek
This method returns an undefined value.
Changes the playback position (seek/jump).
Handles position changes from various sources, converting between formats. IMPORTANT: Position changes trigger fast-forward, which executes all intermediate events between current and target positions.
Fast-Forward Behavior
When changing position forward:
- Calls
sequencer.on_fast_forwardcallbacks withtrue(entering fast-forward) - Ticks through all intermediate positions, executing all scheduled events
- Calls
sequencer.on_fast_forwardcallbacks withfalse(exiting fast-forward) - Calls
on_change_positioncallbacks at the new position
When changing position backward (requires stop):
- Stops the transport (calls
after_stopcallbacks) - Resets the sequencer to initial state
- Sets position to target
- Restarts (calls
on_startcallbacks)
Handling Intermediate Events
During fast-forward, all scheduled events execute. To prevent unwanted sound
output (e.g., MIDI notes), use on_fast_forward callbacks:
- MIDIVoices integration: Set
midi_voices.fast_forward = trueduring fast-forward to register note state internally without emitting MIDI messages - Custom handlers: Check fast-forward state in event handlers to skip audio/visual output during position jumps
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 |
# File 'lib/musa-dsl/transport/transport.rb', line 377 def change_position_to(bars: nil, beats: nil, midi_beats: nil) logger.debug('Transport') do "asked to change position to #{"#{bars} bars " if bars}#{"#{beats} beats " if beats}" \ "#{"#{midi_beats} midi beats " if midi_beats}" end # Calculate position from provided parameters position = &.rationalize || 1r position += Rational(midi_beats, 4 * @sequencer.) if midi_beats position += Rational(beats, @sequencer.) if beats # Adjust for sequencer offset and tick duration position += @sequencer.offset position -= @sequencer.tick_duration raise ArgumentError, "undefined new position" unless position logger.debug('Transport') { "received message position change to #{position.inspect}" } start_again_later = false # Backward seek requires stop/restart to reinitialize state if @sequencer.position > position do_stop start_again_later = true end logger.debug('Transport') { "setting sequencer position #{position.inspect}" } # Schedule position change callback at new position @sequencer.raw_at position, force_first: true do @on_change_position.each { |block| block.call @sequencer } end @sequencer.position = position do_on_start if start_again_later end |
#logger ⇒ Logger
Returns the transport's logger.
Delegates to sequencer's logger.
431 432 433 |
# File 'lib/musa-dsl/transport/transport.rb', line 431 def logger @sequencer.logger end |
#on_change_position {|sequencer| ... } ⇒ void
This method returns an undefined value.
Registers a callback for position changes.
Called when playback position changes non-linearly (seek/jump), typically from MIDI Song Position Pointer or manual position changes.
248 249 250 |
# File 'lib/musa-dsl/transport/transport.rb', line 248 def on_change_position(&block) @on_change_position << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#on_start {|sequencer| ... } ⇒ void
This method returns an undefined value.
Registers a callback to run each time the transport starts.
on_start callbacks run every time #start is called, after before_begin.
213 214 215 |
# File 'lib/musa-dsl/transport/transport.rb', line 213 def on_start(&block) @on_start << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block) end |
#start ⇒ void
Blocking behavior varies by clock type (see above)
For TimerClock, must call clock.start from separate thread
This method returns an undefined value.
Starts the transport and begins playback.
Runs before_begin (if first start or after stop), then starts the clock's run loop. Behavior depends on the clock type:
Clock Activation by Type
DummyClock (automatic activation):
- Starts generating ticks immediately
- Blocks until tick count/condition completes
- No external activation needed
TimerClock (external activation required):
- Blocks but remains paused until
clock.startis called - Must call
clock.start()from another thread to begin ticks - Typical pattern:
Thread.new { transport.start }thenclock.start
InputMidiClock (MIDI activation required):
- Blocks waiting for MIDI "Start" (0xFA) message
- External DAW/device controls when ticks begin
- Automatically starts when MIDI Start received
ExternalTickClock (manual tick control):
- Returns immediately (doesn't block)
- Call
clock.tick()manually to generate each tick - Complete control over timing from external system
313 314 315 316 317 318 319 320 |
# File 'lib/musa-dsl/transport/transport.rb', line 313 def start do_before_begin unless @before_begin_already_done @clock.run do @before_begin_already_done = false @sequencer.tick end end |
#stop ⇒ void
This method returns an undefined value.
Stops the transport.
Terminates the clock, which triggers the stop sequence (after_stop callbacks, sequencer reset, etc.)
422 423 424 |
# File 'lib/musa-dsl/transport/transport.rb', line 422 def stop @clock.terminate end |