musa-dsl 0.30.2 → 0.41.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +0 -1
  6. data/README.md +227 -6
  7. data/docs/README.md +83 -0
  8. data/docs/api-reference.md +86 -0
  9. data/docs/getting-started/quick-start.md +93 -0
  10. data/docs/getting-started/tutorial.md +58 -0
  11. data/docs/subsystems/core-extensions.md +316 -0
  12. data/docs/subsystems/datasets.md +465 -0
  13. data/docs/subsystems/generative.md +290 -0
  14. data/docs/subsystems/matrix.md +63 -0
  15. data/docs/subsystems/midi.md +123 -0
  16. data/docs/subsystems/music.md +544 -0
  17. data/docs/subsystems/musicxml-builder.md +264 -0
  18. data/docs/subsystems/neumas.md +71 -0
  19. data/docs/subsystems/repl.md +135 -0
  20. data/docs/subsystems/sequencer.md +98 -0
  21. data/docs/subsystems/series.md +302 -0
  22. data/docs/subsystems/transcription.md +152 -0
  23. data/docs/subsystems/transport.md +177 -0
  24. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  25. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  26. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  27. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  28. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  29. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  30. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  31. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  32. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  33. data/lib/musa-dsl/core-ext/with.rb +114 -0
  34. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  35. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  36. data/lib/musa-dsl/datasets/e.rb +186 -2
  37. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  38. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  39. data/lib/musa-dsl/datasets/helper.rb +75 -0
  40. data/lib/musa-dsl/datasets/p.rb +177 -2
  41. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  42. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  43. data/lib/musa-dsl/datasets/ps.rb +134 -4
  44. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  45. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  48. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  49. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  50. data/lib/musa-dsl/datasets/score.rb +279 -0
  51. data/lib/musa-dsl/datasets/v.rb +88 -0
  52. data/lib/musa-dsl/generative/darwin.rb +215 -1
  53. data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
  54. data/lib/musa-dsl/generative/markov.rb +135 -3
  55. data/lib/musa-dsl/generative/rules.rb +312 -4
  56. data/lib/musa-dsl/generative/variatio.rb +286 -2
  57. data/lib/musa-dsl/logger/logger.rb +267 -2
  58. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  59. data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
  60. data/lib/musa-dsl/midi/midi-voices.rb +275 -4
  61. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  62. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  63. data/lib/musa-dsl/music/chords.rb +353 -2
  64. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
  65. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  66. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  67. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  68. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  69. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  70. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  71. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  72. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  73. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  74. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  75. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  76. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  77. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  78. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  79. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  80. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  81. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  82. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  83. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  84. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  85. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  86. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  87. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  88. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  89. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  90. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  91. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  92. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  93. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  94. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  95. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  96. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  97. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  98. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  99. data/lib/musa-dsl/music/scales.rb +1384 -40
  100. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  101. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  102. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  103. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  104. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  105. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  106. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  107. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  108. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  109. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  110. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  111. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  112. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  113. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  114. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  115. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  116. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  117. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  118. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  119. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  120. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  121. data/lib/musa-dsl/repl/repl.rb +550 -0
  122. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  123. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  124. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  125. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  126. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  127. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  128. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  129. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  130. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  131. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  132. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  133. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  134. data/lib/musa-dsl/series/base-series.rb +843 -5
  135. data/lib/musa-dsl/series/buffer-serie.rb +54 -0
  136. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
  137. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  138. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  139. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  140. data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
  141. data/lib/musa-dsl/series/queue-serie.rb +78 -0
  142. data/lib/musa-dsl/series/series-composer.rb +701 -0
  143. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  144. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  145. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  146. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  147. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  148. data/lib/musa-dsl/transport/clock.rb +125 -0
  149. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  150. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  151. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  152. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  153. data/lib/musa-dsl/transport/timer.rb +83 -0
  154. data/lib/musa-dsl/transport/transport.rb +318 -0
  155. data/lib/musa-dsl/version.rb +2 -1
  156. data/lib/musa-dsl.rb +132 -25
  157. data/musa-dsl.gemspec +25 -18
  158. metadata +158 -16
