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
@@ -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
@@ -5,6 +5,117 @@ require_relative 'helper'
5
5
 
6
6
 
7
7
  module Musa::Datasets
8
+ # Delta-encoded score events for efficient compression.
9
+ #
10
+ # GDVd (Grade/Duration/Velocity delta) represents musical events using
11
+ # delta encoding - storing only changes from previous events.
12
+ # Extends {DeltaD} for flexible duration encoding and {DeltaI} for indexed deltas.
13
+ #
14
+ # ## Purpose
15
+ #
16
+ # GDVd provides efficient delta encoding for musical sequences:
17
+ #
18
+ # - **Compact storage**: Only changed values are stored
19
+ # - **Efficient serialization**: Neuma format uses delta notation
20
+ # - **Lossless compression**: Full reconstruction via {#to_gdv}
21
+ # - **Musical patterns**: Captures relative motion (intervals, velocity changes)
22
+ #
23
+ # ## Encoding Types
24
+ #
25
+ # Each parameter can be encoded as absolute or delta:
26
+ #
27
+ # ### Pitch Encoding
28
+ #
29
+ # **Absolute**:
30
+ #
31
+ # - **abs_grade**: Set grade to specific value
32
+ # - **abs_sharps**: Set chromatic alteration
33
+ # - **abs_octave**: Set octave to specific value
34
+ #
35
+ # **Delta**:
36
+ #
37
+ # - **delta_grade**: Change grade by semitones
38
+ # - **delta_sharps**: Change chromatic alteration
39
+ # - **delta_interval**: Change by scale interval (with delta_interval_sign)
40
+ # - **delta_octave**: Change octave
41
+ #
42
+ # ### Duration Encoding (from {DeltaD})
43
+ #
44
+ # - **abs_duration**: Set duration
45
+ # - **delta_duration**: Add to duration
46
+ # - **factor_duration**: Multiply duration
47
+ #
48
+ # ### Velocity Encoding
49
+ #
50
+ # - **abs_velocity**: Set velocity
51
+ # - **delta_velocity**: Add to velocity
52
+ #
53
+ # ## Natural Keys
54
+ #
55
+ # - **:abs_grade**, **:abs_sharps**, **:abs_octave**: Absolute pitch
56
+ # - **:delta_grade**, **:delta_sharps**, **:delta_interval**, **:delta_interval_sign**, **:delta_octave**: Delta pitch
57
+ # - **:abs_velocity**, **:delta_velocity**: Velocity encoding
58
+ # - **:abs_duration**, **:delta_duration**, **:factor_duration**: Duration encoding
59
+ # - **:modifiers**: Hash of additional modifiers
60
+ #
61
+ # ## Reconstruction
62
+ #
63
+ # Delta events require a previous event for reconstruction:
64
+ #
65
+ # gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
66
+ # previous = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
67
+ # gdv = gdvd.to_gdv(scale, previous: previous)
68
+ # # => { grade: 2, octave: 0, duration: 1.0, velocity: 1 }
69
+ #
70
+ # ## Neuma Delta Notation
71
+ #
72
+ # Delta events use special notation in Neuma format:
73
+ #
74
+ # - **Delta grade**: "+2" or "-2" (semitone change)
75
+ # - **Delta sharps**: "+#" or "-_" (chromatic change)
76
+ # - **Delta octave**: "+o1" or "-o1" (octave change)
77
+ # - **Delta duration**: "+0.5" or "-0.5" (duration change)
78
+ # - **Factor duration**: "*2" or "*0.5" (duration multiply)
79
+ # - **Delta velocity**: "+f" or "-f" (dynamics change)
80
+ #
81
+ # @example First event (absolute encoding)
82
+ # gdvd = { abs_grade: 0, abs_duration: 1.0, abs_velocity: 0 }.extend(GDVd)
83
+ # gdvd.base_duration = 1/4r
84
+ # gdvd.to_neuma # => "(0 4 mp)"
85
+ #
86
+ # @example Delta encoding (unchanged duration)
87
+ # gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
88
+ # gdvd.base_duration = 1/4r
89
+ # gdvd.to_neuma # => "(+2 +f)"
90
+ # # Grade +2 semitones, velocity +1 (one f louder)
91
+ #
92
+ # @example Chromatic change
93
+ # gdvd = { delta_sharps: 1 }.extend(GDVd)
94
+ # gdvd.to_neuma # => "(+#)"
95
+ # # Add one sharp
96
+ #
97
+ # @example Duration multiplication
98
+ # gdvd = { factor_duration: 2 }.extend(GDVd)
99
+ # gdvd.base_duration = 1/4r
100
+ # gdvd.to_neuma # => "(. *2)"
101
+ # # Double duration
102
+ #
103
+ # @example Reconstruction from delta
104
+ # previous = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
105
+ # gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
106
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
107
+ # gdv = gdvd.to_gdv(scale, previous: previous)
108
+ # # => { grade: 2, octave: 0, duration: 1.0, velocity: 1 }
109
+ #
110
+ # @example Octave change
111
+ # gdvd = { delta_grade: -2, delta_octave: 1 }.extend(GDVd)
112
+ # gdvd.to_neuma # => "(-2 +o1)"
113
+ # # Down 2 semitones, up one octave
114
+ #
115
+ # @see GDV Absolute score notation
116
+ # @see DeltaD Delta duration encoding
117
+ # @see DeltaI Delta indexed encoding
118
+ # @see Helper String formatting utilities
8
119
  module GDVd
