musa-dsl 0.30.2 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -4,7 +4,81 @@ require_relative 'clock'
4
4
 
5
5
  module Musa
6
6
  module Clock
7
+ # Clock synchronized to external MIDI Clock messages.
8
+ #
9
+ # InputMidiClock receives MIDI Clock, Start, Stop, Continue, and Song Position
10
+ # messages from an external source (typically a DAW or hardware sequencer) and
11
+ # generates ticks synchronized to that source.
12
+ #
13
+ # ## Activation Model
14
+ #
15
+ # **IMPORTANT**: InputMidiClock requires external MIDI activation. After calling
16
+ # `transport.start` (which blocks), the clock waits for MIDI "Start" (0xFA)
17
+ # message from the external source to begin generating ticks.
18
+ #
19
+ # This activation model is appropriate for:
20
+ #
21
+ # - **DAW synchronization**: DAW controls start/stop via MIDI Clock
22
+ # - **Hardware sequencer sync**: External device controls timing
23
+ # - **Multi-device setups**: One master device controls all slaves
24
+ #
25
+ # ## MIDI Clock Protocol
26
+ #
27
+ # - **Clock (0xF8)**: Sent 24 times per quarter note (generates ticks when started)
28
+ # - **Start (0xFA)**: Begin playing from start (activates tick generation)
29
+ # - **Stop (0xFC)**: Stop playing (halts tick generation)
30
+ # - **Continue (0xFB)**: Resume from current position
31
+ # - **Song Position Pointer (0xF2)**: Jump to specific position
32
+ #
33
+ # ## Features
34
+ #
35
+ # - Automatic synchronization to external MIDI Clock
36
+ # - Position changes via Song Position Pointer
37
+ # - Start/Stop/Continue handling
38
+ # - Performance monitoring (time_table for tick processing times)
39
+ # - Graceful handling of missing input (waits until assigned)
40
+ #
41
+ # ## Special Sequences
42
+ #
43
+ # The clock handles the common sequence: Stop + Song Position + Continue
44
+ # as a position change while running, avoiding unnecessary stop/start cycles.
45
+ #
46
+ # ## Performance Monitoring
47
+ #
48
+ # The time_table tracks processing time per tick in milliseconds, useful
49
+ # for detecting performance issues.
50
+ #
51
+ # @example Basic setup with DAW synchronization
52
+ # input = MIDICommunications::Input.all.first
53
+ # clock = InputMidiClock.new(input, logger: logger)
54
+ # transport = Transport.new(clock)
55
+ #
56
+ # # Start transport (blocks waiting for MIDI Start message)
57
+ # transport.start # Waits until DAW sends MIDI Start (0xFA)
58
+ #
59
+ # @example Dynamic input assignment
60
+ # clock = InputMidiClock.new # No input yet
61
+ # transport = Transport.new(clock)
62
+ # transport.start # Waits for input to be assigned
63
+ #
64
+ # # Later:
65
+ # clock.input = MIDICommunications::Input.all.first
66
+ #
67
+ # @example Checking performance
68
+ # clock.time_table # => [0 => 1543, 1 => 234, 2 => 12, ...]
69
+ # # Shows histogram: X ms took Y ticks
70
+ #
71
+ # @see Transport Connects clock to sequencer
72
+ # @see MIDICommunications::Input MIDI input ports
73
+ # @see TimerClock For internal timing without MIDI
74
+ # @see DummyClock For automatic activation (testing/batch)
7
75
  class InputMidiClock < Clock
76
+ # Creates a new MIDI Clock synchronized clock.
77
+ #
78
+ # @param input [MIDICommunications::Input, nil] MIDI input port.
79
+ # Can be nil; clock will wait for assignment.
80
+ # @param logger [Logger, nil] logger for messages
81
+ # @param do_log [Boolean, nil] enable debug logging
8
82
  def initialize(input = nil, logger: nil, do_log: nil)
9
83
  do_log ||= false
10
84
 
@@ -25,25 +99,64 @@ module Musa
25
99
  @midi_parser = MIDIParser.new
