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
@@ -1,8 +1,185 @@
1
1
  module Musa
2
+ # Transcription framework for converting GDV musical events to output formats.
3
+ #
4
+ # Provides infrastructure for transcribing GDV (Grade-Duration-Velocity) events
5
+ # into various output formats (MIDI, MusicXML) through a pipeline of feature
6
+ # processors. The transcription system handles musical ornaments, articulations,
7
+ # and notation-specific transformations.
8
+ #
9
+ # ## Architecture Overview
10
+ #
11
+ # ### Core Components
12
+ #
13
+ # 1. **Transcriptor** - Main orchestrator that chains feature processors
14
+ # 2. **FeatureTranscriptor** - Base class for individual feature processors
15
+ # 3. **Transcriptor Sets** - Pre-configured processor chains for specific formats
16
+ #
17
+ # ### Processing Pipeline
18
+ #
19
+ # ```ruby
20
+ # GDV Event → [Transcriptor 1] → [Transcriptor 2] → ... → Output Format
21
+ # ```
22
+ #
23
+ # Each transcriptor in the chain:
24
+ #
25
+ # - Extracts specific features (appogiatura, trill, staccato, etc.)
26
+ # - Transforms/expands the event based on those features
27
+ # - Passes result to next transcriptor in chain
28
+ #
29
+ # ## GDV Format
30
+ #
31
+ # GDV events are hashes representing musical notes/events:
32
+ # ```ruby
33
+ # {
34
+ # grade: 0, # Scale degree (pitch)
35
+ # duration: 1r, # Rational duration
36
+ # velocity: 0.8, # Note velocity (0.0-1.0)
37
+ # # Plus optional ornament/articulation attributes:
38
+ # tr: true, # Trill
39
+ # mor: :up, # Mordent
40
+ # st: 2, # Staccato
41
+ # appogiatura: {...} # Grace note
42
+ # }
43
+ # ```
44
+ #
45
+ # ## Output Formats
46
+ #
47
+ # - **MIDI** (`FromGDV::ToMIDI`): Expands ornaments to note sequences for playback
48
+ # - **MusicXML** (`FromGDV::ToMusicXML`): Preserves ornaments as notation symbols
49
+ #
50
+ # ## Usage
51
+ #
52
+ # ```ruby
53
+ # # MIDI transcription (expands ornaments)
54
+ # transcriptor = Musa::Transcription::Transcriptor.new(
55
+ # Musa::Transcriptors::FromGDV::ToMIDI.transcription_set(duration_factor: 1/4r),
56
+ # base_duration: 1/4r,
57
+ # tick_duration: 1/96r
58
+ # )
59
+ # midi_events = transcriptor.transcript(gdv_event)
60
+ #
61
+ # # MusicXML transcription (preserves ornaments as symbols)
62
+ # transcriptor = Musa::Transcription::Transcriptor.new(
63
+ # Musa::Transcriptors::FromGDV::ToMusicXML.transcription_set,
64
+ # base_duration: 1/4r
65
+ # )
66
+ # musicxml_events = transcriptor.transcript(gdv_event)
67
+ # ```
68
+ #
69
+ # ## Supported Features
70
+ #
71
+ # ### Ornaments
72
+ #
73
+ # - **Appogiatura**: Grace notes
74
+ # - **Mordent**: Quick alternation with adjacent note
75
+ # - **Turn**: Four-note circling figure
76
+ # - **Trill**: Rapid alternation with upper neighbor
77
+ #
78
+ # ### Articulations
79
+ #
80
+ # - **Staccato**: Shortened note duration
81
+ # - **Base/Rest**: Zero-duration structural markers
82
+ #
83
+ # ## Creating Custom Transcriptors
84
+ #
85
+ # Extend `FeatureTranscriptor` and implement `transcript` method:
86
+ # ```ruby
87
+ # class MyOrnament < Musa::Transcription::FeatureTranscriptor
88
+ # def transcript(gdv, base_duration:, tick_duration:)
89
+ # if ornament = gdv.delete(:my_ornament)
90
+ # # Process ornament, return modified event(s)
91
+ # [event1, event2, ...]
92
+ # else
93
+ # super # Pass through unchanged
94
+ # end
95
+ # end
96
+ # end
97
+ # ```
98
+ #
99
+ # ## Integration
100
+ #
101
+ # The transcription system integrates with:
102
+ #
103
+ # - **Sequencer**: Converting generative patterns to playable events
104
+ # - **MIDI**: Real-time MIDI output with ornament expansion
105
+ # - **MusicXML**: Score generation with notation symbols
106
+ # - **Datasets**: Using AbsD (absolute duration) extensions
107
+ #
108
+ # @example Complete transcription workflow
109
+ # # 1. Generate GDV events
110
+ # gdv_events = [
111
+ # { grade: 0, duration: 1r, tr: true },
112
+ # { grade: 2, duration: 1r, mor: :up },
113
+ # { grade: 4, duration: 1/2r, st: true }
114
+ # ]
115
+ #
116
+ # # 2. Create MIDI transcriptor
117
+ # transcriptor = Musa::Transcription::Transcriptor.new(
118
+ # Musa::Transcriptors::FromGDV::ToMIDI.transcription_set,
119
+ # base_duration: 1/4r
120
+ # )
121
+ #
122
+ # # 3. Transcribe to MIDI events
123
+ # midi_events = gdv_events.collect { |gdv| transcriptor.transcript(gdv) }.flatten
124
+ #
125
+ # # 4. Send to MIDI output
126
+ # midi_events.each { |event| midi_output.send_event(event) }
127
+ #
128
+ # @see Musa::Transcriptors::FromGDV::ToMIDI
129
+ # @see Musa::Transcriptors::FromGDV::ToMusicXML
130
+ # @see Musa::Sequencer
131
+ #
132
+ # @api public
2
133
  module Transcription
