Class: Musa::Clock::TimerClock

Inherits:
Clock show all
Defined in:
lib/musa-dsl/transport/timer-clock.rb

Overview

Internal timer-based clock for standalone operation.

TimerClock uses a high-precision Timer to generate ticks at a configurable rate. Unlike DummyClock, TimerClock requires external activation and is designed for scenarios where timing control comes from outside (e.g., live coding clients, interactive systems).

Activation Model

IMPORTANT: TimerClock starts in a paused state. After calling transport.start (which blocks), you must call clock.start() from another thread to begin generating ticks.

This activation model is appropriate for:

  • Live coding: Client controls when to start/stop
  • Interactive systems: External controller manages playback
  • Testing with control: Precise control over when ticks begin

Configuration Methods

The clock can be configured in three equivalent ways:

  1. BPM + ticks_per_beat: Musical tempo-based (most common)
  2. Period: Direct tick period in seconds
  3. Any combination: Changes one parameter, others auto-calculate

Relationship Between Parameters

period = 60 / (bpm * ticks_per_beat)

Example: 120 BPM, 24 ticks/beat → period = 60/(120*24) = 0.02083s

States

  • Not started: Clock created but not running
  • Started: Clock running, generating ticks
  • Paused: Clock started but temporarily stopped

Examples:

Complete setup with external activation (live coding pattern)

clock = TimerClock.new(bpm: 120, ticks_per_beat: 24)
transport = Transport.new(clock, beats_per_bar: 4)

# Schedule events
transport.sequencer.at(4) { transport.stop }

# Start transport in background (it blocks)
thread = Thread.new { transport.start }
sleep 0.1  # Let transport initialize

# Activate clock from external control (e.g., live coding client)
clock.start  # Now ticks begin generating

thread.join  # Wait for completion

With timing correction

# Correction compensates for system-specific timing offsets
clock = TimerClock.new(bpm: 140, correction: -0.001)

Dynamic tempo changes

clock = TimerClock.new(bpm: 120)
# ... later, while running:
clock.bpm = 140  # Tempo change takes effect immediately

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(period = nil, ticks_per_beat: nil, bpm: nil, correction: nil, delayed_ticks_error: nil, logger: nil, do_log: nil) ⇒ TimerClock

Creates a new timer-based clock.

At least one timing parameter must be provided (period, bpm, or ticks_per_beat). Missing parameters use defaults: bpm=120, ticks_per_beat=24.

Examples:

# All equivalent for 120 BPM, 24 ticks/beat:
TimerClock.new(bpm: 120, ticks_per_beat: 24)
TimerClock.new(bpm: 120)  # ticks_per_beat defaults to 24
TimerClock.new(period: 0.02083, ticks_per_beat: 24)

Parameters:

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

    tick period in seconds (direct specification)

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

    number of ticks per beat (default: 24)

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

    beats per minute (default: 120)

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

    timing correction in seconds (for calibration)

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

    threshold for error-level logging

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

    logger for warnings/errors

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

    enable timing logs



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/musa-dsl/transport/timer-clock.rb', line 91

def initialize(period = nil, ticks_per_beat: nil, bpm: nil, correction: nil, delayed_ticks_error: nil, logger: nil, do_log: nil)
  do_log ||= false

  super()

  @correction = correction

  # Set parameters in any combination
  self.period = period if period
  self.ticks_per_beat = ticks_per_beat if ticks_per_beat
  self.bpm = bpm if bpm

  # Apply defaults
  self.bpm ||= 120
  self.ticks_per_beat ||= 24

  @started = false
  @paused = false

  @delayed_ticks_error = delayed_ticks_error
  @logger = logger
  @do_log = do_log
end

Instance Attribute Details

#bpmRational

Current tempo in beats per minute.

Returns:



128
129
130
# File 'lib/musa-dsl/transport/timer-clock.rb', line 128

def bpm
  @bpm
end

#periodRational

Current tick period in seconds.

Returns:



118
119
120
# File 'lib/musa-dsl/transport/timer-clock.rb', line 118

def period
  @period
end

#ticks_per_beatRational

Number of ticks per beat.

Returns:

  • (Rational)

    ticks per beat (typically 24 or 96)



123
124
125
# File 'lib/musa-dsl/transport/timer-clock.rb', line 123

def ticks_per_beat
  @ticks_per_beat
end

Instance Method Details

#continuevoid

Note:

No effect if not started or not paused

This method returns an undefined value.

Resumes the clock from paused state.

Continues generating ticks. Has no effect if not paused.

See Also:



274
275
276
277
278
279
# File 'lib/musa-dsl/transport/timer-clock.rb', line 274

def continue
  if @started && @paused
    @paused = false
    @timer.continue
  end
end

#pausevoid

Note:

No effect if not started or already paused

This method returns an undefined value.

Pauses the clock without stopping it.

Ticks stop but clock remains in started state. Use #continue to resume.

See Also:



259
260
261
262
263
264
# File 'lib/musa-dsl/transport/timer-clock.rb', line 259

def pause
  if @started && !@paused
    @timer.stop
    @paused = true
  end
end

#paused?Boolean

Checks if the clock is paused.

Returns:

  • (Boolean)

    true if paused



181
182
183
# File 'lib/musa-dsl/transport/timer-clock.rb', line 181

def paused?
  @paused
end

#run { ... } ⇒ void

Note:

This method blocks until #terminate is called

Note:

Clock begins paused; call #start to begin ticking

This method returns an undefined value.

Starts the clock's run loop.

This method blocks and runs the timer loop, yielding for each tick. The clock starts in a paused state and must be explicitly started via #start.

Yields:

  • Called once per tick



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/musa-dsl/transport/timer-clock.rb', line 196

def run
  @run = true
  @stopped = false

  while @run
    @timer = Timer.new(@period,
                       correction: @correction,
                       stop: true,
                       delayed_ticks_error: @delayed_ticks_error,
                       logger: @logger,
                       do_log: @do_log)

    @timer.run do
      yield if block_given?
    end
  end
end

#startvoid

Note:

Must call #run first to start the run loop

Note:

Calls registered on_start callbacks

This method returns an undefined value.

Starts the clock from paused state.

Triggers @on_start callbacks and begins generating ticks. Has no effect if already started.



223
224
225
226
227
228
229
230
231
# File 'lib/musa-dsl/transport/timer-clock.rb', line 223

def start
  unless @started
    @stopped = false
    @on_start.each(&:call)
    @started = true
    @paused = false
    @timer.continue
  end
end

#started?Boolean

Checks if the clock has been started.

Returns:

  • (Boolean)

    true if started (even if currently paused)



174
175
176
# File 'lib/musa-dsl/transport/timer-clock.rb', line 174

def started?
  @started
end

#stopvoid

Note:

Different from #pause: stop resets to initial state

This method returns an undefined value.

Stops the clock and resets to initial state.

Stops the internal timer, resets state flags, and fires on_stop callbacks via super (idempotent).



241
242
243
244
245
246
247
248
# File 'lib/musa-dsl/transport/timer-clock.rb', line 241

def stop
  if @started
    @timer.stop
    @started = false
    @paused = false
  end
  super
end

#terminatevoid

Note:

After calling this, #run will exit and Transport::Transport#start will return

This method returns an undefined value.

Terminates the clock's run loop.

Calls #stop to ensure on_stop callbacks fire, then exits the run loop by terminating the internal timer.



289
290
291
292
293
# File 'lib/musa-dsl/transport/timer-clock.rb', line 289

def terminate
  stop
  @run = false
  @timer&.terminate
end