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,70 +1,254 @@
1
1
  require_relative 'dataset'
2
2
 
3
3
  module Musa::Datasets
4
+ # Base module for musical events.
5
+ #
6
+ # E (Event) is the base module for all dataset types representing musical events.
7
+ # It provides validation interface and defines the concept of "natural keys" -
8
+ # keys that are inherent to the dataset type.
9
+ #
10
+ # ## Natural Keys
11
+ #
12
+ # Each dataset type defines which keys are "natural" to it (i.e., semantically
13
+ # meaningful for that type). Keys not in NaturalKeys are considered modifiers
14
+ # or extensions.
15
+ #
16
+ # ## Validation
17
+ #
18
+ # Events can be validated to ensure they contain required keys and valid values.
19
+ # Subclasses should override {#valid?} to implement type-specific validation.
20
+ #
21
+ # @example Basic validation
22
+ # event = { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::E)
23
+ # event.valid? # => true
24
+ # event.validate! # Returns if valid, raises if not
25
+ #
26
+ # @see Abs Absolute value events
27
+ # @see Delta Delta (incremental) events
4
28
  module E
5
29
  include Dataset
6
30
 
31
+ # Natural keys for base events (empty).
32
+ # @return [Array<Symbol>]
7
33
  NaturalKeys = [].freeze
8
34
 
9
- # TODO implement valid? in all 'subclasses'. This implies recollecting from other places where validations are done and refactoring
10
- # TODO should valid? and validate! be on Dataset instead of E? P dataset inherits from Dataset but probably it could be validated
35
+ # Checks if event is valid.
11
36
  #
37
+ # Base implementation always returns true. Subclasses should override
38
+ # to implement specific validation logic.
39
+ #
40
+ # @return [Boolean] true if valid
41
+ #
42
+ # @example
43
+ # event.valid? # => true
12
44
  def valid?
13
45
  true
14
46
  end
15
47
 
48
+ # Validates event, raising if invalid.
49
+ #
50
+ # @raise [RuntimeError] if event is not valid
51
+ # @return [void]
52
+ #
53
+ # @example
54
+ # event.validate! # Raises if invalid
16
55
  def validate!
17
56
  raise RuntimeError, "Invalid dataset #{self}" unless valid?
18
57
  end
19
58
  end
20
59
 
60
+ # Events with absolute values.
61
+ #
62
+ # Abs (Absolute) represents events where all values are absolute (not relative).
63
+ # Examples: actual MIDI pitch 60, duration 1.0 seconds, velocity 64.
64
+ #
65
+ # Contrast with {Delta} where values are incremental.
66
+ #
67
+ # @see Delta Incremental events
68
+ # @see AbsI Absolute indexed (arrays)
69
+ # @see AbsTimed Absolute with time
70
+ # @see AbsD Absolute with duration
21
71
  module Abs
22
72
  include E
23
73
  end
24
74
 
75
+ # Events with delta (incremental) values.
76
+ #
77
+ # Delta represents events where values are incremental changes from a previous
78
+ # state. Examples: pitch +2 semitones, duration +0.5 beats, velocity -10.
79
+ #
80
+ # Delta encoding is efficient for sequences where consecutive events have
81
+ # similar values.
82
+ #
83
+ # @example Delta vs Absolute
84
+ # # Absolute encoding (3 events)
85
+ # { pitch: 60, duration: 1.0 }
86
+ # { pitch: 62, duration: 1.0 }
87
+ # { pitch: 64, duration: 1.0 }
88
+ #
89
+ # # Delta encoding (same 3 events)
90
+ # { abs_pitch: 60, abs_duration: 1.0 } # First event absolute
91
+ # { delta_pitch: +2 } # Duration unchanged
92
+ # { delta_pitch: +2 } # Duration unchanged
93
+ #
94
+ # @see Abs Absolute events
95
+ # @see DeltaD Delta with duration
25
96
  module Delta
26
97
  include E
27
98
  end
28
99
 
100
+ # Absolute indexed events (array-based).
101
+ #
102
+ # AbsI represents absolute events stored in indexed structures (arrays).
103
+ # Used by {V} and {PackedV} modules.
104
+ #
105
+ # @see Abs Parent absolute module
106
+ # @see V Value arrays
107
+ # @see PackedV Packed value hashes
29
108
  module AbsI