26
100
  end
27
101
 
102
+ # Current MIDI input port.
103
+ #
104
+ # @return [MIDICommunications::Input, nil] the input port
28
105
  attr_reader :input
106
+
107
+ # Performance timing histogram.
108
+ #
109
+ # Maps processing time in milliseconds to tick count.
110
+ #
111
+ # @return [Array<Integer>] histogram indexed by milliseconds
112
+ #
113
+ # @example
114
+ # time_table[5] # => 123 (123 ticks took 5ms)
29
115
  attr_reader :time_table
30
116
 
117
+ # Assigns a MIDI input port.
118
+ #
119
+ # If the clock is waiting for input (sleeping), wakes it up.
120
+ #
121
+ # @param input_midi_port [MIDICommunications::Input] MIDI input port
122
+ # @return [MIDICommunications::Input] the assigned input
31
123
  def input=(input_midi_port)
32
124
  @input = input_midi_port
33
125
  @waiting_for_input&.wakeup
34
126
  end
35
127
 
128
+ # Runs the MIDI Clock processing loop.
129
+ #
130
+ # This method blocks and processes incoming MIDI messages, generating ticks
131
+ # in response to MIDI Clock messages. If no input is assigned, it waits
132
+ # until one is assigned via {#input=}.
133
+ #
134
+ # ## Message Handling
135
+ #
136
+ # - **Clock**: Yields (generates tick) if started
137
+ # - **Start**: Triggers on_start callbacks
138
+ # - **Stop**: Triggers on_stop callbacks
139
+ # - **Continue**: Resumes (typically after Stop)
140
+ # - **Song Position**: Triggers on_change_position
141
+ #
142
+ # @yield Called once per MIDI Clock message (24 ppqn)
143
+ # @return [void]
144
+ #
145
+ # @note This method blocks until {#terminate} is called
146
+ # @note Waits if no input assigned
36
147
  def run
37
148
  @run = true
38
149
 
39
150
  while @run
40
151
  if @input
152
+ # Read raw MIDI messages from input port
41
153
  raw_messages = @input.gets
42
154
  else
155
+ # No input assigned yet - wait for assignment
43
156
  @logger.warn('InputMidiClock') { 'Waiting for clock input MIDI port' }
44
157
 
45
158
  @waiting_for_input = Thread.current
46
- sleep
159
+ sleep # Wait until input= wakes us
47
160
  @waiting_for_input = nil
48
161
 
49
162
  if @input
@@ -53,6 +166,7 @@ module Musa
53
166
  end
54
167
  end
55
168
 
169
+ # Parse raw MIDI bytes into message objects
56
170
  messages = []
57
171
  stop_index = nil
58
172
 
@@ -104,12 +218,20 @@ module Musa
104
218
  end
105
219
  end
106
220
 
221
+ # Terminates the MIDI Clock processing loop.
222
+ #
223
+ # @return [void]
107
224
  def terminate
108
225
  @run = false
109
226
  end
110
227
 
111
228
  private
112
229
 
230
+ # Processes MIDI Start message.
231
+ #
232
+ # Calls registered on_start callbacks and marks as started.
233
+ #
234
+ # @api private
113
235
  def process_start
114
236
  @logger.debug('InputMidiClock') { 'processing Start...' }
115
237
 
@@ -119,6 +241,16 @@ module Musa
119
241
  @logger.debug('InputMidiClock') { 'processing Start... done' }
120
242
  end
121
243
 
244
+ # Processes individual MIDI Clock protocol messages.
245
+ #
246
+ # Handles Start, Stop, Continue, Clock, and Song Position Pointer messages.
247
+ # For Clock messages, yields and tracks processing time.
248
+ #
249
+ # @param m [MIDIEvents::Event] parsed MIDI message
250
+ # @yield Called for Clock messages if started
251
+ # @return [void]
252
+ #
253
+ # @api private
122
254
  def process_message(m)
123
255
  case m.name
124
256
  when 'Start'
@@ -2,7 +2,92 @@ require_relative 'clock'
2
2
 
3
3
  module Musa
