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,11 +1,71 @@
1
1
  module Musa::Datasets
2
2
  class Score
3
+ # Query extensions for Score result sets.
4
+ #
5
+ # Queriable provides mixins that extend query results from Score methods
6
+ # with convenient filtering, grouping, and sorting capabilities.
7
+ #
8
+ # Two result types are supported:
9
+ # - **Time slot queries**: Direct event arrays from {Score#at}
10
+ # - **Interval queries**: Result hashes from {Score#between} and {Score#changes_between}
11
+ #
12
+ # These modules are applied automatically to query results and provide
13
+ # chainable query methods for further filtering.
14
+ #
15
+ # @see Score Score class using these modules
3
16
  module Queriable
17
+ # Query methods for time slot arrays.
18
+ #
19
+ # QueryableByTimeSlot extends Arrays returned by {Score#at} with query methods.
20
+ # Each event in the array is a dataset (hash) with musical attributes.
21
+ #
22
+ # Methods access attributes directly on events.
23
+ #
24
+ # @example Group events by pitch
25
+ # events = score.at(0r) # Returns array extended with QueryableByTimeSlot
26
+ # by_pitch = events.group_by_attribute(:pitch)
27
+ # # => { 60 => [event1, event2], 64 => [event3] }
28
+ #
29
+ # @example Select events with attribute
30
+ # staccato = events.select_by_attribute(:staccato)
31
+ # # Returns events where :staccato is not nil
32
+ #
33
+ # @example Select by value
34
+ # forte = events.select_by_attribute(:velocity, 1)
35
+ # # Returns events where velocity == 1
36
+ #
37
+ # @api private
4
38
  module QueryableByTimeSlot
39
+ # Groups events by attribute value.
40
+ #
41
+ # @param attribute [Symbol] attribute to group by
42
+ #
43
+ # @return [Hash{Object => Array}] grouped events, values extended with QueryableByTimeSlot
44
+ #
45
+ # @example Group by grade
46
+ # by_grade = events.group_by_attribute(:grade)
47
+ # # => { 0 => [events with grade 0], 2 => [events with grade 2] }
5
48
  def group_by_attribute(attribute)
6
49
  group_by { |e| e[attribute] }.transform_values! { |e| e.extend(QueryableByTimeSlot) }
7
50
  end
8
51
 
52
+ # Selects events by attribute presence or value.
53
+ #
54
+ # Without value: selects events where attribute is not nil.
55
+ # With value: selects events where attribute equals value.
56
+ #
57
+ # @param attribute [Symbol] attribute to filter by
58
+ # @param value [Object, nil] optional value to match
59
+ #
60
+ # @return [Array] filtered events, extended with QueryableByTimeSlot
61
+ #
62
+ # @example Select with attribute present
63
+ # events.select_by_attribute(:staccato)
64
+ # # Events where :staccato is not nil
65
+ #
66
+ # @example Select by specific value
67
+ # events.select_by_attribute(:pitch, 60)
68
+ # # Events where pitch == 60
9
69
  def select_by_attribute(attribute, value = nil)
10
70
  if value.nil?
11
71
  select { |e| !e[attribute].nil? }
@@ -14,6 +74,17 @@ module Musa::Datasets
14
74
  end.extend(QueryableByTimeSlot)
15
75
  end
16
76
 
77
+ # Sorts events by attribute value.
78
+ #
79
+ # First filters to events with the attribute, then sorts by its value.
80
+ #
81
+ # @param attribute [Symbol] attribute to sort by
82
+ #
83
+ # @return [Array] sorted events, extended with QueryableByTimeSlot
84
+ #
85
+ # @example Sort by pitch
86
+ # sorted = events.sort_by_attribute(:pitch)
87
+ # # Events sorted by ascending pitch
17
88
  def sort_by_attribute(attribute)
18
89
  select_by_attribute(attribute).sort_by { |e| e[attribute] }.extend(QueryableByTimeSlot)
19
90
  end
@@ -21,11 +92,57 @@ module Musa::Datasets
21
92
 
22
93
  private_constant :QueryableByTimeSlot
23
94
 
