Class: Musa::Clock::InputMidiClock

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

Overview

Clock synchronized to external MIDI Clock messages.

InputMidiClock receives MIDI Clock, Start, Stop, Continue, and Song Position messages from an external source (typically a DAW or hardware sequencer) and generates ticks synchronized to that source.

Activation Model

IMPORTANT: InputMidiClock requires external MIDI activation. After calling transport.start (which blocks), the clock waits for MIDI "Start" (0xFA) message from the external source to begin generating ticks.

This activation model is appropriate for:

  • DAW synchronization: DAW controls start/stop via MIDI Clock
  • Hardware sequencer sync: External device controls timing
  • Multi-device setups: One master device controls all slaves

MIDI Clock Protocol

  • Clock (0xF8): Sent 24 times per quarter note (generates ticks when started)
  • Start (0xFA): Begin playing from start (activates tick generation)
  • Stop (0xFC): Stop playing (halts tick generation)
  • Continue (0xFB): Resume from current position
  • Song Position Pointer (0xF2): Jump to specific position

Features

  • Automatic synchronization to external MIDI Clock
  • Position changes via Song Position Pointer
  • Start/Stop/Continue handling
  • Performance monitoring (time_table for tick processing times)
  • Graceful handling of missing input (waits until assigned)

Special Sequences

The clock handles the common sequence: Stop + Song Position + Continue as a position change while running, avoiding unnecessary stop/start cycles.

Performance Monitoring

The time_table tracks processing time per tick in milliseconds, useful for detecting performance issues.

Examples:

Basic setup with DAW synchronization

input = MIDICommunications::Input.all.first
clock = InputMidiClock.new(input, logger: logger)
transport = Transport.new(clock)

# Start transport (blocks waiting for MIDI Start message)
transport.start  # Waits until DAW sends MIDI Start (0xFA)

Dynamic input assignment

clock = InputMidiClock.new  # No input yet
transport = Transport.new(clock)
transport.start  # Waits for input to be assigned

# Later:
clock.input = MIDICommunications::Input.all.first

Checking performance

clock.time_table  # => [0 => 1543, 1 => 234, 2 => 12, ...]
# Shows histogram: X ms took Y ticks

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input = nil, logger: nil, do_log: nil) ⇒ InputMidiClock

Creates a new MIDI Clock synchronized clock.

Parameters:

  • input (MIDICommunications::Input, nil) (defaults to: nil)

    MIDI input port. Can be nil; clock will wait for assignment.

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

    logger for messages

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

    enable debug logging



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/musa-dsl/transport/input-midi-clock.rb', line 82

def initialize(input = nil, logger: nil, do_log: nil)
  do_log ||= false

  super()

  @logger = logger

  self.input = input

  if logger
    @logger = logger
  else
    @logger = Musa::Logger::Logger.new
    @logger.debug! if do_log
  end

  @time_table = []
  @midi_parser = MIDIParser.new
end

Instance Attribute Details

#inputMIDICommunications::Input?

Current MIDI input port.

Returns:

  • (MIDICommunications::Input, nil)

    the input port



105
106
107
# File 'lib/musa-dsl/transport/input-midi-clock.rb', line 105

def input
  @input
end

#time_tableArray<Integer> (readonly)

Performance timing histogram.

Maps processing time in milliseconds to tick count.

Examples:

time_table[5]  # => 123 (123 ticks took 5ms)

Returns:

  • (Array<Integer>)

    histogram indexed by milliseconds



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

def time_table
  @time_table
end

Instance Method Details

#run { ... } ⇒ void

Note:

This method blocks until #terminate is called

Note:

Waits if no input assigned

This method returns an undefined value.

Runs the MIDI Clock processing loop.

This method blocks and processes incoming MIDI messages, generating ticks in response to MIDI Clock messages. If no input is assigned, it waits until one is assigned via #input=.

Message Handling

  • Clock: Yields (generates tick) if started
  • Start: Triggers on_start callbacks
  • Stop: Triggers on_stop callbacks
  • Continue: Resumes (typically after Stop)
  • Song Position: Triggers on_change_position

Yields:

  • Called once per MIDI Clock message (24 ppqn)



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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/musa-dsl/transport/input-midi-clock.rb', line 147

def run
  @run = true

  while @run
    if @input
      # Read raw MIDI messages from input port
      raw_messages = @input.gets
    else
      # No input assigned yet - wait for assignment
      @logger.warn('InputMidiClock') { 'Waiting for clock input MIDI port' }

      @waiting_for_input = Thread.current
      sleep  # Wait until input= wakes us
      @waiting_for_input = nil

      if @input
        @logger.info('InputMidiClock') { "Assigned clock input MIDI port '#{@input.name}'" }
      else
        @logger.warn('InputMidiClock') { 'Clock input MIDI port not found' }
      end
    end

    # Parse raw MIDI bytes into message objects
    messages = []
    stop_index = nil

    raw_messages&.each do |message|
      mm = @midi_parser.parse message[:data]

      if mm
        if mm.is_a? Array
          mm.each do |m|
            stop_index = messages.size if m.name == 'Stop' && !stop_index
            messages << m
          end
        else
          stop_index = messages.size if mm.name == 'Stop' && !stop_index
          messages << mm
        end
      end
    end

    size = messages.size
    index = 0
    while index < size
      if index == stop_index && size >= index + 3 &&
          messages[index + 1].name == 'Song Position Pointer' &&
          messages[index + 2].name == 'Continue'

        @logger.debug('InputMidiClock') { 'processing Stop + Song Position Pointer + Continue...' }

        process_start unless @started

        process_message messages[index + 1] do
          yield if block_given?
        end

        index += 2

        @logger.debug('InputMidiClock') { 'processing Stop + Song Position Pointer + Continue... done' }

      else
        process_message messages[index] do
          yield if block_given?
        end
      end

      index += 1
    end

    Thread.pass
  end
end

#terminatevoid

This method returns an undefined value.

Terminates the MIDI Clock processing loop.



224
225
226
# File 'lib/musa-dsl/transport/input-midi-clock.rb', line 224

def terminate
  @run = false
end