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, 4, 24)  # positional: beats/bar, ticks/beat

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

clock.bpm             # => (140/1)
clock.ticks_per_beat  # => (24/1)

# The correction shifts WHEN each tick fires, not how many there are:
# neither the tempo nor the resolution moves.

Dynamic tempo changes

clock = TimerClock.new(bpm: 120)
clock.period  # => (1/48)

# ... later, while running:
clock.bpm = 140  # Tempo change takes effect immediately

clock.bpm     # => (140/1)
clock.period  # => (1/56)

# The period follows the tempo: 60 / (bpm * ticks_per_beat), rational
# throughout, so a tempo change does not accumulate rounding.

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

# 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(0.02083, ticks_per_beat: 24)  # period is positional

three = [TimerClock.new(bpm: 120, ticks_per_beat: 24),
         TimerClock.new(bpm: 120),
         TimerClock.new(0.02083, ticks_per_beat: 24)]

three.map(&:bpm)             # => [(120/1), (120/1), (120/1)]
three.map(&:ticks_per_beat)  # => [(24/1), (24/1), (24/1)]
three.map(&:period)          # => [(1/48), (1/48), (1/48)]

# The third really is equal and not merely close: the period is
# rationalized, and 0.02083 rationalizes to 1/48.

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



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/musa-dsl/transport/timer-clock.rb', line 116

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:



153
154
155
# File 'lib/musa-dsl/transport/timer-clock.rb', line 153

def bpm
  @bpm
end

#periodRational

Current tick period in seconds.

Returns:



143
144
145
# File 'lib/musa-dsl/transport/timer-clock.rb', line 143

def period
  @period
end

#ticks_per_beatRational

Number of ticks per beat.

Returns:

  • (Rational)

    ticks per beat (typically 24 or 96)



148
149
150
# File 'lib/musa-dsl/transport/timer-clock.rb', line 148

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:



308
309
310
311
312
313
# File 'lib/musa-dsl/transport/timer-clock.rb', line 308

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:



293
294
295
296
297
298
# File 'lib/musa-dsl/transport/timer-clock.rb', line 293

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

#paused?Boolean

Checks if the clock is paused.

Returns:

  • (Boolean)

    true if paused



215
216
217
# File 'lib/musa-dsl/transport/timer-clock.rb', line 215

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



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/musa-dsl/transport/timer-clock.rb', line 230

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.



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

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)



208
209
210
# File 'lib/musa-dsl/transport/timer-clock.rb', line 208

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



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

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.



323
324
325
326
327
# File 'lib/musa-dsl/transport/timer-clock.rb', line 323

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