4
4
  module Clock
5
+ # Internal timer-based clock for standalone operation.
6
+ #
7
+ # TimerClock uses a high-precision Timer to generate ticks at a configurable
8
+ # rate. Unlike DummyClock, TimerClock requires external activation and is
9
+ # designed for scenarios where timing control comes from outside (e.g., live
10
+ # coding clients, interactive systems).
11
+ #
12
+ # ## Activation Model
13
+ #
14
+ # **IMPORTANT**: TimerClock starts in a paused state. After calling
15
+ # `transport.start` (which blocks), you must call `clock.start()` from
16
+ # another thread to begin generating ticks.
17
+ #
18
+ # This activation model is appropriate for:
19
+ #
20
+ # - **Live coding**: Client controls when to start/stop
21
+ # - **Interactive systems**: External controller manages playback
22
+ # - **Testing with control**: Precise control over when ticks begin
23
+ #
24
+ # ## Configuration Methods
25
+ #
26
+ # The clock can be configured in three equivalent ways:
27
+ #
28
+ # 1. **BPM + ticks_per_beat**: Musical tempo-based (most common)
29
+ # 2. **Period**: Direct tick period in seconds
30
+ # 3. **Any combination**: Changes one parameter, others auto-calculate
31
+ #
32
+ # ## Relationship Between Parameters
33
+ #
34
+ # period = 60 / (bpm * ticks_per_beat)
35
+ #
36
+ # Example: 120 BPM, 24 ticks/beat → period = 60/(120*24) = 0.02083s
37
+ #
38
+ # ## States
39
+ #
40
+ # - **Not started**: Clock created but not running
41
+ # - **Started**: Clock running, generating ticks
42
+ # - **Paused**: Clock started but temporarily stopped
43
+ #
44
+ # @example Complete setup with external activation (live coding pattern)
45
+ # clock = TimerClock.new(bpm: 120, ticks_per_beat: 24)
46
+ # transport = Transport.new(clock, beats_per_bar: 4)
47
+ #
48
+ # # Schedule events
49
+ # transport.sequencer.at(4) { transport.stop }
50
+ #
51
+ # # Start transport in background (it blocks)
52
+ # thread = Thread.new { transport.start }
53
+ # sleep 0.1 # Let transport initialize
54
+ #
55
+ # # Activate clock from external control (e.g., live coding client)
56
+ # clock.start # Now ticks begin generating
57
+ #
58
+ # thread.join # Wait for completion
59
+ #
60
+ # @example With timing correction
61
+ # # Correction compensates for system-specific timing offsets
62
+ # clock = TimerClock.new(bpm: 140, correction: -0.001)
63
+ #
64
+ # @example Dynamic tempo changes
65
+ # clock = TimerClock.new(bpm: 120)
66
+ # # ... later, while running:
67
+ # clock.bpm = 140 # Tempo change takes effect immediately
68
+ #
69
+ # @see Timer Internal precision timer
70
+ # @see Transport Connects clock to sequencer
71
+ # @see DummyClock For automatic activation (testing/batch)
5
72
  class TimerClock < Clock
73
+ # Creates a new timer-based clock.
74
+ #
75
+ # At least one timing parameter must be provided (period, bpm, or ticks_per_beat).
76
+ # Missing parameters use defaults: bpm=120, ticks_per_beat=24.
77
+ #
78
+ # @param period [Numeric, nil] tick period in seconds (direct specification)
79
+ # @param ticks_per_beat [Numeric, nil] number of ticks per beat (default: 24)
80
+ # @param bpm [Numeric, nil] beats per minute (default: 120)
81
+ # @param correction [Numeric, nil] timing correction in seconds (for calibration)
82
+ # @param delayed_ticks_error [Numeric, nil] threshold for error-level logging
83
+ # @param logger [Logger, nil] logger for warnings/errors
84
+ # @param do_log [Boolean, nil] enable timing logs
85
+ #
86
+ # @example
87
+ # # All equivalent for 120 BPM, 24 ticks/beat:
88
+ # TimerClock.new(bpm: 120, ticks_per_beat: 24)
89
+ # TimerClock.new(bpm: 120) # ticks_per_beat defaults to 24
90
+ # TimerClock.new(period: 0.02083, ticks_per_beat: 24)
6
91
  def initialize(period = nil, ticks_per_beat: nil, bpm: nil, correction: nil, delayed_ticks_error: nil, logger: nil, do_log: nil)