@@ -1,9 +1,47 @@
1
1
  require 'prime'
2
2
 
3
+ # Time and duration processing for MusicXML export.
4
+ #
5
+ # This module provides helper methods for converting musical durations
6
+ # to MusicXML note types, dots, and tuplet ratios. Handles the complex
7
+ # mathematics of decomposing arbitrary rational durations into standard
8
+ # notation elements.
9
+ #
10
+ # ## Duration Representation
11
+ #
12
+ # Durations are Rational numbers where 1 = one beat (typically quarter note).
13
+ # - 1r = quarter note
14
+ # - 1/2r = eighth note
15
+ # - 3/2r = dotted quarter
16
+ # - 1/3r = eighth note triplet
17
+ #
18
+ # ## Decomposition Process
19
+ #
20
+ # 1. **Decompose**: Break duration into sum of simple durations (powers of 2)
21
+ # 2. **Integrate**: Combine consecutive halves into dotted notes
22
+ # 3. **Type & Dots**: Determine note type and dot count
23
+ # 4. **Tuplet Ratio**: Calculate tuplet modification if needed
24
+ #
25
+ # @api private
3
26
  module Musa::Datasets::Score::ToMXML
4
27
  private
5
28
 
29
+ # Decomposes duration into dotted note representation.
30
+ #
31
+ # Internal class representing the breakdown of an element's duration
32
+ # within a measure. Handles ties across bar lines and duration decomposition.
33
+ #
34
+ # @api private
6
35
  class ElementDurationDecomposition
36
+ # Creates duration decomposition for element.
37
+ #
38
+ # @param element [Hash] event with :start, :finish, :dataset keys
39
+ # @param bar [Integer] bar number (1-based)
40
+ # @param bar_size [Rational] bar duration (default: 1r)
41
+ #
42
+ # @note This method is experimental and currently unused. See TODO comment.
43
+ #
44
+ # @api private
7
45
  def initialize(element, bar, bar_size = 1r) # TODO remove (unused because of bad strategy to time groups)
8
46
  @continue_from_previous_bar = element[:start] < bar
9
47
  @continue_to_next_bar = element[:finish] >= bar + bar_size
@@ -17,7 +55,25 @@ module Musa::Datasets::Score::ToMXML
17
55
  @duration_decomposition = integrate_as_dotteable_durations(decompose_as_sum_of_simple_durations(@duration))
18
56
  end
19
57
 
20
- attr_reader :continue_from_previous_bar, :continue_to_next_bar, :start, :duration, :duration_decomposition
58
+ # Whether note continues from previous bar (tied).
59
+ # @return [Boolean]
60
+ attr_reader :continue_from_previous_bar
61
+
62
+ # Whether note continues to next bar (tied).
63
+ # @return [Boolean]
64
+ attr_reader :continue_to_next_bar
65
+
66
+ # Start time within bar.
67
+ # @return [Rational]
68
+ attr_reader :start
69
+
70
+ # Total duration.
71
+ # @return [Rational]
72
+ attr_reader :duration
73
+
74
+ # Duration broken into dotteable components.
75
+ # @return [Array<Rational>]
76
+ attr_reader :duration_decomposition
21
77
 
22
78
  def to_s
23
79
  "ElementDurationDecomposition(#{@duration}) = [#{@duration_decomposition}]"
@@ -28,6 +84,21 @@ module Musa::Datasets::Score::ToMXML
28
84
 
29
85
  private_constant :ElementDurationDecomposition
30
86
 
