musa-dsl 0.30.2 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -1,8 +1,84 @@
1
1
  require 'prime'
2
2
 
3
+ # PDV event processing for MusicXML export.
4
+ #
5
+ # Converts {PDV} (Pitch/Duration/Velocity) events to MusicXML notes and rests.
6
+ # Handles pitch mapping, duration decomposition, ties, articulations, and
7
+ # ornaments.
8
+ #
9
+ # ## Processing Steps
10
+ #
11
+ # 1. Extract pitch, octave, and accidentals from MIDI pitch
12
+ # 2. Calculate effective duration within measure (may span bars)
13
+ # 3. Decompose duration into MusicXML-compatible note values
14
+ # 4. Add backup/forward if needed for voice positioning
15
+ # 5. Create MusicXML note/rest elements with all attributes
16
+ #
17
+ # ## Articulations & Ornaments Supported
18
+ #
19
+ # - **:st** → staccato / staccatissimo (1 or > 1)
20
+ # - **:tr** → trill
21
+ # - **:mor** → mordent (:down/:low) or inverted mordent (:up/true)
22
+ # - **:turn** → turn (:up/true) or inverted turn (:down/:low)
23
+ # - **:grace** → grace note (with slur)
24
+ # - **:graced** → note receiving grace note (with slur)
25
+ # - **:voice** → voice number for polyphony
26
+ #
27
+ # ## Ties Across Measures
28
+ #
29
+ # Notes spanning bar lines are automatically tied. Duration is decomposed
30
+ # and tie start/stop/continue markers added appropriately.
31
+ #
32
+ # @api private
3
33
  module Musa::Datasets::Score::ToMXML
4
34
  using Musa::Extension::InspectNice
5
35
 
36
+ # Processes PDV event to MusicXML note or rest.
37
+ #
38
+ # Converts a single PDV event to one or more MusicXML note/rest elements.
39
+ # Handles duration decomposition, ties, backup/forward for polyphony,
40
+ # and all articulations/ornaments.
41
+ #
42
+ # @param measure [Musa::MusicXML::Builder::Measure] target measure
43
+ # @param bar [Integer] bar number (1-based)
44
+ # @param divisions_per_bar [Integer] total divisions in bar
45
+ # @param element [Hash] event hash from score query
46
+ # Contains :start, :finish, :dataset keys
47
+ # @param pointer [Rational] current position in bar (0-1)
48
+ # @param logger [Musa::Logger::Logger] logger for debugging
49
+ # @param do_log [Boolean] enable logging
50
+ #
51
+ # @return [Rational] updated pointer position
52
+ #
53
+ # @raise [NotImplementedError] if tuplet ratios found (not yet supported)
54
+ #
55
+ # @example Simple quarter note
56
+ # element = {
57
+ # start: 1r,
58
+ # finish: 2r,
59
+ # dataset: { pitch: 60, duration: 1r }.extend(Musa::Datasets::PDV)
60
+ # }
61
+ # pointer = process_pdv(measure, 1, 96, element, 0r, logger, false)
62
+ # # Adds C4 quarter note, returns 1r
63
+ #
64
+ # @example Rest
65
+ # element = {
66
+ # start: 1r,
67
+ # finish: 2r,
68
+ # dataset: { pitch: :silence, duration: 1r }.extend(Musa::Datasets::PDV)
69
+ # }
70
+ # pointer = process_pdv(measure, 1, 96, element, 0r, logger, false)
71
+ # # Adds quarter rest, returns 1r
72
+ #
73
+ # @example Note with articulation
74
+ # dataset = { pitch: 64, duration: 1/2r, st: true }.extend(Musa::Datasets::PDV)
75
+ # # Adds staccato eighth note
76
+ #
77
+ # @example Tied note across bar
78
+ # element = { start: 1r, finish: 3r, dataset: { pitch: 60, duration: 2r } }
79
+ # # Automatically tied: tie-start in bar 1, tie-stop in bar 2
80
+ #
81
+ # @api private
6
82
  private def process_pdv(measure, bar, divisions_per_bar, element, pointer, logger, do_log)
7
83
 
8
84
  pitch, octave, sharps = pitch_and_octave_and_sharps(element[:dataset])
@@ -127,6 +203,34 @@ module Musa::Datasets::Score::ToMXML
127
203
  pointer
128
204
  end
129
205
 
