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:



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

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:



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

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
# File 'lib/musa-dsl/transport/timer-clock.rb', line 196

def run
  @run = true

  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.



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

def start
  unless @started
    @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:

Calls registered on_stop callbacks

Note:

Different from #pause: stop resets to initial state

This method returns an undefined value.

Stops the clock and resets to initial state.

Triggers @on_stop callbacks and marks clock as not started. Has no effect if not currently started.



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

def stop
  if @started
    @timer.stop
    @started = false
    @paused = false
    @on_stop.each(&:call)
  end
end

#terminatevoid

Note:

After calling this, #run will exit

This method returns an undefined value.

Terminates the clock's run loop.

Causes #run to exit. This is the clean shutdown mechanism.



287
288
289
# File 'lib/musa-dsl/transport/timer-clock.rb', line 287

def terminate
  @run = false
end