30
109
  include Abs
31
110
  end
32
111
 
112
+ # Absolute events with time component.
113
+ #
114
+ # AbsTimed represents absolute events that occur at a specific time point.
115
+ # The `:time` key indicates when the event occurs.
116
+ #
117
+ # ## Natural Keys
118
+ #
119
+ # - **:time**: Absolute time position
120
+ #
121
+ # @example Timed event
122
+ # { time: 0.0, value: { pitch: 60 } }.extend(AbsTimed)
123
+ # { time: 1.0, value: { pitch: 64 } }.extend(AbsTimed)
124
+ #
125
+ # @see Abs Parent absolute module
126
+ # @see P Pitch series (produces AbsTimed)
33
127
  module AbsTimed
34
128
  include Abs
35
129
 
130
+ # Natural keys including time.
131
+ # @return [Array<Symbol>]
36
132
  NaturalKeys = (NaturalKeys + [:time]).freeze
37
133
  end
38
134
 
135
+ # Delta indexed events (array-based deltas).
136
+ #
137
+ # DeltaI represents delta events stored in indexed structures.
138
+ #
139
+ # @see Delta Parent delta module
39
140
  module DeltaI
40
141
  include Delta
41
142
  end
42
143
 
144
+ # Absolute events with duration.
145
+ #
146
+ # AbsD represents absolute events that have duration - they occupy a time span
147
+ # rather than occurring at a single instant.
148
+ #
149
+ # ## Natural Keys
150
+ #
151
+ # - **:duration**: Total duration of the event process
152
+ # - **:note_duration**: Actual note duration (may differ for staccato, etc.)
153
+ # - **:forward_duration**: Time until next event (may be 0 for simultaneous events)
154
+ #
155
+ # ## Duration Types
156
+ #
157
+ # **duration**: How long the event process lasts (note playing, dynamics change, etc.)
158
+ #
159
+ # **note_duration**: Actual note length. For staccato, this is shorter than duration.
160
+ # Defaults to duration if not specified.
161
+ #
162
+ # **forward_duration**: Time to wait before next event. Can be:
163
+ #
164
+ # - Same as duration (default): next event starts when this one ends
165
+ # - Less than duration: events overlap
166
+ # - Zero: next event starts simultaneously
167
+ # - More than duration: gap/rest before next event
168
+ #
169
+ # @example Basic duration
170
+ # { pitch: 60, duration: 1.0 }.extend(AbsD)
171
+ # event.duration # => 1.0
172
+ # event.note_duration # => 1.0 (defaults to duration)
173
+ # event.forward_duration # => 1.0 (defaults to duration)
174
+ #
175
+ # @example Staccato note
176
+ # { pitch: 60, duration: 1.0, note_duration: 0.5 }.extend(AbsD)
177
+ # # Note sounds for 0.5, but next event waits 1.0
178
+ #
179
+ # @example Simultaneous events
180
+ # { pitch: 60, duration: 1.0, forward_duration: 0 }.extend(AbsD)
181
+ # # Next event starts immediately (chord)
182
+ #
183
+ # @see Abs Parent absolute module
184
+ # @see PS Pitch series with duration
185
+ # @see PDV Pitch/Duration/Velocity
186
+ # @see GDV Grade/Duration/Velocity
43
187
  module AbsD
44
188
  include Abs
45
189
 
190
+ # Natural keys including duration variants.
191
+ # @return [Array<Symbol>]
46
192
  NaturalKeys = (NaturalKeys +
47
193
  [:duration, # duration of the process (note reproduction, dynamics evolution, etc)
48
194
  :note_duration, # duration of the note (a staccato note is effectively shorter than elapsed duration until next note)
49
195
  :forward_duration # duration to wait until next event (if 0 means the next event should be executed at the same time than this one)
50
196
  ]).freeze
51
197
 
198
+ # Returns forward duration (time until next event).
199
+ #
200
+ # Defaults to `:duration` if `:forward_duration` not specified.
201
+ #
202
+ # @return [Numeric] forward duration
203
+ #
204
+ # @example
205
+ # event.forward_duration # => 1.0
52
206
  def forward_duration
53
207
  self[:forward_duration] || self[:duration]
54
208
  end