206
+ # Converts MIDI pitch to note name, octave, and accidental.
207
+ #
208
+ # Maps MIDI pitch number (0-127) to MusicXML pitch representation.
209
+ # Middle C (MIDI 60) = C4 in scientific pitch notation.
210
+ #
211
+ # @param pdv [Hash] PDV dataset with :pitch key
212
+ #
213
+ # @return [Array(String, Integer, Integer), Array(Symbol, nil, nil)]
214
+ # - For pitches: [note_name, octave, sharps]
215
+ # - For silence: [:silence, nil, nil]
216
+ #
217
+ # @example Middle C
218
+ # pitch_and_octave_and_sharps({ pitch: 60 })
219
+ # # => ["C", 4, 0]
220
+ #
221
+ # @example C#4
222
+ # pitch_and_octave_and_sharps({ pitch: 61 })
223
+ # # => ["C", 4, 1]
224
+ #
225
+ # @example A4 (440Hz)
226
+ # pitch_and_octave_and_sharps({ pitch: 69 })
227
+ # # => ["A", 4, 0]
228
+ #
229
+ # @example Rest
230
+ # pitch_and_octave_and_sharps({ pitch: :silence })
231
+ # # => [:silence, nil, nil]
232
+ #
233
+ # @api private
130
234
  private def pitch_and_octave_and_sharps(pdv)
131
235
  if pdv[:pitch] == :silence
132
236
  [:silence, nil, nil]
@@ -145,16 +249,49 @@ module Musa::Datasets::Score::ToMXML
145
249
  end
146
250
  end
147
251
 
252
+ # Converts MIDI velocity to dynamics index.
253
+ #
254
+ # Maps MIDI velocity (0-127) to dynamics marking index (0-10).
255
+ # Used for determining dynamics from velocity values.
256
+ #
257
+ # @param midi_velocity [Integer, nil] MIDI velocity value
258
+ #
259
+ # @return [Integer, nil] dynamics index (0-10), or nil if no velocity
260
+ #
261
+ # @example Pianissimo
262
+ # dynamics_index_of(16) # => 3 (ppp)
263
+ #
264
+ # @example Mezzo-forte
265
+ # dynamics_index_of(64) # => 6 (mf)
266
+ #
267
+ # @example Fortissimo
268
+ # dynamics_index_of(100) # => 9 (ff)
269
+ #
270
+ # @api private
148
271
  private def dynamics_index_of(midi_velocity)
149
272
  return nil unless midi_velocity
150
273
 
151
274
  # ppp = midi 16 ... fff = midi 127
152
275
  # mp = dynamics index 6; dynamics = 0..10
153
276
  # TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
154
- [0..0, 1..1, 2..8, 9..16, 17..33, 34..49, 49..64, 65..80, 81..96, 97..112, 113..127]
277
+ [0..0, 1..1, 2..8, 9..16, 17..33, 34..48, 49..64, 65..80, 81..96, 97..112, 113..127]
155
278
  .index { |r| r.cover? midi_velocity.round.to_i }
156
279
  end
157
280
 
281
+ # Converts dynamics index to MusicXML dynamics string.
282
+ #
283
+ # Maps dynamics index (0-10) to standard dynamics marking string.
284
+ #
285
+ # @param dynamics_index [Integer, nil] dynamics index
286
+ #
287
+ # @return [String, nil] dynamics marking string, or nil if no index
288
+ #
289
+ # @example
290
+ # dynamics_to_string(3) # => "ppp"
291
+ # dynamics_to_string(6) # => "mp"
292
+ # dynamics_to_string(9) # => "ff"
293
+ #
294
+ # @api private
158
295
  private def dynamics_to_string(dynamics_index)
159
296
  return nil unless dynamics_index
160
297
  ['pppppp', 'ppppp', 'pppp', 'ppp', 'pp', 'p', 'mp', 'mf', 'f', 'ff', 'fff'][dynamics_index.round.to_i]