9
120
  include DeltaD
10
121
  include DeltaI
@@ -13,14 +124,28 @@ module Musa::Datasets
13
124
 
14
125
  using Musa::Extension::InspectNice
15
126
 
127
+ # Natural keys for delta encoding.
128
+ # @return [Array<Symbol>]
16
129
  NaturalKeys = (NaturalKeys +
17
130
  [:abs_grade, :abs_sharps, :abs_octave,
18
131
  :delta_grade, :delta_sharps, :delta_interval_sign, :delta_interval, :delta_octave,
19
132
  :abs_velocity, :delta_velocity,
20
133
  :modifiers]).freeze
21
134
 
135
+ # Base duration for time calculations.
136
+ # @return [Rational]
22
137
  attr_reader :base_duration
23
138
 
139
+ # Sets base duration, adjusting existing duration values.
140
+ #
141
+ # When base_duration changes, existing abs_duration and delta_duration
142
+ # are scaled proportionally to maintain actual time values.
143
+ #
144
+ # @param value [Rational] new base duration
145
+ #
146
+ # @example
147
+ # gdvd[:abs_duration] = 1.0
148
+ # gdvd.base_duration = 1/4r # abs_duration scaled by factor
24
149
  def base_duration=(value)
25
150
  factor = value / (@base_duration || 1)
26
151
  @base_duration = value
@@ -29,6 +154,33 @@ module Musa::Datasets
29
154
  self[:delta_duration] *= factor if has_key?(:delta_duration)
30
155
  end
31
156
 
157
+ # Reconstructs absolute GDV from delta encoding.
158
+ #
159
+ # Applies delta changes to previous event to create new absolute event.
160
+ # Handles all encoding types (abs_, delta_, factor_) appropriately.
161
+ #
162
+ # @param scale [Musa::Scales::Scale] reference scale for pitch calculations
163
+ # @param previous [GDV] previous absolute event (required for reconstruction)
164
+ #
165
+ # @return [GDV] reconstructed absolute event
166
+ #
167
+ # @example Basic delta reconstruction
168
+ # previous = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
169
+ # gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
170
+ # gdv = gdvd.to_gdv(scale, previous: previous)
171
+ # # => { grade: 2, octave: 0, duration: 1.0, velocity: 1 }
172
+ #
173
+ # @example Absolute override
174
+ # previous = { grade: 0, duration: 1.0 }.extend(GDV)
175
+ # gdvd = { abs_grade: 5, abs_duration: 2.0 }.extend(GDVd)
176
+ # gdv = gdvd.to_gdv(scale, previous: previous)
177
+ # # => { grade: 5, duration: 2.0 }
178
+ #
179
+ # @example Duration factor
180
+ # previous = { grade: 0, duration: 1.0 }.extend(GDV)
181
+ # gdvd = { factor_duration: 2 }.extend(GDVd)
182
+ # gdv = gdvd.to_gdv(scale, previous: previous)
183
+ # # => { grade: 0, duration: 2.0 }
32
184
  def to_gdv(scale, previous:)
33
185
  r = previous.clone.delete_if {|k,_| !GDV::NaturalKeys.include?(k)}.extend GDV
34
186
 
@@ -109,6 +261,18 @@ module Musa::Datasets
109
261
  r
110
262
  end
111
263
 
264
+ # Normalizes chromatic pitch to scale note.
265
+ #
266
+ # Converts arbitrary grade + sharps to closest scale note representation.
267
+ # If chromatic, returns background note + chromatic alteration.
268
+ #
269
+ # @param scale [Musa::Scales::Scale] reference scale
270
+ # @param grade [Integer] scale degree (wide grade)
271
+ # @param sharps [Integer] chromatic alteration
272
+ #
273
+ # @return [Array(Integer, Integer)] [normalized_grade, normalized_sharps]
274
+ #
275
+ # @api private
112
276
  private def normalize_to_scale(scale, grade, sharps)
113
277
  note = scale[grade].sharp(sharps)
114
278
  background = note.background_note
@@ -120,6 +284,43 @@ module Musa::Datasets
120
284
  end
121
285
  end
122
286
 
