Class: Musa::Clock::DummyClock

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

Overview

Simple clock for testing with fixed tick count or custom condition.

DummyClock is designed for testing and batch processing where automatic execution without external dependencies is desired.

Activation Model

IMPORTANT: Unlike TimerClock, InputMidiClock, and ExternalTickClock, DummyClock activates automatically when transport.start is called. It immediately begins generating ticks without waiting for external signals.

This activation model is appropriate for:

  • Unit testing: No external dependencies, deterministic execution
  • Batch processing: Generate music as fast as possible
  • Fast-forward simulations: Skip real-time delays
  • Deterministic debugging: Predictable tick counts

Modes of Operation

  1. Fixed tick count: Runs for exactly N ticks then stops
  2. Custom condition: Runs while a block returns true

Differences from Other Clocks

DummyClock is the only clock that starts generating ticks immediately upon transport.start. It uses Thread.pass instead of sleep, making execution as fast as possible without real-time constraints.

Examples:

Fixed tick count (automatic activation)

clock = DummyClock.new(100)  # Exactly 100 ticks
transport = Transport.new(clock, 4, 24)

pulses = []
transport.sequencer.every(1) { pulses << transport.sequencer.position }

transport.start  # Immediately runs 100 ticks, then stops

pulses  # => [(95/96), (191/96)]

# 4 beats of 24 ticks is 96 ticks to the bar, so a hundred of them is one
# bar and a bit: an `every 1` written outside any `at` pulses twice, one
# tick before each bar. The clock stops where it stops -- nothing rounds
# it up to a whole bar.

Custom condition (automatic activation)

continue = true
clock = DummyClock.new { continue }
transport = Transport.new(clock, 4, 24)

pulses = 0
transport.sequencer.every(1) do
  pulses += 1
  continue = false if pulses >= 5
end

transport.start  # Immediately begins, stops when the block says so

pulses  # => 5

# The block is asked BEFORE each tick, so what it guards is the tick that
# has not happened yet.

Testing specific sequences

ticks = 0
some_condition = true
clock = DummyClock.new { ticks < 50 || some_condition }
transport = Transport.new(clock, 4, 24)

transport.sequencer.every(1) do
  ticks += 1
  some_condition = false if ticks >= 60
end
transport.start

ticks  # => 60

# Sixty and not fifty: the two clauses are OR'd, so the condition holds
# while EITHER is true. `ticks < 50` stops mattering at 50 and the flag
# carries it to 60. Reading it as "a minimum of 50" is reading the first
# clause and ignoring the second.

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ticks = nil, do_log: nil) { ... } ⇒ DummyClock

Note:

Only one of ticks or block should be provided

Creates a new dummy clock with tick limit or condition.

Parameters:

  • ticks (Integer, nil) (defaults to: nil)

    number of ticks to generate (mutually exclusive with block)

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

    enable logging

Yields:

  • Condition block called each iteration; runs while truthy

Raises:

  • (ArgumentError)

    if both ticks and block are provided



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/musa-dsl/transport/dummy-clock.rb', line 100

def initialize(ticks = nil, do_log: nil, &block)
  do_log ||= false

  super()

  raise ArgumentError, 'Cannot initialize with ticks and block. You can only use one of the parameters.' if ticks && block

  @ticks = ticks
  @do_log = do_log
  @block = block
end

Instance Attribute Details

#blockProc?

Condition block for continuing (can be changed dynamically).

Returns:

  • (Proc, nil)

    the condition block



115
116
117
# File 'lib/musa-dsl/transport/dummy-clock.rb', line 115

def block
  @block
end

#ticksInteger?

Number of ticks remaining (can be changed dynamically).

Returns:

  • (Integer, nil)

    ticks remaining



120
121
122
# File 'lib/musa-dsl/transport/dummy-clock.rb', line 120

def ticks
  @ticks
end

Instance Method Details

#run { ... } ⇒ void

Note:

No real-time delays; runs as fast as possible

This method returns an undefined value.

Runs the clock loop, yielding for each tick.

Calls on_start callbacks, then yields while the condition is true. Uses Thread.pass instead of sleep for fast operation. Calls #stop when done (idempotent).

Yields:

  • Called once per tick



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/musa-dsl/transport/dummy-clock.rb', line 132

def run
  @on_start.each(&:call)
  @run = true
  @stopped = false

  while @run && eval_condition
    yield if block_given?

    Thread.pass  # Cooperate with other threads
  end

  stop  # Idempotent: if terminate already called stop, this is a no-op
end

#stopvoid

This method returns an undefined value.

Stops the clock and fires on_stop callbacks.



149
150
151
152
# File 'lib/musa-dsl/transport/dummy-clock.rb', line 149

def stop
  @run = false
  super
end

#terminatevoid

This method returns an undefined value.

Terminates the clock loop.

Calls #stop to ensure on_stop callbacks fire, then ensures the run loop exits.



160
161
162
# File 'lib/musa-dsl/transport/dummy-clock.rb', line 160

def terminate
  stop
end