@@ -1,9 +1,120 @@
1
+ # PS event processing for MusicXML export.
2
+ #
3
+ # Converts {PS} (Pitch Series) events to MusicXML dynamics markings.
4
+ # Handles crescendo, diminuendo wedges (hairpins), and static dynamics markings.
5
+ #
6
+ # ## Processing Steps
7
+ #
8
+ # 1. Extract dynamics type (:crescendo, :diminuendo, or :dynamics)
9
+ # 2. For wedges: determine if it's the start or end of the marking
10
+ # 3. Add dynamics marking at wedge start/end if level changed
11
+ # 4. Add wedge element with appropriate type and niente attribute
12
+ # 5. Track last dynamics to avoid redundant markings
13
+ #
14
+ # ## Dynamics Types Supported
15
+ #
16
+ # - **:crescendo** → crescendo wedge (hairpin opening)
17
+ # - Uses :from attribute for starting dynamics level
18
+ # - Uses :to attribute for ending dynamics level
19
+ # - Supports niente (from silence) when :from == 0
20
+ #
21
+ # - **:diminuendo** → diminuendo wedge (hairpin closing)
22
+ # - Uses :from attribute for starting dynamics level
23
+ # - Uses :to attribute for ending dynamics level
24
+ # - Supports niente (to silence) when :to == 0
25
+ #
26
+ # - **:dynamics** → static dynamics marking (pp, mf, ff, etc.)
27
+ # - Uses :from attribute for dynamics level
28
+ # - No wedge created, only dynamics text
29
+ #
30
+ # ## Dynamics Levels
31
+ #
32
+ # Dynamics levels are numeric indices (0-10) converted to standard markings:
33
+ # - 0: silence (niente)
34
+ # - 1-3: ppp range
35
+ # - 4-5: pp-p range
36
+ # - 6: mp
37
+ # - 7: mf
38
+ # - 8-9: f-ff range
39
+ # - 10: fff
40
+ #
41
+ # ## Context Tracking
42
+ #
43
+ # Uses DynamicsContext to track the last dynamics marking, preventing
44
+ # duplicate markings when consecutive events have the same level.
45
+ #
46
+ # @api private
1
47
  module Musa::Datasets::Score::ToMXML
2
48
  using Musa::Extension::InspectNice
3
49
 
50
+ # Context for tracking dynamics state across events.
51
+ #
52
+ # @api private
4
53
  DynamicsContext = Struct.new(:last_dynamics)
5
54
  private_constant :DynamicsContext
6
55
 
56
+ # Processes PS event to MusicXML dynamics marking.
57
+ #
58
+ # Converts a single PS event to one or more MusicXML dynamics/wedge elements.
59
+ # Handles crescendo/diminuendo wedges and static dynamics markings. Tracks
60
+ # context to avoid redundant markings.
61
+ #
62
+ # @param measure [Musa::MusicXML::Builder::Measure] target measure
63
+ # @param element [Hash] event hash from score query
64
+ # Contains :dataset (PS event), :change (:start/:finish for wedges)
65
+ # @param context [DynamicsContext, nil] dynamics tracking context
66
+ # @param logger [Musa::Logger::Logger] logger for debugging
67
+ # @param do_log [Boolean] enable logging
68
+ #
69
+ # @return [DynamicsContext] updated context with last dynamics
70
+ #
71
+ # @example Crescendo from pp to ff
72
+ # element_start = {
73
+ # dataset: { type: :crescendo, from: 4, to: 9, duration: 2r }.extend(Musa::Datasets::PS),
74
+ # change: :start
75
+ # }
76
+ # context = process_ps(measure, element_start, nil, logger, false)
77
+ # # Adds "pp" dynamics and crescendo wedge start
78
+ #
79
+ # element_finish = {
80
+ # dataset: { type: :crescendo, from: 4, to: 9, duration: 2r }.extend(Musa::Datasets::PS),
81
+ # change: :finish
82
+ # }
83
+ # context = process_ps(measure, element_finish, context, logger, false)
84
+ # # Adds wedge stop and "ff" dynamics
85
+ #
86
+ # @example Diminuendo to silence (niente)
87
+ # element_start = {
88
+ # dataset: { type: :diminuendo, from: 7, to: 0, duration: 1r }.extend(Musa::Datasets::PS),
89
+ # change: :start
90
+ # }
91
+ # process_ps(measure, element_start, nil, logger, false)
92
+ # # Adds "mf" dynamics and diminuendo wedge start
93
+ #
94
+ # element_finish = {
95
+ # dataset: { type: :diminuendo, from: 7, to: 0, duration: 1r }.extend(Musa::Datasets::PS),
96
+ # change: :finish
97
+ # }
98
+ # process_ps(measure, element_finish, context, logger, false)
99
+ # # Adds wedge stop with niente=true (diminuendo to silence)
100
+ #
101
+ # @example Crescendo from silence (niente)
102
+ # element_start = {
103
+ # dataset: { type: :crescendo, from: 0, to: 6, duration: 1r }.extend(Musa::Datasets::PS),
104
+ # change: :start
105
+ # }
106
+ # process_ps(measure, element_start, nil, logger, false)
107
+ # # Adds crescendo wedge with niente=true (from silence)
108
+ #
109
+ # @example Static dynamics marking
110
+ # element = {
111
+ # dataset: { type: :dynamics, from: 8, duration: 0r }.extend(Musa::Datasets::PS),
112
+ # change: :start
113
+ # }
114
+ # process_ps(measure, element, nil, logger, false)
115
+ # # Adds "f" dynamics marking only
116
+ #
117
+ # @api private
7
118
  private def process_ps(measure, element, context, logger, do_log)
8
119
  context ||= DynamicsContext.new
9
120
 
@@ -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