287
+ # Converts to Neuma delta notation string.
288
+ #
289
+ # Neuma delta format uses special notation for changes:
290
+ #
291
+ # ([grade_delta] [octave_delta] [duration_delta] [velocity_delta] [modifiers...])
292
+ #
293
+ # - **Grade delta**: "+2" or "-2" (semitone change)
294
+ # - **Sharp delta**: "+#" or "-_" (chromatic change)
295
+ # - **Octave delta**: "+o1" or "-o1" (octave change)
296
+ # - **Duration delta**: "+0.5", "-0.5", or "*2" (duration change)
297
+ # - **Velocity delta**: "+f" or "-f" (dynamics change by f's)
298
+ #
299
+ # @return [String] Neuma delta notation
300
+ #
301
+ # @example Delta grade
302
+ # gdvd = { delta_grade: 2 }.extend(GDVd)
303
+ # gdvd.base_duration = 1/4r
304
+ # gdvd.to_neuma # => "(+2)"
305
+ #
306
+ # @example Multiple deltas
307
+ # gdvd = { delta_grade: -2, delta_velocity: 1 }.extend(GDVd)
308
+ # gdvd.base_duration = 1/4r
309
+ # gdvd.to_neuma # => "(-2 +f)"
310
+ #
311
+ # @example Duration factor
312
+ # gdvd = { factor_duration: 2 }.extend(GDVd)
313
+ # gdvd.base_duration = 1/4r
314
+ # gdvd.to_neuma # => "(. *2)"
315
+ #
316
+ # @example Chromatic change
317
+ # gdvd = { delta_sharps: 1 }.extend(GDVd)
318
+ # gdvd.to_neuma # => "(+#)"
319
+ #
320
+ # @example Absolute values
321
+ # gdvd = { abs_grade: 0, abs_duration: 1.0 }.extend(GDVd)
322
+ # gdvd.base_duration = 1/4r
323
+ # gdvd.to_neuma # => "(0 4)"
123
324
  def to_neuma
124
325
  @base_duration ||= Rational(1,4)
125
326
 
@@ -1,19 +1,86 @@
1
1
  module Musa::Datasets
2
+ # Helper utilities for dataset formatting and string generation.
3
+ #
4
+ # Helper provides utility methods for converting datasets to string
5
+ # representations, particularly for the Neuma notation format.
6
+ #
7
+ # These methods handle:
8
+ #
9
+ # - Sign formatting (+/-)
10
+ # - Velocity to dynamics conversion
11
+ # - Modifier parameter formatting
12
+ #
13
+ # @api private
2
14
  module Helper
3
15
  private
4
16
 
17
+ # Returns '+' for non-negative numbers, empty string for negative.
18
+ #
19
+ # Used for formatting delta values in Neuma notation.
20
+ #
21
+ # @param x [Numeric] number to check
22
+ # @return [String] '+' or ''
23
+ #
24
+ # @example
25
+ # positive_sign_of(5) # => '+'
26
+ # positive_sign_of(-3) # => ''
27
+ #
28
+ # @api private
5
29
  def positive_sign_of(x)
6
30
  x >= 0 ? '+' : ''
7
31
  end
8
32
 
33
+ # Returns '+', '+', or '-' based on number's sign.
34
+ #
35
+ # @param x [Numeric] number to check
36
+ # @return [String] '+' (positive), '+' (zero), or '-' (negative)
37
+ #
38
+ # @example
39
+ # sign_of(5) # => '+'
40
+ # sign_of(0) # => '+'
41
+ # sign_of(-3) # => '-'
42
+ #
43
+ # @api private
9
44
  def sign_of(x)
10
45
  '++-'[x <=> 0]
11
46
  end
12
47
 
48
+ # Converts numeric velocity to dynamics marking.
49
+ #
50
+ # Maps velocity values (-5 to +4) to standard dynamics markings.
51
+ # Range: ppp (-5) to fff (+4), centered at mf (0).
52
+ #
53
+ # @param x [Integer] velocity value
54
+ # @return [String] dynamics marking
55
+ #
56
+ # @example
57
+ # velocity_of(-5) # => 'ppp'
58
+ # velocity_of(0) # => 'mf'
59
+ # velocity_of(4) # => 'fff'
60
+ #
61
+ # @api private
13
62
  def velocity_of(x)
14
63
  %w[ppp pp p mp mf f ff fff][x + 3]
15
64
  end
16
65
 
66
+ # Formats modifier with parameters for Neuma notation.
67
+ #
68
+ # Converts modifier keys and their parameters into Neuma string format.
69
+ #
70
+ # @param modificator [Symbol] modifier key name
71
+ # @param parameter_or_parameters [Boolean, Array, Object] modifier parameters
72
+ # @return [String] formatted modifier string
73
+ #
74
+ # @example Boolean modifier (flag)
75
+ # modificator_string(:staccato, true) # => 'staccato'
76
+ #
77
+ # @example Single parameter
78
+ # modificator_string(:pedal, 'down') # => 'pedal("down")'
79
+ #
80
+ # @example Multiple parameters
81
+ # modificator_string(:bend, [2, 'up']) # => 'bend(2, "up")'
82
+ #
83
+ # @api private
17
84
  def modificator_string(modificator, parameter_or_parameters)
18
85
  case parameter_or_parameters
19
86
  when true
@@ -27,6 +94,14 @@ module Musa::Datasets
27
94
 
28
95
  private
29
96
 
97
+ # Converts parameter to string representation.
98
+ #
99
+ # Handles different parameter types for Neuma notation.
100
+ #
101
+ # @param parameter [String, Numeric, Symbol] parameter value
102
+ # @return [String] formatted parameter
103
+ #
104
+ # @api private
30
105
  def parameter_to_string(parameter)
31
106
  case parameter
32
107
  when String