55
209
 
210
+ # Returns actual note duration.
211
+ #
212
+ # Defaults to `:duration` if `:note_duration` not specified.
213
+ #
214
+ # @return [Numeric] note duration
215
+ #
216
+ # @example
217
+ # event.note_duration # => 0.5 (staccato)
56
218
  def note_duration
57
219
  self[:note_duration] || self[:duration]
58
220
  end
59
221
 
222
+ # Returns event duration.
223
+ #
224
+ # @return [Numeric] duration
225
+ #
226
+ # @example
227
+ # event.duration # => 1.0
60
228
  def duration
61
229
  self[:duration]
62
230
  end
63
231
 
232
+ # Checks if thing can be converted to AbsD.
233
+ #
234
+ # @param thing [Object] object to check
235
+ # @return [Boolean] true if compatible
236
+ #
237
+ # @example
238
+ # AbsD.is_compatible?({ duration: 1.0 }) # => true
239
+ # AbsD.is_compatible?({ pitch: 60 }) # => false
64
240
  def self.is_compatible?(thing)
65
241
  thing.is_a?(AbsD) || thing.is_a?(Hash) && thing.has_key?(:duration)
66
242
  end
67
243
 
244
+ # Converts thing to AbsD if possible.
245
+ #
246
+ # @param thing [Object] object to convert
247
+ # @return [AbsD] AbsD dataset
248
+ # @raise [ArgumentError] if thing cannot be converted
249
+ #
250
+ # @example
251
+ # AbsD.to_AbsD({ duration: 1.0 }) # => AbsD dataset
68
252
  def self.to_AbsD(thing)
69
253
  if thing.is_a?(AbsD)
70
254
  thing
@@ -5,6 +5,136 @@ require_relative 'pdv'
5
5
  require_relative 'helper'
6
6
 
7
7
  module Musa::Datasets