87
+ # Optimizes time and tuplet representation (experimental).
88
+ #
89
+ # Attempts to find optimal tuplet grouping for elements in a bar.
90
+ # Currently unused due to incomplete implementation.
91
+ #
92
+ # @param elements [Array] PDV events
93
+ # @param bar [Integer] bar number
94
+ # @param bar_size [Rational] bar duration
95
+ #
96
+ # @return [nil] incomplete implementation
97
+ #
98
+ # @note This method is experimental and currently unused. See TODO comment.
99
+ #
100
+ # @api private
101
+ # @todo Complete or remove this experimental method
31
102
  def time_and_tuplet_optimize(elements, bar, bar_size = 1r) # TODO remove (unused because of bad strategy to time groups)
32
103
  decompositions = elements.collect { |pdv| ElementDurationDecomposition.new(pdv, bar, bar_size) }
33
104
 
@@ -46,6 +117,32 @@ module Musa::Datasets::Score::ToMXML
46
117
  nil
47
118
  end
48
119
 
120
+ # Decomposes duration into sum of simple durations.
121
+ #
122
+ # Breaks a rational duration into sum of fractions with power-of-2 denominators.
123
+ # This is the first step in converting arbitrary durations to standard notation.
124
+ #
125
+ # Uses greedy algorithm: repeatedly subtracts largest possible simple duration.
126
+ #
127
+ # @param duration [Rational] duration to decompose
128
+ #
129
+ # @return [Array<Rational>] simple durations that sum to input
130
+ #
131
+ # @raise [ArgumentError] if duration cannot be decomposed
132
+ #
133
+ # @example Quarter note
134
+ # decompose_as_sum_of_simple_durations(1r)
135
+ # # => [1r]
136
+ #
137
+ # @example Dotted quarter
138
+ # decompose_as_sum_of_simple_durations(3/2r)
139
+ # # => [1r, 1/2r]
140
+ #
141
+ # @example Complex duration
142
+ # decompose_as_sum_of_simple_durations(5/8r)
143
+ # # => [1/2r, 1/8r]
144
+ #
145
+ # @api private
49
146
  def decompose_as_sum_of_simple_durations(duration)
50
147
  return [] if duration.zero?
51
148
 
@@ -69,6 +166,17 @@ module Musa::Datasets::Score::ToMXML
69
166
  summands
70
167
  end
71
168
 
169
+ # Generates all combinations of array elements.
170
+ #
171
+ # @param numbers [Array] elements to combine
172
+ #
173
+ # @return [Array<Array>] all unique combinations (excluding empty)
174
+ #
175
+ # @example
176
+ # all_combinations([2, 3])
177
+ # # => [[2], [3], [2, 3]]
178
+ #
179
+ # @api private
72
180
  def all_combinations(numbers)
73
181
  all_combinations = []
74
182
  i = 1
@@ -80,6 +188,28 @@ module Musa::Datasets::Score::ToMXML
80
188
  all_combinations.uniq
81
189
  end
82
190
 
191
+ # Integrates consecutive halves into dotted durations.
192
+ #
193
+ # Combines simple durations where each is half the previous into
194
+ # single dotted duration. Example: [1r, 1/2r] → [3/2r] (dotted quarter).
195
+ #
196
+ # @param simple_durations [Array<Rational>] simple durations from decomposition
197
+ #
198
+ # @return [Array<Rational>] integrated dotted durations
199
+ #
200
+ # @example Dotted quarter
201
+ # integrate_as_dotteable_durations([1r, 1/2r])
202
+ # # => [3/2r]
203
+ #
204
+ # @example Double-dotted half
205
+ # integrate_as_dotteable_durations([1/2r, 1/4r, 1/8r])
206
+ # # => [7/8r]
207
+ #
208
+ # @example Non-dotteable
209
+ # integrate_as_dotteable_durations([1r, 1/4r])
210
+ # # => [1r, 1/4r] (no integration possible)
211
+ #
212
+ # @api private
83
213
  def integrate_as_dotteable_durations(simple_durations)
84
214
  integrated_durations = []
85
215
  last = nil