134
+ # Main transcription orchestrator.
135
+ #
136
+ # Chains multiple feature transcriptors to process GDV events through a
137
+ # transformation pipeline. Each transcriptor in the chain processes specific
138
+ # musical features (ornaments, articulations, etc.).
139
+ #
140
+ # ## Processing
141
+ #
142
+ # The transcriptor applies each feature processor in sequence:
143
+ # 1. First transcriptor processes event
144
+ # 2. Result passed to second transcriptor
145
+ # 3. Continue through chain
146
+ # 4. Final result returned
147
+ #
148
+ # ## Array Handling
149
+ #
150
+ # If a transcriptor returns an array (e.g., expanding one note to many),
151
+ # subsequent transcriptors process each element and results are flattened.
152
+ #
153
+ # @example Create transcriptor chain
154
+ # transcriptor = Musa::Transcription::Transcriptor.new(
155
+ # [Appogiatura.new, Trill.new, Staccato.new],
156
+ # base_duration: 1/4r,
157
+ # tick_duration: 1/96r
158
+ # )
159
+ #
160
+ # @api public
3
161
  class Transcriptor
162
+ # Returns the transcriptor chain.
163
+ #
164
+ # @return [Array<FeatureTranscriptor>] array of feature processors
165
+ #
166
+ # @api public
4
167
  attr_reader :transcriptors
5
168
 
169
+ # Creates transcriptor with specified feature processors.
170
+ #
171
+ # @param transcriptors [Array<FeatureTranscriptor>] chain of feature processors
172
+ # @param base_duration [Rational] base duration unit (e.g., quarter note = 1/4)
173
+ # @param tick_duration [Rational] minimum tick duration (e.g., 1/96 for MIDI)
174
+ #
175
+ # @example Create MIDI transcriptor
176
+ # transcriptor = Musa::Transcription::Transcriptor.new(
177
+ # Musa::Transcriptors::FromGDV::ToMIDI.transcription_set,
178
+ # base_duration: 1/4r,
179
+ # tick_duration: 1/96r
180
+ # )
181
+ #
182
+ # @api public
6
183
  def initialize(transcriptors = nil, base_duration: nil, tick_duration: nil)
7
184
  @transcriptors = transcriptors || []
8
185
 