8
+ # Score-style musical events with scale degrees.
9
+ #
10
+ # GDV (Grade/Duration/Velocity) represents musical events using score notation
11
+ # with scale degrees, octaves, and dynamics. Extends {AbsD} for duration support.
12
+ #
13
+ # ## Purpose
14
+ #
15
+ # GDV is the score representation layer of the dataset framework:
16
+ #
17
+ # - Uses scale degrees (grade) instead of absolute pitches
18
+ # - Uses dynamics markings (velocity -5 to +4) instead of MIDI velocity
19
+ # - Human-readable and musically meaningful
20
+ # - Independent of specific tuning or scale
21
+ #
22
+ # Contrast with {PDV} which uses MIDI absolute pitches and velocities.
23
+ #
24
+ # ## Natural Keys
25
+ #
26
+ # - **:grade**: Scale degree (integer, 0-based)
27
+ # - **:sharps**: Chromatic alteration (integer, positive = sharp, negative = flat)
28
+ # - **:octave**: Octave offset (integer, 0 = base octave)
29
+ # - **:velocity**: Dynamics (-3 to +4, where 0 = mp, 1 = mf)
30
+ # - **:silence**: Indicates rest (boolean or symbol)
31
+ # - **:duration**: Event duration (from {AbsD})
32
+ # - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
33
+ #
34
+ # ## Pitch Representation
35
+ #
36
+ # Pitches are specified as:
37
+ #
38
+ # - **grade**: Position in scale (0 = first note, 1 = second note, etc.)
39
+ # - **octave**: Octave offset (0 = base, 1 = up one octave, -1 = down one octave)
40
+ # - **sharps**: Chromatic alteration (1 = sharp, -1 = flat, 2 = double sharp, etc.)
41
+ #
42
+ # Example in C major scale:
43
+ #
44
+ # - C4 = { grade: 0, octave: 0 }
45
+ # - D4 = { grade: 1, octave: 0 }
46
+ # - C5 = { grade: 0, octave: 1 }
47
+ # - C#4 = { grade: 0, octave: 0, sharps: 1 }
48
+ #
49
+ # ## Velocity (Dynamics)
50
+ #
51
+ # Velocity represents musical dynamics in range -3 to +4:
52
+ #
53
+ # -3: ppp (pianississimo)
54
+ # -2: pp (pianissimo)
55
+ # -1: p (piano)
56
+ # 0: mp (mezzo-piano)
57
+ # +1: mf (mezzo-forte)
58
+ # +2: f (forte)
59
+ # +3: ff (fortissimo)
60
+ # +4: fff (fortississimo)
61
+ #
62
+ # ## Conversions
63
+ #
64
+ # ### To PDV (MIDI)
65
+ #
66
+ # Converts score notation to MIDI using a scale:
67
+ #
68
+ # gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
69
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
70
+ # pdv = gdv.to_pdv(scale)
71
+ # # => { pitch: 60, duration: 1.0, velocity: 64 }
72
+ #
73
+ # ### To GDVd (Delta Encoding)
74
+ #
75
+ # Converts to delta encoding for efficient storage:
76
+ #
77
+ # gdv1 = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
78
+ # gdv2 = { grade: 2, octave: 0, duration: 1.0, velocity: 1 }.extend(GDV)
79
+ # gdvd = gdv2.to_gdvd(scale, previous: gdv1)
80
+ # # => { delta_grade: 2, delta_velocity: 1 }
81
+ #
82
+ # ### To Neuma Notation
83
+ #
84
+ # Converts to Neuma string format for serialization:
85
+ #
86
+ # gdv = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
87
+ # gdv.base_duration = 1/4r
88
+ # gdv.to_neuma # => "(0 4 mp)"
89
+ #
90
+ # ## MIDI Velocity Mapping
91
+ #
92
+ # Dynamics are mapped to MIDI velocities using interpolation:
93
+ #
94
+ # -3 (ppp) → 16
95
+ # -2 (pp) → 33
96
+ # -1 (p) → 49
97
+ # 0 (mp) → 64
98
+ # +1 (mf) → 80
99
+ # +2 (f) → 96
100
+ # +3 (ff) → 112
101
+ # +4 (fff) → 127
102
+ #
103
+ # @example Basic score event
104
+ # gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(Musa::Datasets::GDV)
105
+ # gdv.base_duration = 1/4r
106
+ # # First scale degree, base octave, 1 beat, mp dynamics
107
+ #
108
+ # @example Chromatic alteration
109
+ # gdv = { grade: 0, octave: 0, sharps: 1, duration: 1.0 }.extend(GDV)
110
+ # # First scale degree sharp (C# in C major)
111
+ #
112
+ # @example Silence (rest)
113
+ # gdv = { grade: :silence, duration: 1.0 }.extend(GDV)
114
+ # # Rest for 1 beat
115
+ #
116
+ # @example Convert to MIDI
117
+ # gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
118
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
119
+ # pdv = gdv.to_pdv(scale)
120
+ # # => { pitch: 60, duration: 1.0, velocity: 64 }
121
+ #
122
+ # @example Convert to delta encoding
123
+ # gdv1 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
124
+ # gdv2 = { grade: 2, duration: 1.0, velocity: 1 }.extend(GDV)
125
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
126
+ # gdvd = gdv2.to_gdvd(scale, previous: gdv1)
127
+ # # => { delta_grade: 2, delta_velocity: 1 }
128
+ #
129
+ # @example Convert to Neuma notation
130
+ # gdv = { grade: 0, octave: 1, duration: 1.0, velocity: 2 }.extend(GDV)
131
+ # gdv.base_duration = 1/4r
132
+ # gdv.to_neuma # => "(0 o1 4 ff)"
133
+ #
134
+ # @see PDV MIDI-style representation
135
+ # @see GDVd Delta encoding
136
+ # @see AbsD Absolute duration events
137
+ # @see Helper String formatting utilities
8
138
  module GDV
9
139
  using Musa::Extension::InspectNice
10
140
 
@@ -12,14 +142,60 @@ module Musa::Datasets
12
142
 
13
143
  include Helper
14
144
 
145
+ # Natural keys for score events.
146
+ # @return [Array<Symbol>]
15
147
  NaturalKeys = (NaturalKeys + [:grade, :sharps, :octave, :velocity, :silence]).freeze
16
148
 
149
+ # Base duration for time calculations.
150
+ # @return [Rational]
17
151
  attr_accessor :base_duration
18
152
 
153
+ # MIDI velocity mapping for dynamics.
154
+ #
155
+ # Maps dynamics values (-5 to +4) to MIDI velocities (0-127).
156
+ # Used for interpolation in {#to_pdv}.
157
+ #
158
+ # @return [Array<Integer>] MIDI velocity breakpoints
159
+ # @api private
19
160
  # TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