@@ -94,6 +224,32 @@ module Musa::Datasets::Score::ToMXML
94
224
  integrated_durations
95
225
  end
96
226
 
227
+ # Calculates note type, dots, and tuplet ratio.
228
+ #
229
+ # Converts a dotteable duration into MusicXML note representation:
230
+ # - **type**: Base note type (quarter, eighth, etc.)
231
+ # - **dots**: Number of dots (0-3 typically)
232
+ # - **tuplet_ratio**: Tuplet modification (3:2 for triplets, etc.)
233
+ #
234
+ # @param noteable_duration [Rational] duration to convert
235
+ #
236
+ # @return [Array(String, Integer, Rational)] [type, dots, tuplet_ratio]
237
+ #
238
+ # @raise [ArgumentError] if duration cannot be represented with dots
239
+ #
240
+ # @example Quarter note
241
+ # type_and_dots_and_tuplet_ratio(1r)
242
+ # # => ["quarter", 0, 1r]
243
+ #
244
+ # @example Dotted quarter
245
+ # type_and_dots_and_tuplet_ratio(3/2r)
246
+ # # => ["quarter", 1, 1r]
247
+ #
248
+ # @example Eighth triplet
249
+ # type_and_dots_and_tuplet_ratio(1/3r)
250
+ # # => ["eighth", 0, 3/2r]
251
+ #
252
+ # @api private
97
253
  def type_and_dots_and_tuplet_ratio(noteable_duration)
98
254
  r = decompose_as_sum_of_simple_durations(noteable_duration)
99
255
  n = r.shift
@@ -117,6 +273,18 @@ module Musa::Datasets::Score::ToMXML
117
273
  return type, dots, tuplet_ratio
118
274
  end
119
275
 
276
+ # Finds nearest power of 2 greater than or equal to number.
277
+ #
278
+ # @param number [Numeric] number to round up
279
+ #
280
+ # @return [Integer] nearest upper power of 2
281
+ #
282
+ # @example
283
+ # nearest_upper_power_of_2(5) # => 8
284
+ # nearest_upper_power_of_2(8) # => 8
285
+ # nearest_upper_power_of_2(9) # => 16
286
+ #
287
+ # @api private
120
288
  def nearest_upper_power_of_2(number)
121
289
  return 0 if number.zero?
122
290
 
@@ -127,6 +295,18 @@ module Musa::Datasets::Score::ToMXML
127
295
  2 ** (exp_floor + plus)
128
296
  end
129
297
 
298
+ # Finds nearest power of 2 less than or equal to number.
299
+ #
300
+ # @param number [Numeric] number to round down
301
+ #
302
+ # @return [Integer] nearest lower power of 2
303
+ #
304
+ # @example
305
+ # nearest_lower_power_of_2(5) # => 4
306
+ # nearest_lower_power_of_2(8) # => 8
307
+ # nearest_lower_power_of_2(15) # => 8
308
+ #
309
+ # @api private
130
310
  def nearest_lower_power_of_2(number)
131
311
  return 0 if number.zero?
132
312
 
@@ -135,6 +315,25 @@ module Musa::Datasets::Score::ToMXML
135
315
  2 ** exp_floor
136
316
  end
137
317
 
318
+ # Converts duration to MusicXML note type name.
319
+ #
320
+ # Maps inverse powers of 2 to standard note type names.
321
+ # Duration must be power of 2 between 1/1024 and maxima (8 whole notes).
322
+ #
323
+ # @param base_type_duration [Numeric] duration as power of 2
324
+ #
325
+ # @return [String] MusicXML note type name
326
+ #
327
+ # @raise [ArgumentError] if duration is not power of 2 or out of range
328
+ #
329
+ # @example Standard durations
330
+ # type_of(1r) # => "quarter"
331
+ # type_of(1/2r) # => "eighth"
332
+ # type_of(1/4r) # => "16th"
333
+ # type_of(2r) # => "half"
334
+ # type_of(4r) # => "whole"
335
+ #
336
+ # @api private
138
337
  def type_of(base_type_duration)