@@ -10,6 +187,28 @@ module Musa
10
187
  @tick_duration = tick_duration || 1/96r
11
188
  end
12
189
 
190
+ # Transcribes GDV event(s) through the processor chain.
191
+ #
192
+ # Applies each transcriptor in sequence. Handles both single events and
193
+ # arrays of events, flattening results when transcriptors expand events.
194
+ #
195
+ # @param element [Hash, Array<Hash>] GDV event or array of events
196
+ #
197
+ # @return [Hash, Array<Hash>, nil] transcribed event(s)
198
+ #
199
+ # @example Transcribe single event
200
+ # gdv = { grade: 0, duration: 1r, tr: true }
201
+ # result = transcriptor.transcript(gdv)
202
+ # # => [{ grade: 1, duration: 1/16r }, { grade: 0, duration: 1/16r }, ...]
203
+ #
204
+ # @example Transcribe array of events
205
+ # gdvs = [
206
+ # { grade: 0, duration: 1r, mor: true },
207
+ # { grade: 2, duration: 1r }
208
+ # ]
209
+ # results = transcriptor.transcript(gdvs)
210
+ #
211
+ # @api public
13
212
  def transcript(element)
14
213
  @transcriptors.each do |transcriptor|
15
214
  if element
@@ -25,7 +224,49 @@ module Musa
25
224
  end
26
225
  end
27
226
 
227
+ # Base class for feature transcriptors.
228
+ #
229
+ # Provides common functionality for processing specific musical features
230
+ # in GDV events. Subclasses implement `transcript` method to handle
231
+ # their specific feature (ornament, articulation, etc.).
232
+ #
233
+ # ## Contract
234
+ #
235
+ # Transcriptor implementations should:
236
+ #
237
+ # 1. Extract their specific feature from GDV hash
238
+ # 2. Process/transform the event based on that feature
239
+ # 3. Return modified event(s) or call `super` if feature not present
240
+ # 4. Use `delete` to remove processed feature attributes
241
+ #
242
+ # ## Helper Methods
243
+ #
244
+ # - `check(value, &block)`: Safely iterate over value or array
245
+ #
246
+ # @example Implement custom transcriptor
247
+ # class Accent < FeatureTranscriptor
248
+ # def transcript(gdv, base_duration:, tick_duration:)
249
+ # if accent = gdv.delete(:accent)
250
+ # gdv[:velocity] *= 1.2 # Increase velocity
251
+ # end
252
+ # super # Clean up and pass through
253
+ # end
254
+ # end
255
+ #
256
+ # @api public
28
257
  class FeatureTranscriptor
258
+ # Transcribes GDV event for this feature.
259
+ #
260
+ # Base implementation cleans up empty `:modifiers` attribute. Subclasses
261
+ # should override to process their specific feature, then call `super`.
262
+ #
263
+ # @param element [Hash, Array<Hash>] GDV event or array of events
264
+ # @param base_duration [Rational] base duration unit
265
+ # @param tick_duration [Rational] minimum tick duration
266
+ #
267
+ # @return [Hash, Array<Hash>] transcribed event(s)
268
+ #
269
+ # @api public
29
270
  def transcript(element, base_duration:, tick_duration:)
30
271
  case element
31
272
  when Hash
@@ -37,6 +278,23 @@ module Musa
37
278
  element
38
279
  end
39
280
 
281
+ # Helper to safely process value or array.
282
+ #
283
+ # Yields each element if array, or yields single value.
284
+ # Useful for processing feature values that may be single or multiple.
285
+ #
286
+ # @param value_or_array [Object, Array] value to check
287
+ # @yield [value] block to call for each value
288
+ #
289
+ # @example Check ornament options
290
+ # check(ornament_value) do |option|
291
+ # case option
292
+ # when :up then direction = :up
293
+ # when :down then direction = :down
294
+ # end
295
+ # end
296
+ #
297
+ # @api public
40
298
  def check(value_or_array, &block)
41
299
  if block_given?
42
300
  if value_or_array.is_a?(Array)
@@ -49,5 +307,12 @@ module Musa
49
307
  end