20
161
  # ppp = 16 ... fff = 127 (-5 ... 4) the standard used by Musescore 3 and others starts at ppp = 16
21
162
  VELOCITY_MAP = [1, 8, 16, 33, 49, 64, 80, 96, 112, 127].freeze
22
163
 
164
+ # Converts to PDV (MIDI representation).
165
+ #
166
+ # Translates score notation to MIDI using a scale:
167
+ # - Scale degree → MIDI pitch (via scale lookup)
168
+ # - Dynamics → MIDI velocity (via interpolation)
169
+ # - Duration values copied
170
+ # - Additional keys preserved
171
+ #
172
+ # @param scale [Musa::Scales::Scale] reference scale for pitch conversion
173
+ #
174
+ # @return [PDV] MIDI representation dataset
175
+ #
176
+ # @example Basic conversion
177
+ # gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
178
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
179
+ # pdv = gdv.to_pdv(scale)
180
+ # # => { pitch: 60, duration: 1.0, velocity: 64 }
181
+ #
182
+ # @example Chromatic note
183
+ # gdv = { grade: 0, octave: 0, sharps: 1, duration: 1.0 }.extend(GDV)
184
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
185
+ # pdv = gdv.to_pdv(scale)
186
+ # # => { pitch: 61, duration: 1.0 }
187
+ #
188
+ # @example Silence
189
+ # gdv = { grade: :silence, duration: 1.0 }.extend(GDV)
190
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
191
+ # pdv = gdv.to_pdv(scale)
192
+ # # => { pitch: :silence, duration: 1.0 }
193
+ #
194
+ # @example Dynamics interpolation
195
+ # gdv = { grade: 0, velocity: 0.5 }.extend(GDV)
196
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
197
+ # pdv = gdv.to_pdv(scale)
198
+ # # velocity 0.5 interpolates between mf (64) and f (80)
23
199
  def to_pdv(scale)
24
200
  pdv = {}.extend PDV
25
201
  pdv.base_duration = @base_duration
@@ -50,7 +226,7 @@ module Musa::Datasets
50
226
  else
51
227
  self[:velocity] < -5 ? -5 : 4
52
228
  end
53
-
229
+
54
230
  index_min = index.floor
55
231
  index_max = index.ceil
56
232
 
@@ -65,6 +241,51 @@ module Musa::Datasets
65
241
  pdv
66
242
  end
67
243
 
244
+ # Converts to Neuma notation string.
245
+ #
246
+ # Neuma is a compact text format for score notation. Format:
247
+ #
248
+ # (grade[sharps] [octave] [duration] [velocity] [modifiers...])
249
+ #
250
+ # - **grade**: Scale degree number (0, 1, 2...) or 'silence' for rests
251
+ # - **sharps**: '#' for sharp, '_' for flat (e.g., "0#" = first degree sharp)
252
+ # - **octave**: 'o' + number (e.g., "o1" = up one octave, "o-1" = down one)
253
+ # - **duration**: Number of base_duration units
254
+ # - **velocity**: Dynamics string (ppp, pp, p, mp, mf, f, ff, fff)
255
+ # - **modifiers**: Additional key-value pairs (e.g., "staccato")
256
+ #
257
+ # @return [String] Neuma notation
258
+ #
259
+ # @example Basic note
260
+ # gdv = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
261
+ # gdv.base_duration = 1/4r
262
+ # gdv.to_neuma # => "(0 4 mf)"
263
+ # # grade 0, duration 4 quarters, mf dynamics
264
+ #
265
+ # @example With octave
266
+ # gdv = { grade: 2, octave: 1, duration: 0.5, velocity: 2 }.extend(GDV)
267
+ # gdv.base_duration = 1/4r
268
+ # gdv.to_neuma # => "(2 o1 2 ff)"
269
+ #
270
+ # @example Sharp note
271
+ # gdv = { grade: 0, sharps: 1, duration: 1.0 }.extend(GDV)
272
+ # gdv.base_duration = 1/4r
273
+ # gdv.to_neuma # => "(0# 4)"
274
+ #
275
+ # @example Flat note
276
+ # gdv = { grade: 1, sharps: -1, duration: 1.0 }.extend(GDV)
277
+ # gdv.base_duration = 1/4r
278
+ # gdv.to_neuma # => "(1_ 4)"
279
+ #
280
+ # @example Silence
281
+ # gdv = { grade: :silence, duration: 1.0 }.extend(GDV)
282
+ # gdv.base_duration = 1/4r
283
+ # gdv.to_neuma # => "(silence 4)"
284
+ #
285
+ # @example With modifiers
286
+ # gdv = { grade: 0, duration: 1.0, staccato: true }.extend(GDV)
287
+ # gdv.base_duration = 1/4r
288
+ # gdv.to_neuma # => "(0 4 staccato)"
68
289
  def to_neuma
