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,17 +1,162 @@
1
1
  module Musa
2
+ # MusicXML generation system.
3
+ #
4
+ # This module provides a comprehensive DSL for generating MusicXML 3.0 files
5
+ # programmatically. It uses a builder pattern with a flexible API that supports
6
+ # both constructor-based and DSL-style notation creation.
7
+ #
8
+ # ## Architecture
9
+ #
10
+ # The MusicXML builder system is organized hierarchically:
11
+ #
12
+ # ScorePartwise (root)
13
+ # ├── Metadata (work, movement, creators, rights)
14
+ # ├── PartGroup (grouping)
15
+ # └── Part
16
+ # └── Measure
17
+ # ├── Attributes (key, time, clef, divisions)
18
+ # ├── Direction (dynamics, tempo, expressions)
19
+ # ├── PitchedNote / Rest / UnpitchedNote
20
+ # └── Backup / Forward (timeline navigation)
21
+ #
22
+ # ## DSL Features
23
+ #
24
+ # The builder provides two equivalent ways to create scores:
25
+ #
26
+ # 1. **Constructor + add methods**: Imperative style
27
+ # 2. **DSL blocks**: Declarative style with `with` blocks
28
+ #
29
+ # Both styles leverage `AttributeBuilder` and `With` mixins from core-ext.
30
+ #
31
+ # ## Use Cases
32
+ #
33
+ # - Algorithmic composition with MusicXML export
34
+ # - Score generation from Musa DSL performances
35
+ # - Converting MIDI recordings to notation
36
+ # - Creating notation examples programmatically
37
+ #
38
+ # @example Simple score with DSL style
39
+ # score = Musa::MusicXML::Builder::ScorePartwise.new do
40
+ # work_title "My Composition"
41
+ # creators composer: "Composer Name"
42
+ #
43
+ # part :p1, name: "Piano" do
44
+ # measure do
45
+ # attributes do
46
+ # divisions 2
47
+ # key fifths: 0 # C major
48
+ # time beats: 4, beat_type: 4
49
+ # clef sign: 'G', line: 2
50
+ # end
51
+ #
52
+ # pitch 'C', octave: 4, duration: 2, type: 'quarter'
53
+ # pitch 'D', octave: 4, duration: 2, type: 'quarter'
54
+ # pitch 'E', octave: 4, duration: 2, type: 'quarter'
55
+ # pitch 'F', octave: 4, duration: 2, type: 'quarter'
56
+ # end
57
+ # end
58
+ # end
59
+ #
60
+ # File.write('output.xml', score.to_xml.string)
61
+ #
62
+ # @example Constructor + add methods style
63
+ # score = Musa::MusicXML::Builder::ScorePartwise.new
64
+ # score.work_title = "My Composition"
65
+ # score.add_creator "composer", "Composer Name"
66
+ #
67
+ # part = score.add_part :p1, name: "Piano"
68
+ # measure = part.add_measure divisions: 2
69
+ # measure.attributes.last.add_key fifths: 0
70
+ # measure.attributes.last.add_time beats: 4, beat_type: 4
71
+ # measure.attributes.last.add_clef sign: 'G', line: 2
72
+ #
73
+ # measure.add_pitch step: 'C', octave: 4, duration: 2, type: 'quarter'
74
+ #
75
+ # File.write('output.xml', score.to_xml.string)
76
+ #
77
+ # @see Builder Main builder namespace
78
+ # @see Builder::ScorePartwise Entry point for score creation
2
79
  module MusicXML
80
+ # Builder classes for MusicXML generation.
81
+ #
82
+ # Contains all the builder classes that construct MusicXML elements.
83
+ # The main entry point is {ScorePartwise}.
84
+ #
85
+ # @see ScorePartwise Main score builder
3
86
  module Builder
87
+ # Internal implementation classes for MusicXML builder.
88
+ #
89
+ # This module contains the actual implementation classes used by the builder.
90
+ # Users should access these through the main `Musa::MusicXML::Builder` namespace.
91
+ #
92
+ # @api private
4
93
  module Internal
94
+ # Helper modules and methods for MusicXML generation.
95
+ #
96
+ # Provides shared functionality for XML serialization, element construction,
97
+ # and value formatting across all builder classes.
5
98
  module Helper