139
338
  duration_log2i = Math.log2(base_type_duration)
140
339
 
@@ -6,11 +6,137 @@ require_relative 'process-time'
6
6
  require_relative 'process-pdv'
7
7
  require_relative 'process-ps'
8
8
 
9
- module Musa::Datasets
9
+ module Musa::Datasets
10
10
  class Score
11
+ # MusicXML export for scores.
12
+ #
13
+ # ToMXML provides conversion of {Score} objects to MusicXML format,
14
+ # suitable for import into notation software like MuseScore, Finale,
15
+ # or Sibelius.
16
+ #
17
+ # ## Conversion Process
18
+ #
19
+ # 1. Creates MusicXML structure with metadata (title, creators, etc.)
20
+ # 2. Defines parts (instruments) with clefs and time signatures
21
+ # 3. Divides score into measures (bars)
22
+ # 4. Processes events in each measure:
23
+ #
24
+ # - {PDV} events → notes and rests
25
+ # - {PS} events → dynamics markings (crescendo, diminuendo)
26
+ #
27
+ # 5. Fills gaps with rests
28
+ #
29
+ # ## Event Types Supported
30
+ #
31
+ # - **{PDV}** (Pitch/Duration/Velocity): Converted to notes or rests
32
+ # - **{PS}** (Pitch Series): Converted to dynamics markings
33
+ #
34
+ # ## Multi-part Scores
35
+ #
36
+ # Scores can contain multiple instruments, differentiated by the
37
+ # :instrument attribute. Each part is rendered separately.
38
+ #
39
+ # ## Time Representation
40
+ #
41
+ # - Score times are 1-based (first beat is at position 1)
42
+ # - Each measure represents one bar
43
+ # - Duration is specified in beats (1.0 = quarter note if beat_type is 4)
44
+ #
45
+ # @example Basic single-part score
46
+ # score = Musa::Datasets::Score.new
47
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
48
+ # score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
49
+ #
50
+ # mxml = score.to_mxml(
51
+ # 4, 24, # 4 beats per bar, 24 ticks per beat
52
+ # bpm: 120,
53
+ # title: 'My Song',
54
+ # creators: { composer: 'John Doe' },
55
+ # parts: { piano: { name: 'Piano', clefs: { g: 2 } } }
56
+ # )
57
+ #
58
+ # File.write('score.musicxml', mxml.to_xml.string)
59
+ #
60
+ # @example Multi-part score
61
+ # score = Musa::Datasets::Score.new
62
+ # score.at(1r, add: { instrument: :violin, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
63
+ # score.at(1r, add: { instrument: :cello, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))
64
+ #
65
+ # mxml = score.to_mxml(
66
+ # 4, 24,
67
+ # parts: {
68
+ # violin: { name: 'Violin', clefs: { g: 2 } },
69
+ # cello: { name: 'Cello', clefs: { f: 4 } }
70
+ # }
71
+ # )
72
+ #
73
+ # @example With dynamics
74
+ # score = Musa::Datasets::Score.new
75
+ # score.at(1r, add: { pitch: 60, duration: 2.0 }.extend(Musa::Datasets::PDV))
76
+ # score.at(1r, add: { type: :crescendo, duration: 2.0 }.extend(Musa::Datasets::PS))
77
+ #
78
+ # @see Musa::MusicXML::Builder MusicXML builder
79
+ # @see PDV MIDI-style events
80
+ # @see PS Pitch series for dynamics
11
81
  module ToMXML
12
82
  using Musa::Extension::InspectNice
13
83
 