69
290
  @base_duration ||= Rational(1, 4)
70
291
 
@@ -80,7 +301,7 @@ module Musa::Datasets
80
301
  if self[:sharps] > 0
81
302
  attributes[c] += '#' * self[:sharps]
82
303
  elsif self[:sharps] < 0
83
- attributes[c] += '_' * self[:sharps]
304
+ attributes[c] += '_' * self[:sharps].abs
84
305
  end
85
306
  end
86
307
  end
@@ -98,12 +319,68 @@ module Musa::Datasets
98
319
  '(' + attributes.join(' ') + ')'
99
320
  end
100
321
 
322
+ # Converts velocity number to dynamics string.
323
+ #
324
+ # Maps numeric velocity (-3 to +4) to standard dynamics markings.
325
+ #
326
+ # @param x [Integer] velocity value
327
+ # @return [String] dynamics marking
328
+ #
329
+ # @example
330
+ # velocity_of(-3) # => "ppp"
331
+ # velocity_of(0) # => "mp"
332
+ # velocity_of(1) # => "mf"
333
+ # velocity_of(4) # => "fff"
334
+ #
335
+ # @api private
101
336
  def velocity_of(x)
102
337
  %w[ppp pp p mp mf f ff fff][x + 3]
103
338
  end
104
339
 
105
340
  private :velocity_of
106
341
 
342
+ # Converts to GDVd (delta encoding).
343
+ #
344
+ # Creates delta-encoded representation relative to a previous event.
345
+ # Only changed values are included, making the representation compact.
346
+ #
347
+ # Without previous event (first in sequence):
348
+ # - Uses abs_ keys for all values
349
+ #
350
+ # With previous event:
351
+ # - Uses delta_ keys for changed values
352
+ # - Omits unchanged values
353
+ # - Uses abs_ keys when changing from nil to value
354
+ #
355
+ # @param scale [Musa::Scales::Scale] reference scale for grade calculation
356
+ # @param previous [GDV, nil] previous event for delta calculation
357
+ #
358
+ # @return [GDVd] delta-encoded dataset
359
+ #
360
+ # @example First event (no previous)
361
+ # gdv = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
362
+ # gdvd = gdv.to_gdvd(scale)
363
+ # # => { abs_grade: 0, abs_duration: 1.0, abs_velocity: 0 }
364
+ #
365
+ # @example Changed values
366
+ # gdv1 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
367
+ # gdv2 = { grade: 2, duration: 1.0, velocity: 1 }.extend(GDV)
368
+ # gdvd = gdv2.to_gdvd(scale, previous: gdv1)
369
+ # # => { delta_grade: 2, delta_velocity: 1 }
370
+ # # duration unchanged, so omitted
371
+ #
372
+ # @example Unchanged values
373
+ # gdv1 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
374
+ # gdv2 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
375
+ # gdvd = gdv2.to_gdvd(scale, previous: gdv1)
376
+ # # => {}
377
+ # # Everything unchanged
378
+ #
379
+ # @example Chromatic alteration
380
+ # gdv1 = { grade: 0, octave: 0 }.extend(GDV)
381
+ # gdv2 = { grade: 0, octave: 0, sharps: 1 }.extend(GDV)
382
+ # gdvd = gdv2.to_gdvd(scale, previous: gdv1)
383
+ # # => { delta_sharps: 1 }
107
384
  def to_gdvd(scale, previous: nil)
108
385
  gdvd = {}.extend GDVd
109
386
  gdvd.base_duration = @base_duration