50
308
  end
51
309
 
310
+ # Namespace for transcriptor implementations.
311
+ #
312
+ # Contains modules for different transcription targets:
313
+ # - `FromGDV::ToMIDI` - MIDI playback transcriptors
314
+ # - `FromGDV::ToMusicXML` - MusicXML notation transcriptors
315
+ #
316
+ # @api public
52
317
  module Transcriptors; end
53
318
  end
@@ -1,6 +1,73 @@
1
1
  module Musa
2
+ # Clock and timing infrastructure for musical transport.
3
+ #
4
+ # The Clock module provides the foundation for all timing mechanisms in Musa DSL.
5
+ # Clocks generate regular ticks that drive the sequencer forward, and can be
6
+ # sourced from internal timers, external MIDI clock, or manual control.
7
+ #
8
+ # ## Architecture
9
+ #
10
+ # - **Clock (base class)**: Abstract interface for all clock implementations
11
+ # - **TimerClock**: Internal high-precision timer-based clock
12
+ # - **InputMidiClock**: Synchronized to external MIDI Clock messages
13
+ # - **ExternalTickClock**: Manually triggered ticks (for testing/integration)
14
+ # - **DummyClock**: Simplified clock for testing
15
+ #
16
+ # ## Clock Lifecycle
17
+ #
18
+ # 1. **Creation**: Clock instance created with configuration
19
+ # 2. **Registration**: Callbacks registered (on_start, on_stop, on_change_position)
20
+ # 3. **Running**: Clock.run called (blocks, generates ticks via yield)
21
+ # 4. **Termination**: Clock.terminate called to stop
22
+ #
23
+ # @see Transport Connects clocks to sequencers
24
+ # @see Sequencer Receives ticks from clocks
2
25
  module Clock
26
+ # Abstract base class for all clock implementations.
27
+ #
28
+ # This class defines the interface and callback infrastructure that all
29
+ # concrete clock implementations must follow. Subclasses must implement
30
+ # the `run` and `terminate` methods.
31
+ #
32
+ # ## Callback System
33
+ #
34
+ # Clocks maintain three callback collections:
35
+ #
36
+ # - **on_start**: Called when clock starts running
37
+ # - **on_stop**: Called when clock stops
38
+ # - **on_change_position**: Called when position changes (seek/jump)
39
+ #
40
+ # ## Subclass Responsibilities
41
+ #
42
+ # Concrete clocks must:
43
+ #
44
+ # 1. Implement `run(&block)` - Start generating ticks, yield for each tick
45
+ # 2. Implement `terminate` - Stop the clock
46
+ # 3. Call registered callbacks at appropriate times
47
+ # 4. Manage @run state properly
48
+ #
49
+ # @example Creating a simple clock subclass
50
+ # class SimpleClock < Clock
51
+ # def run
52
+ # @run = true
53
+ # @on_start.each(&:call)
54
+ #
55
+ # while @run
56
+ # yield if block_given? # Generate tick
57
+ # sleep 0.1
58
+ # end
59
+ #
60
+ # @on_stop.each(&:call)
61
+ # end
62
+ #
63
+ # def terminate
64
+ # @run = false
65
+ # end
66
+ # end
67
+ #
68
+ # @abstract Subclass and implement {#run} and {#terminate}
3
69
  class Clock
70
+ # Initializes the clock with empty callback collections.
4
71
  def initialize
5
72
  @run = nil
6
73
  @on_start = []
@@ -8,26 +75,84 @@ module Musa
8
75
  @on_change_position = []
9
76
  end
10
77
 
78
+ # Checks if the clock is currently running.
79
+ #
80
+ # @return [Boolean] true if clock is running, false otherwise.
11
81
  def running?
12
82
  @run
13
83
  end
14
84
 
85
+ # Registers a callback to be called when the clock starts.
86
+ #
87
+ # Multiple callbacks can be registered and will be called in order.
88
+ #
89
+ # @yield Called when clock starts running.
90
+ # @return [void]
91
+ #
92
+ # @example
93
+ # clock.on_start { puts "Clock started!" }
15
94
  def on_start(&block)