99
+ # Mixin for classes not yet implemented.
100
+ #
101
+ # Used as a placeholder for MusicXML elements that are planned but not
102
+ # yet implemented. Raises `NotImplementedError` when attempting to use.
103
+ #
104
+ # @api private
6
105
  module NotImplemented
106
+ # Placeholder initializer accepting any parameters.
107
+ #
108
+ # @param _args [Hash] ignored keyword arguments
7
109
  def initialize(**_args); end
8
110
 
111
+ # Raises error indicating the class is not implemented.
112
+ #
113
+ # @param io [IO, nil] ignored
114
+ # @param indent [Integer, nil] ignored
115
+ # @raise [NotImplementedError] always raised with helpful message
9
116
  def to_xml(io = nil, indent: nil)
10
117
  raise NotImplementedError, "#{self.class} not yet implemented. Ask Javier do his work!"
11
118
  end
12
119
  end
13
120
 
121
+ # Mixin for XML serialization capability.
122
+ #
123
+ # Provides the public `to_xml` interface that handles IO and indentation
124
+ # setup, delegating to the private `_to_xml` method for actual XML generation.
125
+ #
126
+ # ## Usage
127
+ #
128
+ # Classes including this module must implement `_to_xml(io, indent:, tabs:)`.
129
+ #
130
+ # @example Including in a class
131
+ # class MyElement
132
+ # include Musa::MusicXML::Builder::Internal::Helper::ToXML
133
+ #
134
+ # private
135
+ #
136
+ # def _to_xml(io, indent:, tabs:)
137
+ # io.puts "#{tabs}<my-element />"
138
+ # end
139
+ # end
140
+ #
141
+ # element = MyElement.new
142
+ # element.to_xml # => StringIO with XML content
14
143
  module ToXML
144
+ # Converts the object to MusicXML format.
145
+ #
146
+ # This method sets up the IO stream and indentation, then delegates to
147
+ # the private `_to_xml` method for actual XML generation.
148
+ #
149
+ # @param io [IO, StringIO, nil] output stream (creates StringIO if nil)
150
+ # @param indent [Integer, nil] indentation level (default: 0)
151
+ # @return [IO, StringIO] the io parameter, containing the XML output
152
+ #
153
+ # @example Writing to file
154
+ # File.open('output.xml', 'w') do |f|
155
+ # element.to_xml(f)
156
+ # end
157
+ #
158
+ # @example Getting XML as string
159
+ # xml_string = element.to_xml.string
15
160
  def to_xml(io = nil, indent: nil)
16
161
  io ||= StringIO.new
17
162
  indent ||= 0
@@ -25,10 +170,34 @@ module Musa
25
170
 
26
171
  private
27
172
 
173
+ # Abstract method for XML generation.
174
+ #
175
+ # Subclasses must implement this method to generate their XML content.
176
+ #
177
+ # @param io [IO] output stream to write XML to
178
+ # @param indent [Integer] current indentation level
179
+ # @param tabs [String] precomputed tab string for current indent
180
+ # @return [void]
181
+ #
182
+ # @api private
28
183
  def _to_xml(io, indent:, tabs:); end
29
184
  end
30
185
 
186
+ # Mixin for XML header serialization (used in part-list).
187
+ #
188
+ # Similar to {ToXML}, but for elements that appear in the `<part-list>`
189
+ # section of MusicXML (parts and part groups).
190
+ #
191
+ # Classes including this module must implement `_header_to_xml(io, indent:, tabs:)`.
31
192
  module HeaderToXML
193
+ # Converts the object's header representation to MusicXML.
194
+ #
195
+ # Used for elements that appear in the `<part-list>` section, such as
196
+ # `<score-part>` and `<part-group>` declarations.
197
+ #
198
+ # @param io [IO, StringIO, nil] output stream (creates StringIO if nil)
199
+ # @param indent [Integer, nil] indentation level (default: 0)
200
+ # @return [IO, StringIO] the io parameter, containing the XML output
32
201
  def header_to_xml(io = nil, indent: nil)
33
202
  io ||= StringIO.new
34
203
  indent ||= 0
@@ -42,11 +211,40 @@ module Musa
42
211
 
43
212
  private
44
213
 