7
92
  do_log ||= false
8
93
 
@@ -10,10 +95,12 @@ module Musa
10
95
 
11
96
  @correction = correction
12
97
 
98
+ # Set parameters in any combination
13
99
  self.period = period if period
14
100
  self.ticks_per_beat = ticks_per_beat if ticks_per_beat
15
101
  self.bpm = bpm if bpm
16
102
 
103
+ # Apply defaults
17
104
  self.bpm ||= 120
18
105
  self.ticks_per_beat ||= 24
19
106
 
@@ -25,34 +112,87 @@ module Musa
25
112
  @do_log = do_log
26
113
  end
27
114
 
28
- attr_reader :period, :ticks_per_beat, :bpm
115
+ # Current tick period in seconds.
116
+ #
117
+ # @return [Rational] seconds between ticks
118
+ attr_reader :period
29
119
 
120
+ # Number of ticks per beat.
121
+ #
122
+ # @return [Rational] ticks per beat (typically 24 or 96)
123
+ attr_reader :ticks_per_beat
124
+
125
+ # Current tempo in beats per minute.
126
+ #
127
+ # @return [Rational] BPM
128
+ attr_reader :bpm
129
+
130
+ # Sets the tick period in seconds and recalculates BPM.
131
+ #
132
+ # @param period_in_seconds [Numeric] new period in seconds
133
+ # @return [Rational] the rationalized period
134
+ #
135
+ # @note If clock is running, change takes effect immediately via @timer.period
30
136
  def period=(period_in_seconds)
31
137
  @period = period_in_seconds.rationalize
32
138
  @bpm = 60r / (@period * @ticks_per_beat) if @period && @ticks_per_beat
33
139
  @timer.period = @period if @timer
34
140
  end
35
141
 
142
+ # Sets ticks per beat and recalculates period.
143
+ #
144
+ # @param ticks [Numeric] new ticks per beat
145
+ # @return [Rational] the rationalized ticks_per_beat
146
+ #
147
+ # @note Common values: 24 (standard), 96 (high resolution)
148
+ # @note If clock is running, change takes effect immediately
36
149
  def ticks_per_beat=(ticks)
37
150
  @ticks_per_beat = ticks.rationalize
38
151
  @period = 60r / (@bpm * @ticks_per_beat) if @bpm && @ticks_per_beat
39
152
  @timer.period = @period if @timer && @period
40
153
  end
41
154
 
155
+ # Sets tempo in BPM and recalculates period.
156
+ #
157
+ # @param bpm [Numeric] new tempo in beats per minute
158
+ # @return [Rational] the rationalized BPM
159
+ #
160
+ # @note If clock is running, tempo change takes effect immediately
161
+ # @example Tempo automation
162
+ # clock.bpm = 120
163
+ # sleep 10
164
+ # clock.bpm = 140 # Speed up!
42
165
  def bpm=(bpm)
43
166
  @bpm = bpm.rationalize
44
167
  @period = 60r / (@bpm * @ticks_per_beat) if @bpm && @ticks_per_beat
45
168
  @timer.period = @period if @timer && @period
46
169
  end
47
170
 
171
+ # Checks if the clock has been started.
172
+ #
173
+ # @return [Boolean] true if started (even if currently paused)
48
174
  def started?
49
175
  @started
50
176
  end
51
177
 
178
+ # Checks if the clock is paused.
179
+ #
180
+ # @return [Boolean] true if paused
52
181
  def paused?
53
182
  @paused
54
183
  end
55
184
 
