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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- 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
|
-
|
|
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
|