214
+ # Abstract method for header XML generation.
215
+ #
216
+ # Subclasses must implement this method to generate their header XML.
217
+ #
218
+ # @param io [IO] output stream to write XML to
219
+ # @param indent [Integer] current indentation level
220
+ # @param tabs [String] precomputed tab string for current indent
221
+ # @return [void]
222
+ #
223
+ # @api private
45
224
  def _header_to_xml(io, indent:, tabs:); end
46
225
  end
47
226
 
48
227
  private
49
228
 
229
+ # Creates class instance from Hash or returns existing instance.
230
+ #
231
+ # This helper method provides flexible parameter handling, allowing
232
+ # methods to accept either a fully-constructed instance or a hash of
233
+ # constructor parameters.
234
+ #
235
+ # @param klass [Class] expected class type
236
+ # @param hash_or_class_instance [klass, Hash, nil] value to process
237
+ # @return [klass, nil] instance of klass, or nil
238
+ #
239
+ # @raise [ArgumentError] if value is not klass, Hash, or nil
240
+ #
241
+ # @example Flexible parameter acceptance
242
+ # # Method can accept either:
243
+ # time_modification: { actual_notes: 3, normal_notes: 2 }
244
+ # # or:
245
+ # time_modification: TimeModification.new(actual_notes: 3, normal_notes: 2)
246
+ #
247
+ # @api private
50
248
  def make_instance_if_needed(klass, hash_or_class_instance)
51
249
  case hash_or_class_instance
52
250
  when klass
@@ -60,6 +258,33 @@ module Musa
60
258
  end
61
259
  end
62
260
 
261
+ # Converts value to XML attribute string with boolean support.
262
+ #
263
+ # Handles three types of values:
264
+ # - **String/Numeric**: Outputs as attribute value
265
+ # - **true**: Outputs specified true_value (if provided)
266
+ # - **false**: Outputs specified false_value (if provided)
267
+ # - **Other**: Returns empty string (omits attribute)
268
+ #
269
+ # @param value [Object] value to convert
270
+ # @param attribute [String] attribute name
271
+ # @param true_value [String, nil] value to use for `true`
272
+ # @param false_value [String, nil] value to use for `false`
273
+ # @return [String] formatted attribute string or empty string
274
+ #
275
+ # @example String value
276
+ # decode_bool_or_string_attribute('above', 'placement')
277
+ # # => ' placement="above"'
278
+ #
279
+ # @example Boolean with mappings
280
+ # decode_bool_or_string_attribute(true, 'bracket', 'yes', 'no')
281
+ # # => ' bracket="yes"'
282
+ #
283
+ # @example Nil value
284
+ # decode_bool_or_string_attribute(nil, 'placement')
285
+ # # => ''
286
+ #
287
+ # @api private
63
288
  def decode_bool_or_string_attribute(value, attribute, true_value = nil, false_value = nil)
64
289
  if value.is_a?(String) || value.is_a?(Numeric)
65
290
  " #{attribute}=\"#{value}\""
@@ -72,6 +297,21 @@ module Musa
72
297
  end
73
298
  end
74
299
 
300
+ # Converts value to XML element content with boolean support.
301
+ #
302
+ # Similar to {#decode_bool_or_string_attribute} but for element content
303
+ # rather than attributes.
304
+ #
305
+ # @param value [Object] value to convert
306
+ # @param true_value [String, nil] value to use for `true`
307
+ # @param false_value [String, nil] value to use for `false`
308
+ # @return [String] formatted content string or empty string
309
+ #
310
+ # @example
311
+ # decode_bool_or_string_value(true, 'yes', 'no') # => 'yes'
312
+ # decode_bool_or_string_value('dashed') # => 'dashed'
313
+ #
314
+ # @api private
75
315
  def decode_bool_or_string_value(value, true_value = nil, false_value = nil)
76
316
  if value.is_a?(String) || value.is_a?(Numeric)
77
317
  value
@@ -13,12 +13,117 @@ module Musa
13
13
  module MusicXML
14
14
  module Builder
15
15
  module Internal