84
+ # Converts score to MusicXML.
85
+ #
86
+ # Creates complete MusicXML document with metadata, parts, measures,
87
+ # notes, rests, and dynamics markings.
88
+ #
89
+ # @param beats_per_bar [Integer] time signature numerator (e.g., 4 for 4/4)
90
+ # @param ticks_per_beat [Integer] resolution per beat (typically 24)
91
+ #
92
+ # @param bpm [Integer] tempo in beats per minute (default: 90)
93
+ # @param title [String] work title (default: 'Untitled')
94
+ # @param creators [Hash{Symbol => String}] creator roles and names
95
+ # (default: { composer: 'Unknown' })
96
+ # @param encoding_date [DateTime, nil] encoding date for metadata
97
+ # @param parts [Hash{Symbol => Hash}] part definitions
98
+ # Each part: { name: String, abbreviation: String, clefs: Hash }
99
+ # Clefs: { clef_sign: line_number } (e.g., { g: 2, f: 4 } for piano)
100
+ # @param logger [Musa::Logger::Logger, nil] logger for debugging
101
+ # @param do_log [Boolean, nil] enable logging output
102
+ #
103
+ # @return [Musa::MusicXML::Builder::ScorePartwise] MusicXML document
104
+ #
105
+ # @example Simple piano score
106
+ # score = Musa::Datasets::Score.new
107
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
108
+ #
109
+ # mxml = score.to_mxml(
110
+ # 4, 24,
111
+ # bpm: 120,
112
+ # title: 'Invention',
113
+ # creators: { composer: 'J.S. Bach' },
114
+ # parts: { piano: { name: 'Piano', clefs: { g: 2, f: 4 } } }
115
+ # )
116
+ #
117
+ # @example String quartet
118
+ # score = Musa::Datasets::Score.new
119
+ # score.at(1r, add: { instrument: :vln1, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
120
+ # score.at(1r, add: { instrument: :vln2, pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
121
+ # score.at(1r, add: { instrument: :vla, pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
122
+ # score.at(1r, add: { instrument: :vc, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))
123
+ #
124
+ # mxml = score.to_mxml(
125
+ # 4, 24,
126
+ # parts: {
127
+ # vln1: { name: 'Violin I', abbreviation: 'Vln. I', clefs: { g: 2 } },
128
+ # vln2: { name: 'Violin II', abbreviation: 'Vln. II', clefs: { g: 2 } },
129
+ # vla: { name: 'Viola', abbreviation: 'Vla.', clefs: { c: 3 } },
130
+ # vc: { name: 'Cello', abbreviation: 'Vc.', clefs: { f: 4 } }
131
+ # }
132
+ # )
133
+ #
134
+ # @example Export to file
135
+ # score = Musa::Datasets::Score.new
136
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
137
+ #
138
+ # mxml = score.to_mxml(4, 24, parts: { piano: { name: 'Piano' } })
139
+ # File.write('output.musicxml', mxml.to_xml.string)
14
140
  def to_mxml(beats_per_bar, ticks_per_beat,
15
141
  bpm: nil,
16
142
  title: nil,
@@ -77,6 +203,24 @@ module Musa::Datasets
77
203
 
78
204
  private
79
205
 
206
+ # Fills a MusicXML part with measures and events.
207
+ #
208
+ # Processes each bar (measure) in the score, converting events to
209
+ # MusicXML notes, rests, and dynamics. Handles:
210
+ #
211
+ # - Initial silences (gaps before first event)
212
+ # - Event processing (PDV → notes, PS → dynamics)
213
+ # - Ending silences (filling remainder of measure)
214
+ #
215
+ # @param part [Musa::MusicXML::Builder::Part] MusicXML part to fill
216
+ # @param divisions_per_bar [Integer] total divisions in one bar
217
+ # @param instrument [Symbol, nil] instrument filter (nil for single-part scores)
218
+ # @param logger [Musa::Logger::Logger] logger for debugging
219
+ # @param do_log [Boolean] enable logging
220
+ #
221
+ # @return [void]
222
+ #
223
+ # @api private
80
224
  def fill_part(part, divisions_per_bar, instrument, logger, do_log)
81
225
  measure = nil
82
226
  dynamics_context = nil