16
95
  @on_start << block
17
96
  end
18
97
 
98
+ # Registers a callback to be called when the clock stops.
99
+ #
100
+ # Multiple callbacks can be registered and will be called in order.
101
+ #
102
+ # @yield Called when clock stops running.
103
+ # @return [void]
104
+ #
105
+ # @example
106
+ # clock.on_stop { puts "Clock stopped!" }
19
107
  def on_stop(&block)
20
108
  @on_stop << block
21
109
  end
22
110
 
111
+ # Registers a callback to be called when playback position changes.
112
+ #
113
+ # This is typically used for handling seek/jump operations where the
114
+ # transport position changes non-linearly.
115
+ #
116
+ # @yield [bars, beats, midi_beats] Position change information
117
+ # @yieldparam bars [Rational, nil] new position in bars
118
+ # @yieldparam beats [Rational, nil] new position in beats
119
+ # @yieldparam midi_beats [Integer, nil] new position in MIDI beats (for MIDI Clock)
120
+ # @return [void]
121
+ #
122
+ # @example
123
+ # clock.on_change_position do |bars:, beats:, midi_beats:|
124
+ # puts "Position changed to bar #{bars}"
125
+ # end
23
126
  def on_change_position(&block)
24
127
  @on_change_position << block
25
128
  end
26
129
 
130
+ # Starts the clock running and generates ticks.
131
+ #
132
+ # This method should block and yield once per tick. Subclasses must
133
+ # implement this method.
134
+ #
135
+ # @yield Called once per tick to advance the sequencer.
136
+ # @return [void]
137
+ #
138
+ # @raise [NotImplementedError] if not overridden by subclass.
139
+ #
140
+ # @note This method typically runs in a loop until {#terminate} is called.
141
+ # @note Subclasses should call @on_start callbacks when starting.
142
+ # @note Subclasses should call @on_stop callbacks when stopping.
27
143
  def run
28
144
  raise NotImplementedError
29
145
  end
30
146
 
147
+ # Stops the clock and terminates the run loop.
148
+ #
149
+ # Subclasses must implement this method to cleanly stop the clock.
150
+ #
151
+ # @return [void]
152
+ #
153
+ # @raise [NotImplementedError] if not overridden by subclass.
154
+ #
155
+ # @note After calling this, {#run} should exit.
31
156
  def terminate
32
157
  raise NotImplementedError
33
158
  end
@@ -2,7 +2,67 @@ require_relative 'clock'
2
2
 
3
3
  module Musa
4
4
  module Clock
5
+ # Simple clock for testing with fixed tick count or custom condition.
6
+ #
7
+ # DummyClock is designed for testing and batch processing where automatic
8
+ # execution without external dependencies is desired.
9
+ #
10
+ # ## Activation Model
11
+ #
12
+ # **IMPORTANT**: Unlike TimerClock, InputMidiClock, and ExternalTickClock,
13
+ # DummyClock **activates automatically** when `transport.start` is called.
14
+ # It immediately begins generating ticks without waiting for external signals.
15
+ #
16
+ # This activation model is appropriate for:
17
+ #
18
+ # - **Unit testing**: No external dependencies, deterministic execution
19
+ # - **Batch processing**: Generate music as fast as possible
20
+ # - **Fast-forward simulations**: Skip real-time delays
21
+ # - **Deterministic debugging**: Predictable tick counts
22
+ #
23
+ # ## Modes of Operation
24
+ #
25
+ # 1. **Fixed tick count**: Runs for exactly N ticks then stops
26
+ # 2. **Custom condition**: Runs while a block returns true
27
+ #
28
+ # ## Differences from Other Clocks
29
+ #
30
+ # DummyClock is the only clock that starts generating ticks immediately
31
+ # upon `transport.start`. It uses Thread.pass instead of sleep, making
32
+ # execution as fast as possible without real-time constraints.
33
+ #
34
+ # @example Fixed tick count (automatic activation)
35
+ # clock = DummyClock.new(100) # Exactly 100 ticks
36
+ # transport = Transport.new(clock)
37
+ # transport.start # Immediately runs 100 ticks, then stops
38
+ #
39
+ # @example Custom condition (automatic activation)
40
+ # continue = true
41
+ # clock = DummyClock.new { continue }
42
+ # transport = Transport.new(clock)
43
+ #
44
+ # transport.sequencer.at(10) { continue = false }
45
+ # transport.start # Immediately begins, stops at tick 10
46
+ #
47
+ # @example Testing specific sequences
48
+ # ticks = 0
49
+ # clock = DummyClock.new { ticks < 50 || some_condition }
50
+ # transport.sequencer.every(1) { ticks += 1 }
51
+ # transport.start # Immediately runs minimum 50 ticks
52
+ #
53
+ # @see TimerClock For real-time operation with external activation
54
+ # @see InputMidiClock For MIDI-synchronized operation
55
+ # @see ExternalTickClock For manual tick control
5
56
  class DummyClock < Clock