16
+ # Measure container for musical content.
17
+ #
18
+ # Measure represents a single measure (bar) of music, containing musical elements
19
+ # in chronological order: attributes, notes, rests, backup/forward commands, and
20
+ # directions (dynamics, tempo markings, etc.).
21
+ #
22
+ # ## Element Order
23
+ #
24
+ # Elements within a measure follow MusicXML's sequential model:
25
+ # 1. **Attributes** (key, time, clef, divisions) - typically in first measure
26
+ # 2. **Directions** (tempo, dynamics) - before or between notes
27
+ # 3. **Notes/Rests** - musical content
28
+ # 4. **Backup/Forward** - timeline navigation for multiple voices/staves
29
+ #
30
+ # ## Multiple Voices and Staves
31
+ #
32
+ # For piano (grand staff) or polyphonic notation, use backup to rewind the timeline:
33
+ #
34
+ # measure do
35
+ # # Right hand (treble clef)
36
+ # pitch 'C', octave: 5, duration: 4, type: 'quarter', staff: 1
37
+ # pitch 'D', octave: 5, duration: 4, type: 'quarter', staff: 1
38
+ #
39
+ # backup 8 # Rewind to start of measure
40
+ #
41
+ # # Left hand (bass clef)
42
+ # pitch 'C', octave: 3, duration: 8, type: 'half', staff: 2
43
+ # end
44
+ #
45
+ # ## Divisions
46
+ #
47
+ # The `divisions` attribute sets timing resolution (divisions per quarter note).
48
+ # Higher values allow finer rhythmic subdivisions:
49
+ # - **divisions: 1** → quarter, half, whole only
50
+ # - **divisions: 2** → adds eighths
51
+ # - **divisions: 4** → adds sixteenths
52
+ # - **divisions: 8** → adds thirty-seconds
53
+ # - **divisions: 16** → allows complex tuplets
54
+ #
55
+ # Duration is calculated as: `duration = (note_type_value * divisions) / beat_type`
56
+ #
57
+ # @example Simple measure with quarter notes
58
+ # measure = Measure.new(1, divisions: 2) do
59
+ # attributes do
60
+ # key fifths: 0 # C major
61
+ # time beats: 4, beat_type: 4
62
+ # clef sign: 'G', line: 2
63
+ # end
64
+ #
65
+ # pitch 'C', octave: 4, duration: 2, type: 'quarter'
66
+ # pitch 'D', octave: 4, duration: 2, type: 'quarter'
67
+ # pitch 'E', octave: 4, duration: 2, type: 'quarter'
68
+ # pitch 'F', octave: 4, duration: 2, type: 'quarter'
69
+ # end
70
+ #
71
+ # @example Measure with dynamics and tempo
72
+ # measure do
73
+ # metronome beat_unit: 'quarter', per_minute: 120
74
+ #
75
+ # direction do
76
+ # dynamics 'p'
77
+ # wedge 'crescendo'
78
+ # end
79
+ #
80
+ # pitch 'C', octave: 4, duration: 4, type: 'quarter'
81
+ # pitch 'D', octave: 4, duration: 4, type: 'quarter'
82
+ #
83
+ # direction do
84
+ # wedge 'stop'
85
+ # dynamics 'f'
86
+ # end
87
+ # end
88
+ #
89
+ # @see Attributes Musical attributes (key, time, clef)
90
+ # @see PitchedNote Pitched note
91
+ # @see Rest Rest
92
+ # @see Direction Tempo, dynamics, expressions
16
93
  class Measure
17
94
  extend Musa::Extension::AttributeBuilder
18
95
  include Musa::Extension::With
19
96
 
20
97
  include Helper::ToXML
21
98
 