95
+ # Query methods for interval query results.
96
+ #
97
+ # QueryableByDataset extends Arrays returned by {Score#between} and
98
+ # {Score#changes_between} with query methods. Each element is a hash
99
+ # containing timing info and a :dataset key with the event.
100
+ #
101
+ # Methods access attributes through the :dataset key.
102
+ #
103
+ # @example Interval query result structure
104
+ # results = score.between(0r, 4r)
105
+ # # Each result: { start: ..., finish: ..., dataset: event, ... }
106
+ #
107
+ # @example Group by pitch
108
+ # by_pitch = results.group_by_attribute(:pitch)
109
+ # # Groups by event[:dataset][:pitch]
110
+ #
111
+ # @example Select with custom condition
112
+ # high = results.subset { |event| event[:pitch] > 60 }
113
+ #
114
+ # @api private
24
115
  module QueryableByDataset
116
+ # Groups results by dataset attribute value.
117
+ #
118
+ # @param attribute [Symbol] dataset attribute to group by
119
+ #
120
+ # @return [Hash{Object => Array}] grouped results, values extended with QueryableByDataset
121
+ #
122
+ # @example Group by velocity
123
+ # by_velocity = results.group_by_attribute(:velocity)
124
+ # # => { 0 => [results with velocity 0], 1 => [results with velocity 1] }
25
125
  def group_by_attribute(attribute)
26
126
  group_by { |e| e[:dataset][attribute] }.transform_values! { |e| e.extend(QueryableByDataset) }
27
127
  end
28
128
 
129
+ # Selects results by dataset attribute presence or value.
130
+ #
131
+ # Without value: selects where dataset attribute is not nil.
132
+ # With value: selects where dataset attribute equals value.
133
+ #
134
+ # @param attribute [Symbol] dataset attribute to filter by
135
+ # @param value [Object, nil] optional value to match
136
+ #
137
+ # @return [Array] filtered results, extended with QueryableByDataset
138
+ #
139
+ # @example Select with attribute
140
+ # results.select_by_attribute(:staccato)
141
+ # # Where dataset[:staccato] is not nil
142
+ #
143
+ # @example Select by value
144
+ # results.select_by_attribute(:grade, 0)
145
+ # # Where dataset[:grade] == 0
29
146
  def select_by_attribute(attribute, value = nil)
30
147
  if value.nil?
31
148
  select { |e| !e[:dataset][attribute].nil? }
@@ -34,11 +151,36 @@ module Musa::Datasets
34
151
  end.extend(QueryableByDataset)
35
152
  end
36
153
 
154
+ # Filters results by custom condition on dataset.
155
+ #
156
+ # @yieldparam dataset [Hash] event dataset
157
+ # @yieldreturn [Boolean] true to include result
158
+ #
159
+ # @return [Array] filtered results, extended with QueryableByDataset
160
+ #
161
+ # @raise [ArgumentError] if no block given
162
+ #
163
+ # @example Filter by pitch range
164
+ # results.subset { |event| event[:pitch] > 60 && event[:pitch] < 72 }
165
+ #
166
+ # @example Filter by multiple conditions
167
+ # results.subset { |event| event[:grade] == 0 && event[:velocity] > 0 }
37
168
  def subset
38
169
  raise ArgumentError, "subset needs a block with the inclusion condition on the dataset" unless block_given?
39
170
  select { |e| yield e[:dataset] }.extend(QueryableByDataset)
40
171
  end
41
172
 
173
+ # Sorts results by dataset attribute value.
174
+ #
175
+ # First filters to results with the attribute, then sorts by its value.
176
+ #
177
+ # @param attribute [Symbol] dataset attribute to sort by
178
+ #
179
+ # @return [Array] sorted results, extended with QueryableByDataset
180
+ #
181
+ # @example Sort by start time within interval
182
+ # sorted = results.sort_by_attribute(:pitch)
183
+ # # Results sorted by ascending pitch
42
184
  def sort_by_attribute(attribute)
43
185
  select_by_attribute(attribute).sort_by { |e| e[:dataset][attribute] }.extend(QueryableByDataset)
44
186
  end
@@ -46,4 +188,4 @@ module Musa::Datasets
46
188
 
47
189
  private_constant :QueryableByDataset
48
190
  end
49
- end; end
191
+ end; end
@@ -4,7 +4,111 @@ require_relative '../../series'
4
4
  module Musa
5
5
  module Datasets
6
6
  class Score