185
+ # Starts the clock's run loop.
186
+ #
187
+ # This method blocks and runs the timer loop, yielding for each tick.
188
+ # The clock starts in a paused state and must be explicitly started
189
+ # via {#start}.
190
+ #
191
+ # @yield Called once per tick
192
+ # @return [void]
193
+ #
194
+ # @note This method blocks until {#terminate} is called
195
+ # @note Clock begins paused; call {#start} to begin ticking
56
196
  def run
57
197
  @run = true
58
198
 
@@ -70,6 +210,15 @@ module Musa
70
210
  end
71
211
  end
72
212
 
213
+ # Starts the clock from paused state.
214
+ #
215
+ # Triggers @on_start callbacks and begins generating ticks.
216
+ # Has no effect if already started.
217
+ #
218
+ # @return [void]
219
+ #
220
+ # @note Must call {#run} first to start the run loop
221
+ # @note Calls registered on_start callbacks
73
222
  def start
74
223
  unless @started
75
224
  @on_start.each(&:call)
@@ -79,6 +228,15 @@ module Musa
79
228
  end
80
229
  end
81
230
 
231
+ # Stops the clock and resets to initial state.
232
+ #
233
+ # Triggers @on_stop callbacks and marks clock as not started.
234
+ # Has no effect if not currently started.
235
+ #
236
+ # @return [void]
237
+ #
238
+ # @note Calls registered on_stop callbacks
239
+ # @note Different from {#pause}: stop resets to initial state
82
240
  def stop
83
241
  if @started
84
242
  @timer.stop
@@ -88,6 +246,15 @@ module Musa
88
246
  end
89
247
  end
90
248
 
249
+ # Pauses the clock without stopping it.
250
+ #
251
+ # Ticks stop but clock remains in started state. Use {#continue}
252
+ # to resume.
253
+ #
254
+ # @return [void]
255
+ #
256
+ # @note No effect if not started or already paused
257
+ # @see #continue
91
258
  def pause
92
259
  if @started && !@paused
93
260
  @timer.stop
@@ -95,6 +262,14 @@ module Musa
95
262
  end
96
263
  end
97
264
 
265
+ # Resumes the clock from paused state.
266
+ #
267
+ # Continues generating ticks. Has no effect if not paused.
268
+ #
269
+ # @return [void]
270
+ #
271
+ # @note No effect if not started or not paused
272
+ # @see #pause
98
273
  def continue
99
274
  if @started && @paused
100
275
  @paused = false
@@ -102,6 +277,13 @@ module Musa
102
277
  end
103
278
  end
104
279
 
280
+ # Terminates the clock's run loop.
281
+ #
282
+ # Causes {#run} to exit. This is the clean shutdown mechanism.
283
+ #
284
+ # @return [void]
285
+ #
286
+ # @note After calling this, {#run} will exit
105
287
  def terminate
106
288
  @run = false
107
289
  end
@@ -1,8 +1,54 @@
1
1
  module Musa
2
2
  module Clock
3
+ # High-precision timer for generating regular ticks.
4
+ #
5
+ # Timer uses Ruby's monotonic clock (Process::CLOCK_MONOTONIC) for drift-free
6
+ # timing. It compensates for processing delays and reports when the system
7
+ # cannot keep up with the requested tick rate.
8
+ #
9
+ # ## Precision Features
10
+ #
11
+ # - **Monotonic clock**: Immune to system time changes
12
+ # - **Drift compensation**: Calculates exact next tick time
13
+ # - **Overload detection**: Reports delayed ticks when processing is slow
14
+ # - **Correction parameter**: Fine-tune timing for specific systems
15
+ #
16
+ # ## Usage Pattern
17
+ #
18
+ # Timer is typically used internally by TimerClock, not directly. It runs
19
+ # in a loop, yielding for each tick and managing precise sleep intervals.
20
+ #
21
+ # ## Timing Algorithm
22
+ #
23
+ # 1. Record next expected tick time
24
+ # 2. Yield (caller processes tick)
25
+ # 3. Add period to next_moment
26
+ # 4. Calculate sleep time = (next_moment + correction) - current_time
27
+ # 5. Sleep if positive, warn if negative (delayed)
28
+ #
29
+ # @example Internal use by TimerClock
30
+ # timer = Timer.new(0.02083, logger: logger) # ~48 ticks/second
31
+ # timer.run { sequencer.tick }
32
+ #
33
+ # @see TimerClock Uses Timer internally
3
34
  class Timer