99
+ # Creates a new measure.
100
+ #
101
+ # @param number [Integer] measure number (automatically assigned by Part)
102
+ # @param divisions [Integer, nil] divisions per quarter note (timing resolution)
103
+ # @param key_cancel [Integer, nil] key cancellation
104
+ # @param key_fifths [Integer, nil] key signature (-7 to +7, circle of fifths)
105
+ # @param key_mode [String, nil] mode ('major' or 'minor')
106
+ # @param time_senza_misura [Boolean, nil] unmeasured time
107
+ # @param time_beats [Integer, nil] time signature numerator
108
+ # @param time_beat_type [Integer, nil] time signature denominator
109
+ # @param clef_sign [String, nil] clef sign ('G', 'F', 'C')
110
+ # @param clef_line [Integer, nil] clef line number
111
+ # @param clef_octave_change [Integer, nil] octave transposition
112
+ # @yield Optional DSL block for adding measure content
113
+ #
114
+ # @example First measure with all attributes
115
+ # Measure.new(1,
116
+ # divisions: 4,
117
+ # key_fifths: 2, # D major
118
+ # time_beats: 3, time_beat_type: 4,
119
+ # clef_sign: 'G', clef_line: 2
120
+ # )
121
+ #
122
+ # @example Measure with DSL block
123
+ # Measure.new(2) do
124
+ # pitch 'E', octave: 4, duration: 4, type: 'quarter'
125
+ # rest duration: 4, type: 'quarter'
126
+ # end
22
127
  def initialize(number, divisions: nil,
23
128
  key_cancel: nil, key_fifths: nil, key_mode: nil,
24
129
  time_senza_misura: nil, time_beats: nil, time_beat_type: nil,
@@ -44,9 +149,39 @@ module Musa
44
149
  with &block if block_given?
45
150
  end
46
151
 
152
+ # Measure number.
153
+ # @return [Integer]
47
154
  attr_accessor :number
155
+
156
+ # Ordered list of elements in this measure.
157
+ # @return [Array<Object>] notes, rests, attributes, directions, etc.
48
158
  attr_reader :elements
49
159
 
160
+ # Adds musical attributes to the measure.
161
+ #
162
+ # Attributes define key signature, time signature, clef, and timing divisions.
163
+ # Typically appear at the start of the first measure or when they change.
164
+ #
165
+ # @option divisions [Integer, nil] divisions per quarter note
166
+ # @option key_cancel [Integer, nil] key to cancel
167
+ # @option key_fifths [Integer, nil] key signature (-7 to +7)
168
+ # @option key_mode [String, nil] 'major' or 'minor'
169
+ # @option time_senza_misura [Boolean, nil] unmeasured time
170
+ # @option time_beats [Integer, nil] time signature numerator
171
+ # @option time_beat_type [Integer, nil] time signature denominator
172
+ # @option clef_sign [String, nil] 'G', 'F', or 'C'
173
+ # @option clef_line [Integer, nil] clef line
174
+ # @option clef_octave_change [Integer, nil] octave transposition
175
+ # @yield Optional DSL block for adding keys, times, clefs
176
+ # @return [Attributes] the created attributes object
177
+ #
178
+ # @example Via DSL block
179
+ # measure.attributes do
180
+ # divisions 4
181
+ # key fifths: 1 # G major
182
+ # time beats: 3, beat_type: 4
183
+ # clef sign: 'G', line: 2
184
+ # end
50
185
  attr_complex_adder_to_custom :attributes, plural: :attributes, variable: :@attributes do
51
186
  | divisions: nil,
52
187
  key_cancel: nil, key_fifths: nil, key_mode: nil,
@@ -65,62 +200,211 @@ module Musa
65
200
  end
66
201
  end
67
202
 
203
+ # Adds a pitched note.
204
+ #
205
+ # @return [PitchedNote] the created note
206
+ #
207
+ # @example
208
+ # measure.pitch 'C', octave: 4, duration: 4, type: 'quarter'
209
+ # measure.pitch step: 'E', octave: 4, duration: 2, type: 'eighth', dots: 1
210
+ #
211
+ # @see PitchedNote For full parameter list
68
212
  attr_complex_adder_to_custom :pitch do | *parameters, **key_parameters |
69
213
  PitchedNote.new(*parameters, **key_parameters).tap { |note| @elements << note }
70
214
  end
71
215
 
216
+ # Adds a rest.
217
+ #
218
+ # @return [Rest] the created rest
219
+ #
220
+ # @example
221
+ # measure.rest duration: 4, type: 'quarter'
222
+ # measure.rest duration: 8, type: 'half', measure: true # whole measure rest
223
+ #
224
+ # @see Rest For full parameter list
72
225
  attr_complex_adder_to_custom :rest do | *parameters, **key_parameters |
73
226
  Rest.new(*parameters, **key_parameters).tap { |rest| @elements << rest }
74
227
  end
75
228
 
229
+ # Adds an unpitched note (for percussion).
230
+ #
231
+ # @return [UnpitchedNote] the created unpitched note
232
+ #
233
+ # @see UnpitchedNote For details
76
234
  attr_complex_adder_to_custom :unpitched do | *parameters, **key_parameters |
77
235
  UnpitchedNote.new(*parameters, **key_parameters).tap { |unpitched| @elements << unpitched }
78
236
  end
79
237
 
238
+ # Rewinds the musical timeline.
239
+ #
240
+ # Backup moves the current time position backward by the specified duration,
241
+ # allowing multiple voices or staves to be layered in the same time span.
242
+ #
243
+ # @return [Backup] the created backup element
244
+ #
245
+ # @example Piano grand staff
246
+ # measure do
247
+ # pitch 'C', octave: 5, duration: 8, type: 'half', staff: 1
248
+ # backup 8 # Rewind to start
249
+ # pitch 'C', octave: 3, duration: 8, type: 'half', staff: 2
250
+ # end
251
+ #
252
+ # @see Forward For moving forward
80
253
  attr_complex_adder_to_custom :backup do |duration|
81
254
  Backup.new(duration).tap { |backup| @elements << backup }
82
255
  end
83
256
 
257
+ # Advances the musical timeline.
258
+ #
259
+ # Forward moves the current time position forward without sounding,
260
+ # creating rests or gaps in the timeline.
261
+ #
262
+ # @return [Forward] the created forward element
263
+ #
264
+ # @example Skip to beat 3
265
+ # measure do
266
+ # pitch 'C', octave: 4, duration: 2, type: 'quarter'
267
+ # forward 4 # Skip 2 beats
268
+ # pitch 'D', octave: 4, duration: 2, type: 'quarter'
269
+ # end
84
270
  attr_complex_adder_to_custom :forward do |duration, voice: nil, staff: nil|
85
271
  Forward.new(duration, voice: voice, staff: staff).tap { |forward| @elements << forward }
86
272
  end
87
273
 
274
+ # Adds a direction element (dynamics, tempo, expressions).
275
+ #
276
+ # Directions contain non-note musical instructions like dynamics (p, f),
277
+ # tempo markings, wedges (crescendo/diminuendo), pedal marks, etc.
278
+ #
279
+ # @yield Optional DSL block for direction content
280
+ # @return [Direction] the created direction
281
+ #
282
+ # @example Dynamics with crescendo
283
+ # measure.direction do
284
+ # dynamics 'p'
285
+ # wedge 'crescendo'
286
+ # end
287
+ #
288
+ # @see Direction For direction types
88
289
  attr_complex_adder_to_custom :direction do |*parameters, **key_parameters, &block|
89
290
  Direction.new(*parameters, **key_parameters, &block).tap { |direction| @elements << direction }
90
291
  end
91
292
 
293
+ # Direction shortcuts - these create a Direction automatically.
294
+ #
295
+ # The following methods are convenience shortcuts that create a Direction
296
+ # element containing the specified direction type. They accept placement and
297
+ # offset parameters that are passed to the Direction wrapper.
298
+
299
+ # Adds a metronome (tempo) marking.
300
+ #
301
+ # @option placement [String, nil] 'above' or 'below'
302
+ # @option offset [Numeric, nil] offset in divisions
303
+ # @option beat_unit [String] note value ('quarter', 'half', etc.)
304
+ # @option per_minute [Numeric] tempo in BPM
305
+ # @yield Optional block
306
+ # @return [Direction] direction containing metronome
307
+ #
308
+ # @example
309
+ # measure.metronome beat_unit: 'quarter', per_minute: 120
92
310
  attr_complex_adder_to_custom(:metronome) {
93
311
  |*p, placement: nil, offset: nil, **kp, &b|
94
312
  direction(placement: placement, offset: offset) { metronome *p, **kp, &b } }
95
313
 
314
+ # Adds a wedge (crescendo/diminuendo).
315
+ #
316
+ # @option placement [String, nil] 'above' or 'below'
317
+ # @option offset [Numeric, nil] offset in divisions
318
+ # @option niente [Boolean, nil] niente attribute
319
+ # @return [Direction] direction containing wedge
320
+ #
321
+ # @example
322
+ # measure.wedge 'crescendo', niente: true
96
323
  attr_complex_adder_to_custom(:wedge) {
97
324
  |*p, placement: nil, offset: nil, **kp, &b|
98
325
  direction(placement: placement, offset: offset) { wedge *p, **kp, &b } }
99
326
 
327
+ # Adds dynamics (p, pp, f, ff, etc.).
328
+ #
329
+ # @option placement [String, nil] 'above' or 'below'
330
+ # @option offset [Numeric, nil] offset in divisions
331
+ # @return [Direction] direction containing dynamics
332
+ #
333
+ # @example
334
+ # measure.dynamics 'pp'
335
+ # measure.dynamics ['mf', 'sf'] # Multiple dynamics
100
336
  attr_complex_adder_to_custom(:dynamics) {
101
337
  |*p, placement: nil, offset: nil, **kp, &b|
102
338
  direction(placement: placement, offset: offset) { dynamics *p, **kp, &b } }
103
339
 
340
+ # Adds pedal marking.
341
+ #
342
+ # @option placement [String, nil] 'above' or 'below'
343
+ # @option offset [Numeric, nil] offset in divisions
344
+ # @option line [Boolean, nil] show pedal line
345
+ # @return [Direction] direction containing pedal
346
+ #
347
+ # @example
348
+ # measure.pedal 'start', line: true
104
349
  attr_complex_adder_to_custom(:pedal) {
105
350
  |*p, placement: nil, offset: nil, **kp, &b|
106
351
  direction(placement: placement, offset: offset) { pedal *p, **kp, &b } }
107
352
 
353
+ # Adds bracket notation.
354
+ #
355
+ # @option placement [String, nil] 'above' or 'below'
356
+ # @option offset [Numeric, nil] offset in divisions
357
+ # @option line_end [String, nil] line end type
358
+ # @return [Direction] direction containing bracket
108
359
  attr_complex_adder_to_custom(:bracket) {
109
360
  |*p, placement: nil, offset: nil, **kp, &b|
110
361
  direction(placement: placement, offset: offset) { bracket *p, **kp, &b } }
111
362
 
363
+ # Adds dashed line.
364
+ #
365
+ # @option placement [String, nil] 'above' or 'below'
366
+ # @option offset [Numeric, nil] offset in divisions
367
+ # @return [Direction] direction containing dashes
112
368
  attr_complex_adder_to_custom(:dashes) {
113
369
  |*p, placement: nil, offset: nil, **kp, &b|
114
370
  direction(placement: placement, offset: offset) { dashes *p, **kp, &b } }
115
371
 
372
+ # Adds text annotation.
373
+ #
374
+ # @option placement [String, nil] 'above' or 'below'
375
+ # @option offset [Numeric, nil] offset in divisions
376
+ # @return [Direction] direction containing words
377
+ #
378
+ # @example
379
+ # measure.words "rit.", placement: 'above'
116
380
  attr_complex_adder_to_custom(:words) {
117
381
  |*p, placement: nil, offset: nil, **kp, &b|
118
382
  direction(placement: placement, offset: offset) { words *p, **kp, &b } }
119
383
 
384
+ # Adds octave shift (8va/8vb).
385
+ #
386
+ # @option placement [String, nil] 'above' or 'below'
387
+ # @option offset [Numeric, nil] offset in divisions
388
+ # @option size [Integer, nil] octave shift size (8 or 15)
389
+ # @return [Direction] direction containing octave_shift
390
+ #
391
+ # @example
392
+ # measure.octave_shift 'up', size: 8
120
393
  attr_complex_adder_to_custom(:octave_shift) {
121
394
  |*p, placement: nil, offset: nil, **kp, &b|
122
395
  direction(placement: placement, offset: offset) { octave_shift *p, **kp, &b } }
123
396
 
397
+ # Generates the measure XML element with all contained elements.
398
+ #
399
+ # Outputs elements in the order they were added, which must follow
400
+ # MusicXML's element ordering rules (attributes, then notes/directions/backup/forward).
401
+ #
402
+ # @param io [IO] output stream
403
+ # @param indent [Integer] indentation level
404
+ # @param tabs [String] tab string
405
+ # @return [void]
406
+ #
407
+ # @api private
124
408
  def _to_xml(io, indent:, tabs:)
125
409
  io.puts "#{tabs}<measure number=\"#{@number.to_i}\">"
126
410