57
+ # Creates a new dummy clock with tick limit or condition.
58
+ #
59
+ # @param ticks [Integer, nil] number of ticks to generate (mutually exclusive with block)
60
+ # @param do_log [Boolean, nil] enable logging
61
+ # @yield Condition block called each iteration; runs while truthy
62
+ #
63
+ # @raise [ArgumentError] if both ticks and block are provided
64
+ #
65
+ # @note Only one of ticks or block should be provided
6
66
  def initialize(ticks = nil, do_log: nil, &block)
7
67
  do_log ||= false
8
68
 
@@ -15,8 +75,26 @@ module Musa
15
75
  @block = block
16
76
  end
17
77
 
18
- attr_accessor :block, :ticks
78
+ # Condition block for continuing (can be changed dynamically).
79
+ #
80
+ # @return [Proc, nil] the condition block
81
+ attr_accessor :block
19
82
 
83
+ # Number of ticks remaining (can be changed dynamically).
84
+ #
85
+ # @return [Integer, nil] ticks remaining
86
+ attr_accessor :ticks
87
+
88
+ # Runs the clock loop, yielding for each tick.
89
+ #
90
+ # Calls on_start callbacks, then yields while the condition is true.
91
+ # Uses Thread.pass instead of sleep for fast operation.
92
+ # Calls on_stop callbacks when done.
93
+ #
94
+ # @yield Called once per tick
95
+ # @return [void]
96
+ #
97
+ # @note No real-time delays; runs as fast as possible
20
98
  def run
21
99
  @on_start.each(&:call)
22
100
  @run = true
@@ -24,23 +102,32 @@ module Musa
24
102
  while @run && eval_condition
25
103
  yield if block_given?
26
104
 
27
- Thread.pass
105
+ Thread.pass # Cooperate with other threads
28
106
  end
29
107
 
30
108
  @on_stop.each(&:call)
31
109
  end
32
110
 
111
+ # Terminates the clock loop.
112
+ #
113
+ # @return [void]
33
114
  def terminate
34
115
  @run = false
35
116
  end
36
117
 
37
118
  private
38
119
 
120
+ # Evaluates continuation condition based on mode.
121
+ #
122
+ # @return [Boolean] true to continue, false to stop
123
+ # @api private
39
124
  def eval_condition
40
125
  if @ticks
126
+ # Tick count mode: decrement and check
41
127
  @ticks -= 1
42
128
  @ticks.positive?
43
129
  else
130
+ # Block condition mode
44
131
  @block.call
45
132
  end
46
133
  end
@@ -2,7 +2,76 @@ require_relative 'clock'
2
2
 
3
3
  module Musa
4
4
  module Clock