35
+ # The period between ticks in seconds.
36
+ #
37
+ # @return [Rational] tick period in seconds
4
38
  attr_accessor :period
5
39
 
40
+ # Creates a new precision timer.
41
+ #
42
+ # @param tick_period_in_seconds [Numeric] time between ticks in seconds
43
+ # @param correction [Numeric, nil] timing correction offset in seconds (for calibration)
44
+ # @param stop [Boolean, nil] initial stopped state (true = paused)
45
+ # @param delayed_ticks_error [Numeric, nil] threshold for error-level logging (default: 1.0 tick)
46
+ # @param logger [Logger, nil] logger for timing warnings/errors
47
+ # @param do_log [Boolean, nil] enable logging
48
+ #
49
+ # @example 120 BPM, 24 ticks per beat
50
+ # period = 60.0 / (120 * 24) # 0.02083 seconds
51
+ # timer = Timer.new(period, logger: logger)
6
52
  def initialize(tick_period_in_seconds, correction: nil, stop: nil, delayed_ticks_error: nil, logger: nil, do_log: nil)
7
53
  @period = tick_period_in_seconds.rationalize
8
54
  @correction = (correction || 0r).rationalize
@@ -13,6 +59,21 @@ module Musa
13
59
  @do_log = do_log
14
60
  end
15
61
 
62
+ # Runs the timer loop, yielding for each tick.
63
+ #
64
+ # This method blocks and runs indefinitely until stopped. For each tick:
65
+ # 1. Yields to caller if not stopped
66
+ # 2. Calculates next tick time
67
+ # 3. Sleeps precisely until next tick
68
+ # 4. Logs warnings if timing cannot be maintained
69
+ #
70
+ # When stopped (@stop = true), the thread sleeps until {#continue} is called.
71
+ #
72
+ # @yield Called once per tick for processing
73
+ # @return [void]
74
+ #
75
+ # @note This method blocks the current thread
76
+ # @note Uses monotonic clock for drift-free timing
16
77
  def run
17
78
  @thread = Thread.current
18
79
 
@@ -20,11 +81,14 @@ module Musa
20
81
 
21
82
  loop do
22
83
  unless @stop
84
+ # Process the tick
23
85
  yield
24
86
 
87
+ # Calculate next tick moment (compensates for processing time)
25
88
  @next_moment += @period
26
89
  to_sleep = (@next_moment + @correction) - Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
90
 
91
+ # Log timing issues if enabled
28
92
  if @do_log && to_sleep.negative? & @logger
29
93
  tick_errors = -to_sleep / @period
30
94
  if tick_errors >= @delayed_ticks_error
@@ -34,17 +98,36 @@ module Musa
34
98
  end
35
99
  end
36
100
 
101
+ # Sleep precisely until next tick (if not already late)
37
102
  sleep to_sleep if to_sleep > 0.0
38
103
  end
39
104
 
105
+ # When stopped, sleep thread until continue is called
40
106
  sleep if @stop
41
107
  end
42
108
  end
43
109
 
110
+ # Pauses the timer without terminating the loop.
111
+ #
112
+ # The timer thread sleeps until {#continue} is called. Ticks are not
113
+ # generated while stopped.
114
+ #
115
+ # @return [void]
116
+ #
117
+ # @see #continue
44
118
  def stop
45
119
  @stop = true
46
120
  end
47
121
 
122
+ # Resumes the timer after being stopped.
123
+ #
124
+ # Resets the next tick moment to avoid a burst of catchup ticks,
125
+ # then wakes the timer thread.
126
+ #
127
+ # @return [void]
128
+ #
129
+ # @note Resets timing baseline to prevent tick accumulation
130
+ # @see #stop
48
131
  def continue
49
132
  @stop = false
50
133
  @next_moment = Process.clock_gettime(Process::CLOCK_MONOTONIC)