7
+ # Real-time rendering of scores on sequencers.
8
+ #
9
+ # Render provides the {#render} method for playing back scores on a
10
+ # {Musa::Sequencer::Sequencer}. Events are scheduled at their score times
11
+ # relative to the sequencer's current position.
12
+ #
13
+ # ## Time Calculation
14
+ #
15
+ # Score times are 1-based (first beat is 1), but sequencer waits are
16
+ # 0-based. The conversion is:
17
+ #
18
+ # effective_wait = score_time - 1
19
+ #
20
+ # So score time 1 becomes wait 0 (immediate), time 2 becomes wait 1, etc.
21
+ #
22
+ # ## Nested Scores
23
+ #
24
+ # Scores can contain other scores. When a nested score is encountered,
25
+ # it's rendered recursively at the appropriate time.
26
+ #
27
+ # @example Basic rendering
28
+ # score = Musa::Datasets::Score.new
29
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
30
+ # score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
31
+ #
32
+ # seq = Musa::Sequencer::Sequencer.new(4, 24)
33
+ # score.render(on: seq) do |event|
34
+ # puts "Play #{event[:pitch]} at #{seq.position}"
35
+ # end
36
+ # seq.run
37
+ #
38
+ # @example Nested scores
39
+ # inner = Musa::Datasets::Score.new
40
+ # inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::AbsD))
41
+ # inner.at(2r, add: { pitch: 69 }.extend(Musa::Datasets::AbsD))
42
+ #
43
+ # outer = Musa::Datasets::Score.new
44
+ # outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::AbsD))
45
+ # outer.at(2r, add: inner) # Nested score
46
+ # # inner plays at sequencer time 2r
47
+ #
48
+ # @see Musa::Sequencer::Sequencer Sequencer for playback
49
+ # @see Score#at Adding events to scores
7
50
  module Render
51
+ # Renders score on sequencer.
52
+ #
53
+ # Schedules all events in the score on the sequencer, calling the block
54
+ # for each event at its scheduled time. Score times are converted to
55
+ # sequencer wait times (score_time - 1).
56
+ #
57
+ # Supports nested scores recursively.
58
+ #
59
+ # @param on [Musa::Sequencer::Sequencer] sequencer to render on
60
+ #
61
+ # @yieldparam event [Abs] each event to process
62
+ # Block is called at the scheduled time with the event dataset
63
+ #
64
+ # @return [nil]
65
+ #
66
+ # @raise [ArgumentError] if element is not Abs or Score
67
+ #
68
+ # @example MIDI output
69
+ # require 'midi-communications'
70
+ #
71
+ # score = Musa::Datasets::Score.new
72
+ # score.at(1r, add: { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV))
73
+ #
74
+ # midi_out = MIDICommunications::Output.gets
75
+ # sequencer = Musa::Sequencer::Sequencer.new(4, 24)
76
+ #
77
+ # score.render(on: sequencer) do |event|
78
+ # if event[:pitch]
79
+ # midi_out.puts(0x90, event[:pitch], event[:velocity] || 64)
80
+ # sequencer.at event[:duration] do
81
+ # midi_out.puts(0x80, event[:pitch], event[:velocity] || 64)
82
+ # end
83
+ # end
84
+ # end
85
+ #
86
+ # sequencer.run
87
+ #
88
+ # @example Console output
89
+ # score = Musa::Datasets::Score.new
90
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
91
+ #
92
+ # seq = Musa::Sequencer::Sequencer.new(4, 24)
93
+ # score.render(on: seq) do |event|
94
+ # puts "Time #{seq.position}: #{event.inspect}"
95
+ # end
96
+ # seq.run
97
+ #
98
+ # @example Nested score rendering
99
+ # inner = Musa::Datasets::Score.new
100
+ # inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::PDV))
101
+ #
102
+ # outer = Musa::Datasets::Score.new
103
+ # outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::PDV))
104
+ # outer.at(2r, add: inner)
105
+ #
106
+ # seq = Musa::Sequencer::Sequencer.new(4, 24)
107
+ # outer.render(on: seq) do |event|
108
+ # puts "Event: #{event[:pitch]}"
109
+ # end
110
+ # seq.run
111
+ # # Inner scores automatically rendered at their scheduled times
8
112
  def render(on:, &block)
9
113
  @score.keys.each do |score_at|
10
114
  effective_wait = score_at - 1r
@@ -32,4 +136,4 @@ module Musa
32
136
  end
33
137
  end
34
138
  end
35
- end
139
+ end
@@ -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