5
+ # Clock driven by external tick() calls for integration and testing.
6
+ #
7
+ # ExternalTickClock doesn't generate its own ticks. Instead, ticks are
8
+ # triggered manually by calling the {#tick} method.
9
+ #
10
+ # ## Activation Model
11
+ #
12
+ # **IMPORTANT**: ExternalTickClock requires manual tick generation. After calling
13
+ # `transport.start` (which returns immediately, doesn't block), you must call
14
+ # `clock.tick()` repeatedly from your external system to generate ticks.
15
+ #
16
+ # This activation model is appropriate for:
17
+ #
18
+ # - **Testing**: Precise control over timing for step-by-step debugging
19
+ # - **Game engine integration**: Game loop controls tick timing
20
+ # - **Frame-based systems**: One tick per frame or custom logic
21
+ # - **Offline rendering**: Generate ticks as fast as needed
22
+ #
23
+ # ## Operation
24
+ #
25
+ # 1. Call {#run} to initialize (doesn't block, returns immediately)
26
+ # 2. Call {#tick} repeatedly to generate ticks manually
27
+ # 3. Call {#terminate} when done
28
+ #
29
+ # ## Differences from Other Clocks
30
+ #
31
+ # Unlike TimerClock and InputMidiClock, ExternalTickClock's `run` method
32
+ # **does not block** - it returns immediately. This allows your external
33
+ # system to control the timing loop.
34
+ #
35
+ # @example Manual stepping for testing
36
+ # clock = ExternalTickClock.new
37
+ # transport = Transport.new(clock)
38
+ #
39
+ # # Schedule some events
40
+ # transport.sequencer.at 1 { puts "Tick 1" }
41
+ # transport.sequencer.at 2 { puts "Tick 2" }
42
+ #
43
+ # # Start in background (non-blocking for ExternalTickClock)
44
+ # thread = Thread.new { transport.start }
45
+ # sleep 0.1 # Let transport initialize
46
+ #
47
+ # # Generate ticks manually
48
+ # clock.tick # => (nothing, position 0)
49
+ # clock.tick # => "Tick 1"
50
+ # clock.tick # => "Tick 2"
51
+ #
52
+ # transport.stop
53
+ # thread.join
54
+ #
55
+ # @example Integration with game loop
56
+ # clock = ExternalTickClock.new
57
+ # transport = Transport.new(clock)
58
+ # thread = Thread.new { transport.start }
59
+ # sleep 0.1
60
+ #
61
+ # # In game update loop:
62
+ # def update(delta_time)
63
+ # if should_tick?(delta_time)
64
+ # clock.tick # Advance sequencer by one tick
65
+ # end
66
+ # end
67
+ #
68
+ # @see DummyClock For automatic testing with fixed tick counts
69
+ # @see TimerClock For internal timer-based timing
70
+ # @see InputMidiClock For MIDI-synchronized timing
5
71
  class ExternalTickClock < Clock
72
+ # Creates a new externally-controlled clock.
73
+ #
74
+ # @param do_log [Boolean, nil] enable logging
6
75
  def initialize(do_log: nil)
7
76
  do_log ||= false
8
77
 
@@ -11,18 +80,40 @@ module Musa
11
80
  @do_log = do_log
12
81
  end
13
82
 
83
+ # Initializes the clock (non-blocking).
84
+ #
85
+ # Unlike other clocks, this method doesn't block. It stores the block
86
+ # and calls on_start callbacks, then returns immediately. Ticks are
87
+ # generated by calling {#tick}.
88
+ #
89
+ # @yield Called for each tick triggered by {#tick}
90
+ # @return [void]
91
+ #
92
+ # @note This method does NOT block
14
93
  def run(&block)
15
94
  @on_start.each(&:call)
16
95
  @run = true
17
96
  @block = block
18
97
  end
19
98
 
99
+ # Generates one tick manually.
100
+ #
101
+ # If the clock is running, calls the registered block (typically
102
+ # sequencer.tick). Has no effect if clock is not running.
103
+ #
104
+ # @return [void]
105
+ #
106
+ # @note Only works if {#run} has been called
107
+ # @note Thread-safe for integration with external event loops
20
108
  def tick
21
109
  if @run
22
110
  @block.call if @block
23
111
  end
24
112
  end
25
113
 
114
+ # Terminates the clock and calls on_stop callbacks.
115
+ #
116
+ # @return [void]
26
117
  def terminate
27
118
  @on_stop.each(&:call)
28
119
  @run = false