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,9 +5,113 @@ require_relative '../series'
5
5
  require_relative '../sequencer'
6
6
 
7
7
  module Musa::Datasets
8
+ # Point series: sequential points in time with durations.
9
+ #
10
+ # P (Point series) represents sequential points in time as arrays with alternating
11
+ # structure: [point, duration, point, duration, point, ...].
12
+ #
13
+ # ## Structure
14
+ #
15
+ # The array alternates between points and durations:
16
+ #
17
+ # [point₀, duration₀, point₁, duration₁, point₂]
18
+ #
19
+ # - **Points** (odd positions): Any data (numbers, hashes, complex structures, etc.)
20
+ # - **Durations** (even positions): Time between points (numbers)
21
+ # - **Last point**: Final point has no duration (sequence end)
22
+ #
23
+ # This compact format efficiently represents timed sequences without
24
+ # repeating time information.
25
+ #
26
+ # ## Conversions
27
+ #
28
+ # P can be converted to two different representations:
29
+ #
30
+ # ### 1. Timed Series (to_timed_serie)
31
+ #
32
+ # Converts to series of {AbsTimed} events with absolute time and value.
33
+ # Each value gets a timestamp based on cumulative durations.
34
+ #
35
+ # p = [60, 4, 64, 8, 67].extend(P)
36
+ # serie = p.to_timed_serie
37
+ # # Yields:
38
+ # # { time: 0, value: 60 }
39
+ # # { time: 1.0, value: 64 } (4 * base_duration = 1.0)
40
+ # # { time: 3.0, value: 67 } (8 * base_duration = 2.0)
41
+ #
42
+ # ### 2. Parameter Segment Series (to_ps_serie)
43
+ #
44
+ # Converts to series of {PS} (Parameter Segment) objects representing
45
+ # continuous changes between consecutive points.
46
+ #
47
+ # p = [60, 4, 64, 8, 67].extend(P)
48
+ # serie = p.to_ps_serie
49
+ # # Yields PS objects:
50
+ # # { from: 60, to: 64, duration: 1.0, right_open: true }
51
+ # # { from: 64, to: 67, duration: 2.0, right_open: false }
52
+ #
53
+ # ## Point Transformation
54
+ #
55
+ # The {#map} method transforms points while preserving durations:
56
+ #
57
+ # p = [60, 4, 64, 8, 67].extend(P)
58
+ # p2 = p.map { |point| point + 12 }
59
+ # # => [72, 4, 76, 8, 79]
60
+ # # Durations unchanged, points transformed
61
+ #
62
+ # @example Basic point series (MIDI pitches)
63
+ # # MIDI pitches with durations in quarter notes
64
+ # p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
65
+ # # 60 (C4) for 4 quarters → 64 (E4) for 8 quarters → 67 (G4)
66
+ #
67
+ # @example Hash points (complex data structures)
68
+ # p = [
69
+ # { pitch: 60, velocity: 64 }, 4,
70
+ # { pitch: 64, velocity: 80 }, 8,
71
+ # { pitch: 67, velocity: 64 }
72
+ # ].extend(Musa::Datasets::P)
73
+ #
74
+ # @example Convert to timed serie
75
+ # p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
76
+ # serie = p.to_timed_serie(base_duration: 1/4r)
77
+ # # base_duration: quarter note = 1/4 beat
78
+ #
79
+ # @example Start at specific time
80
+ # serie = p.to_timed_serie(time_start: 10)
81
+ # # First event at time 10
82
+ #
83
+ # @example Start time from component
84
+ # p = [{ time: 100, pitch: 60 }, 4, { time: 200, pitch: 64 }].extend(P)
85
+ # serie = p.to_timed_serie(time_start_component: :time)
86
+ # # First event at time 100 (from first point's :time)
87
+ #
88
+ # @example Transform points
89
+ # p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
90
+ # p2 = p.map { |point| point + 12 }
91
+ # # Transform each point (e.g., transpose pitches up one octave)
92
+ #
93
+ # @see PS Parameter segments
94
+ # @see AbsTimed Timed events
95
+ # @see Dataset Parent dataset module
8
96
  module P
9
97
  include Dataset
10
98
 
99
+ # Converts to series of parameter segments.
100
+ #
101
+ # Creates {PS} objects representing continuous changes from each point
102
+ # to the next. Useful for glissandi, parameter sweeps, or any continuous
103
+ # interpolation between points.
104
+ #
105
+ # @param base_duration [Rational] duration unit multiplier (default: 1/4r)
106
+ # Durations in P are multiplied by this to get actual time
107
+ #
108
+ # @return [Musa::Series::Serie<PS>] series of parameter segments
109
+ #
110
+ # @example Create parameter segments
111
+ # p = [60, 4, 64, 8, 67].extend(P)
112
+ # serie = p.to_ps_serie
113
+ # segment1 = serie.next_value
114
+ # # => { from: 60, to: 64, duration: 1.0, right_open: true }
11
115
  def to_ps_serie(base_duration: nil)
12
116
  base_duration ||= 1/4r # TODO review incoherence between neumalang 1/4r base duration for quarter notes and general 1r size of bar
13
117
 
@@ -22,6 +126,33 @@ module Musa::Datasets
22
126
  end
23
127
  end
24
128
 
129
+ # Converts to series of timed events (AbsTimed).
130
+ #
131
+ # Creates series yielding {AbsTimed} events with absolute time and value.
132
+ # Each value is emitted at its calculated time point based on cumulative durations.
133
+ #
134
+ # @param time_start [Numeric] starting time offset (default: 0)
135
+ # @param time_start_component [Symbol] key in first value to use as time offset
136
+ # If provided, adds first[time_start_component] to time_start
137
+ # @param base_duration [Rational] duration unit multiplier (default: 1/4r)
138
+ #
139
+ # @return [PtoTimedSerie] series of timed events
140
+ #
141
+ # @example Basic timed serie
142
+ # p = [60, 4, 64, 8, 67].extend(P)
143
+ # serie = p.to_timed_serie
144
+ # serie.next_value # => { time: 0, value: 60 }
145
+ # serie.next_value # => { time: 1.0, value: 64 }
146
+ # serie.next_value # => { time: 3.0, value: 67 }
147
+ #
148
+ # @example Custom start time
149
+ # serie = p.to_timed_serie(time_start: 10)
150
+ # # First event at time 10
151
+ #
152
+ # @example Start time from component
153
+ # p = [{ time: 100, pitch: 60 }, 4, { pitch: 64 }].extend(P)
154
+ # serie = p.to_timed_serie(time_start_component: :time)
155
+ # # First event at time 100
25
156
  def to_timed_serie(time_start: nil, time_start_component: nil, base_duration: nil)
26
157
  time_start ||= 0r
27
158
  time_start += self.first[time_start_component] if time_start_component
@@ -31,11 +162,30 @@ module Musa::Datasets
31
162
  PtoTimedSerie.new(self, base_duration, time_start)
32
163
  end
33
164
 
165
+ # Maps over points, preserving durations.
166
+ #
167
+ # Transforms each point (odd positions) using the block while
168
+ # keeping durations (even positions) unchanged.
169
+ #
170
+ # @yieldparam point [Object] each point in the series
171
+ # @yieldreturn [Object] transformed point
172
+ #
173
+ # @return [P] new P with transformed points
174
+ #
175
+ # @example Transform points (e.g., transpose pitches)
176
+ # p = [60, 4, 64, 8, 67].extend(P)
177
+ # p.map { |point| point + 12 }
178
+ # # => [72, 4, 76, 8, 79]
179
+ #
180
+ # @example Transform hash points
181
+ # p = [{ pitch: 60 }, 4, { pitch: 64 }].extend(P)
182
+ # p.map { |point| point.merge(velocity: 80) }
183
+ # # Adds velocity to each point
34
184
  def map(&block)
35
185
  i = 0
36
186
  clone.map! do |element|
37
- # Process with block only the values (values are the alternating elements because P
38
- # structure is <value> <duration> <value> <duration> <value>)
187
+ # Process with block only the points (points are the alternating elements because P
188
+ # structure is <point> <duration> <point> <duration> <point>)
39
189
  #
40
190
  if (i += 1) % 2 == 1
41
191
  block.call(element)
@@ -45,9 +195,26 @@ module Musa::Datasets
45
195
  end
46
196
  end
47
197
 
198
+ # Series adapter for P to AbsTimed conversion.
199
+ #
200
+ # PtoTimedSerie is a {Musa::Series::Serie} that converts a {P} (point series)
201
+ # into a series of {AbsTimed} events. It reads the alternating point/duration
202
+ # structure and emits timed events.
203
+ #
204
+ # This class is created by {P#to_timed_serie} and should not be instantiated
205
+ # directly.
206
+ #
207
+ # @api private
48
208
  class PtoTimedSerie
49
209
  include Musa::Series::Serie.base
50
210
 
211
+ # Creates new timed serie adapter.
212
+ #
213
+ # @param origin [P] source point series
214
+ # @param base_duration [Rational] duration unit multiplier
215
+ # @param time_start [Numeric] starting time offset
216
+ #
217
+ # @api private
51
218
  def initialize(origin, base_duration, time_start)
52
219
  @origin = origin
53
220
  @base_duration = base_duration
@@ -58,8 +225,16 @@ module Musa::Datasets
58
225
  mark_as_prototype!
59
226
  end
60
227
 
228
+ # Source point series.
229
+ # @return [P]
61
230
  attr_accessor :origin
231
+
232
+ # Duration unit multiplier.
233
+ # @return [Rational]
62
234
  attr_accessor :base_duration
235
+
236
+ # Starting time offset.
237
+ # @return [Numeric]
63
238
  attr_accessor :time_start
64
239
 
65
240
  private def _init
@@ -2,9 +2,100 @@ require_relative 'e'
2
2
  require_relative 'v'
3
3
 
4
4
  module Musa::Datasets
5
+ # Hash-based dataset with array conversion.
6
+ #
7
+ # PackedV (Packed Value) represents datasets stored as hashes (named key-value pairs).
8
+ # Extends {AbsI} for absolute indexed events.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # PackedV provides named key-value storage for musical data and conversion
13
+ # to indexed arrays ({V}). This is useful for:
14
+ #
15
+ # - Semantic naming of values (pitch, duration, velocity)
16
+ # - Sparse data (only store non-default values)
17
+ # - Converting between hash and array representations
18
+ # - Serialization to readable formats
19
+ #
20
+ # ## Conversion to V
21
+ #
22
+ # The {#to_V} method converts hashes to arrays using a mapper that
23
+ # defines the correspondence between hash keys and array positions.
24
+ #
25
+ # ### Array Mapper
26
+ #
27
+ # Array mapper defines the order of keys in resulting array.
28
+ # Position i contains value for key mapper[i].
29
+ #
30
+ # pv = { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PackedV)
31
+ # v = pv.to_V([:pitch, :duration, :velocity])
32
+ # # => [60, 1.0, nil]
33
+ # # velocity missing, becomes nil
34
+ #
35
+ # - Missing keys become `nil` in array
36
+ #
37
+ # ### Hash Mapper
38
+ #
39
+ # Hash mapper defines both key order (keys) and default values (values).
40
+ # Position i contains value for key mapper.keys[i], using mapper.values[i]
41
+ # as default if key is missing or value is nil.
42
+ #
43
+ # pv = { pitch: 60 }.extend(Musa::Datasets::PackedV)
44
+ # v = pv.to_V({ pitch: 60, duration: 1.0, velocity: 64 })
45
+ # # => [60, 1.0, 64]
46
+ # # duration and velocity use defaults
47
+ #
48
+ # Defaults fill in missing or nil values.
49
+ #
50
+ # @example Basic hash to array conversion
51
+ # pv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PackedV)
52
+ # v = pv.to_V([:pitch, :duration, :velocity])
53
+ # # => [60, 1.0, 64]
54
+ #
55
+ # @example Missing keys become nil (array mapper)
56
+ # pv = { a: 1, c: 3 }.extend(Musa::Datasets::PackedV)
57
+ # v = pv.to_V([:c, :b, :a])
58
+ # # => [3, nil, 1]
59
+ # # b missing, becomes nil
60
+ #
61
+ # @example Hash mapper with defaults
62
+ # pv = { a: 1, b: nil, c: 3 }.extend(Musa::Datasets::PackedV)
63
+ # v = pv.to_V({ c: 100, b: 200, a: 300, d: 400 })
64
+ # # => [3, 200, 1, 400]
65
+ # # b nil → uses default 200
66
+ # # d missing → uses default 400
67
+ #
68
+ # @example Partial mapper (fewer keys in mapper)
69
+ # pv = { a: 1, b: 2, c: 3 }.extend(Musa::Datasets::PackedV)
70
+ # v = pv.to_V([:c, :b])
71
+ # # => [3, 2]
72
+ # # Only c and b extracted
73
+ #
74
+ # @example Key order matters
75
+ # pv = { a: 1, b: 2, c: 3 }.extend(Musa::Datasets::PackedV)
76
+ # v = pv.to_V([:c, :b, :a])
77
+ # # => [3, 2, 1]
78
+ #
79
+ # @see V Array-based dataset (inverse)
80
+ # @see AbsI Parent absolute indexed module
5
81
  module PackedV
6
82
  include AbsI
7
83
 
84
+ # Converts packed hash to array (V).
85
+ #
86
+ # @param mapper [Array<Symbol>, Hash{Symbol => Object}] key mapping
87
+ # - Array: maps keys to indices (order matters)
88
+ # - Hash: maps keys (keys) to indices with defaults (values)
89
+ #
90
+ # @return [V] array dataset
91
+ #
92
+ # @raise [ArgumentError] if mapper is not Array or Hash
93
+ #
94
+ # @example Array mapper
95
+ # pv.to_V([:pitch, :duration, :velocity])
96
+ #
97
+ # @example Hash mapper with defaults
98
+ # pv.to_V({ pitch: 60, duration: 1.0, velocity: 64 })
8
99
  def to_V(mapper)
9
100
  case mapper
10
101
  when Hash
@@ -4,15 +4,150 @@ require_relative 'gdv'
4
4
  require_relative 'helper'
5
5
 
6
6
  module Musa::Datasets
7
+ # MIDI-style musical events with absolute pitches.
8
+ #
9
+ # PDV (Pitch/Duration/Velocity) represents musical events using MIDI-like
10
+ # absolute pitch numbers. Extends {AbsD} for duration support.
11
+ #
12
+ # ## Purpose
13
+ #
14
+ # PDV is the MIDI representation layer of the dataset framework:
15
+ #
16
+ # - Uses absolute MIDI pitch numbers (0-127)
17
+ # - Uses MIDI velocity values (0-127)
18
+ # - Direct mapping to MIDI messages
19
+ # - Machine-oriented (not human-readable)
20
+ #
21
+ # Contrast with {GDV} which uses score notation (scale degrees, dynamics).
22
+ #
23
+ # ## Natural Keys
24
+ #
25
+ # - **:pitch**: MIDI pitch number (0-127) or :silence for rests
26
+ # - **:velocity**: MIDI velocity (0-127)
27
+ # - **:duration**: Event duration (from {AbsD})
28
+ # - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
29
+ #
30
+ # ## Conversions
31
+ #
32
+ # ### To GDV (Score Notation)
33
+ #
34
+ # Converts MIDI pitches to scale degrees using a scale reference:
35
+ #
36
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
37
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
38
+ # gdv = pdv.to_gdv(scale)
39
+ # # => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
40
+ #
41
+ # - **Pitch → Grade**: Finds closest scale degree
42
+ # - **Chromatic notes**: Represented as grade + sharps
43
+ # - **Velocity**: Maps MIDI 0-127 to dynamics -5 to +4 (ppp to fff)
44
+ #
45
+ # ### Velocity Mapping
46
+ #
47
+ # MIDI velocities are mapped to musical dynamics:
48
+ #
49
+ # MIDI 1-1 → velocity -5 (ppp)
50
+ # MIDI 2-8 → velocity -4 (pp)
51
+ # MIDI 9-16 → velocity -3 (p)
52
+ # MIDI 17-33 → velocity -2 (mp)
53
+ # MIDI 34-48 → velocity -1 (mf-)
54
+ # MIDI 49-64 → velocity 0 (mf)
55
+ # MIDI 65-80 → velocity +1 (f)
56
+ # MIDI 81-96 → velocity +2 (ff)
57
+ # MIDI 97-112 → velocity +3 (fff-)
58
+ # MIDI 113-127 → velocity +4 (fff)
59
+ #
60
+ # ## Base Duration
61
+ #
62
+ # The `base_duration` attribute defines the unit for duration values,
63
+ # typically 1/4r (quarter note).
64
+ #
65
+ # @example Basic MIDI event
66
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV)
67
+ # pdv.base_duration = 1/4r
68
+ # # C4 (middle C) for 1 beat at mf dynamics
69
+ #
70
+ # @example Silence (rest)
71
+ # pdv = { pitch: :silence, duration: 1.0 }.extend(PDV)
72
+ # # Rest for 1 beat
73
+ #
74
+ # @example With articulation
75
+ # pdv = {
76
+ # pitch: 64,
77
+ # duration: 1.0,
78
+ # note_duration: 0.5, # Staccato
79
+ # velocity: 80
80
+ # }.extend(PDV)
81
+ #
82
+ # @example Convert to score notation
83
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
84
+ # pdv.base_duration = 1/4r
85
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
86
+ # gdv = pdv.to_gdv(scale)
87
+ # # => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
88
+ #
89
+ # @example Chromatic pitch
90
+ # pdv = { pitch: 61, duration: 1.0, velocity: 64 }.extend(PDV)
91
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
92
+ # gdv = pdv.to_gdv(scale)
93
+ # # => { grade: 0, octave: 0, sharps: 1, duration: 1.0, velocity: 0 }
94
+ # # C# represented as C (grade 0) + 1 sharp
95
+ #
96
+ # @example Preserve additional keys
97
+ # pdv = {
98
+ # pitch: 60,
99
+ # duration: 1.0,
100
+ # velocity: 64,
101
+ # custom_key: :value
102
+ # }.extend(PDV)
103
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
104
+ # gdv = pdv.to_gdv(scale)
105
+ # # custom_key copied to GDV (not a natural key)
106
+ #
107
+ # @see GDV Score-style representation
108
+ # @see AbsD Absolute duration events
109
+ # @see Helper String formatting utilities
7
110
  module PDV
8
111
  include AbsD
9
112
 
10
113
  include Helper
11
114
 
115
+ # Natural keys for MIDI events.
116
+ # @return [Array<Symbol>]
12
117
  NaturalKeys = (NaturalKeys + [:pitch, :velocity]).freeze
13
118
 
119
+ # Base duration for time calculations.
120
+ # @return [Rational]
14
121
  attr_accessor :base_duration
15
122
 
123
+ # Converts to GDV (score notation).
124
+ #
125
+ # Translates MIDI representation to score notation using a scale:
126
+ # - MIDI pitch → scale degree (grade + octave + sharps)
127
+ # - MIDI velocity → dynamics (-5 to +4)
128
+ # - Duration values copied
129
+ # - Additional keys preserved
130
+ #
131
+ # @param scale [Musa::Scales::Scale] reference scale for pitch conversion
132
+ #
133
+ # @return [GDV] score notation dataset
134
+ #
135
+ # @example Basic conversion
136
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
137
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
138
+ # gdv = pdv.to_gdv(scale)
139
+ #
140
+ # @example Chromatic note
141
+ # pdv = { pitch: 61, duration: 1.0 }.extend(PDV)
142
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
143
+ # gdv = pdv.to_gdv(scale)
144
+ # # => { grade: 0, octave: 0, sharps: 1, duration: 1.0 }
145
+ #
146
+ # @example Silence
147
+ # pdv = { pitch: :silence, duration: 1.0 }.extend(PDV)
148
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
149
+ # gdv = pdv.to_gdv(scale)
150
+ # # => { grade: :silence, duration: 1.0 }
16
151
  def to_gdv(scale)
17
152
  gdv = {}.extend GDV
18
153
  gdv.base_duration = @base_duration
@@ -39,7 +174,7 @@ module Musa::Datasets
39
174
  if self[:velocity]
40
175
  # ppp = 16 ... fff = 127
41
176
  # TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
42
- gdv[:velocity] = [1..1, 2..8, 9..16, 17..33, 34..49, 49..64, 65..80, 81..96, 97..112, 113..127].index { |r| r.cover? self[:velocity] } - 5
177
+ gdv[:velocity] = [1..1, 2..8, 9..16, 17..33, 34..48, 49..64, 65..80, 81..96, 97..112, 113..127].index { |r| r.cover? self[:velocity] } - 5
43
178
  end
44
179
 
45
180
  (keys - NaturalKeys).each { |k| gdv[k] = self[k] }
@@ -4,31 +4,161 @@ require_relative 'score'
4
4
  require_relative '../sequencer'
5
5
 
6
6
  module Musa::Datasets
7
+ # Parameter segments for continuous changes between multidimensional points.
8
+ #
9
+ # PS (Parameter Segment) represents a continuous change from one point
10
+ # to another over a duration. Extends {AbsD} for duration support.
11
+ #
12
+ # ## Purpose
13
+ #
14
+ # PS is used to represent:
15
+ #
16
+ # - **Glissandi**: Continuous pitch slides (portamento)
17
+ # - **Parameter sweeps**: Gradual changes in any sonic parameter
18
+ # - **Interpolations**: Smooth transitions between multidimensional states
19
+ #
20
+ # Unlike discrete events that jump from one value to another, PS represents
21
+ # the continuous path between values.
22
+ #
23
+ # ## Natural Keys
24
+ #
25
+ # - **:from**: Starting value (number, array, or hash)
26
+ # - **:to**: Ending value (must match :from type and structure)
27
+ # - **:right_open**: Whether endpoint is included (true = open interval)
28
+ # - **:duration**: Duration of the change (from {AbsD})
29
+ # - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
30
+ #
31
+ # ## Value Types
32
+ #
33
+ # ### Single Values
34
+ #
35
+ # { from: 60, to: 72, duration: 1.0 }
36
+ # # Single value glissando
37
+ #
38
+ # ### Arrays (parallel interpolation)
39
+ #
40
+ # { from: [60, 64], to: [72, 76], duration: 1.0 }
41
+ # # Both values interpolate in parallel
42
+ # # Arrays must be same size
43
+ #
44
+ # ### Hashes (named parameters)
45
+ #
46
+ # { from: { pitch: 60, velocity: 64 },
47
+ # to: { pitch: 72, velocity: 80 },
48
+ # duration: 1.0 }
49
+ # # Multiple parameters interpolate together
50
+ # # Hashes must have same keys
51
+ #
52
+ # ## Right Open Intervals
53
+ #
54
+ # The :right_open flag determines if the ending value is reached:
55
+ #
56
+ # - **false** (closed): Interpolation reaches :to value
57
+ # - **true** (open): Interpolation stops just before :to value
58
+ #
59
+ # This is important for consecutive segments where you don't want
60
+ # discontinuities at the boundaries.
61
+ #
62
+ # @example Basic parameter segment (pitch glissando)
63
+ # ps = { from: 60, to: 72, duration: 2.0 }.extend(Musa::Datasets::PS)
64
+ # # Continuous slide from C4 to C5 over 2 beats
65
+ #
66
+ # @example Parallel interpolation (multidimensional)
67
+ # ps = {
68
+ # from: [60, 64], # C4 and E4
69
+ # to: [72, 76], # C5 and E5
70
+ # duration: 1.0
71
+ # }.extend(PS)
72
+ # # Both parameters move in parallel
73
+ #
74
+ # @example Multiple parameters (sonic gesture)
75
+ # ps = {
76
+ # from: { pitch: 60, velocity: 64, pan: -1.0 },
77
+ # to: { pitch: 72, velocity: 80, pan: 1.0 },
78
+ # duration: 2.0
79
+ # }.extend(PS)
80
+ # # Pitch, velocity, and pan all change smoothly
81
+ #
82
+ # @example Right open interval
83
+ # ps1 = { from: 60, to: 64, duration: 1.0, right_open: true }.extend(PS)
84
+ # ps2 = { from: 64, to: 67, duration: 1.0, right_open: false }.extend(PS)
85
+ # # ps1 stops just before 64, ps2 starts at 64 - no discontinuity
86
+ #
87
+ # @example Created from P point series
88
+ # p = [60, 4, 64, 8, 67].extend(P)
89
+ # serie = p.to_ps_serie
90
+ # ps1 = serie.next_value
91
+ # # => { from: 60, to: 64, duration: 1.0, right_open: true }
92
+ #
93
+ # @see AbsD Parent absolute duration module
94
+ # @see P Point series (source of PS)
95
+ # @see Helper String formatting utilities
7
96
  module PS
8
97
  include AbsD
9
98
 
10
99
  include Helper
11
100
 
101
+ # Natural keys including segment endpoints.
102
+ # @return [Array<Symbol>]
12
103
  NaturalKeys = (NaturalKeys + [:from, :to, :right_open]).freeze
13
104
 
105
+ # Base duration for time calculations.
106
+ # @return [Rational]
14
107
  attr_accessor :base_duration
15
108
 
109
+ # Converts to Neuma notation string.
110
+ #
111
+ # @return [String] Neuma notation
112
+ # @todo Not yet implemented
16
113
  def to_neuma
17
- # TODO ???????
114
+ raise NotImplementedError, 'PS to_neuma conversion is not yet implemented'
18
115
  end
19
116
 
117
+ # Converts to PDV (Pitch/Duration/Velocity).
118
+ #
119
+ # @return [PDV] PDV dataset
120
+ # @todo Not yet implemented
20
121
  def to_pdv
21
- # TODO ??????
122
+ raise NotImplementedError, 'PS to_pdv conversion is not yet implemented'
22
123
  end
23
124
 
125
+ # Converts to GDV (Grade/Duration/Velocity).
126
+ #
127
+ # @return [GDV] GDV dataset
128
+ # @todo Not yet implemented
24
129
  def to_gdv
25
- # TODO ?????
130
+ raise NotImplementedError, 'PS to_gdv conversion is not yet implemented'
26
131
  end
27
132
 
133
+ # Converts to absolute indexed format.
134
+ #
135
+ # @return [AbsI] indexed dataset
136
+ # @todo Not yet implemented
28
137
  def to_absI
29
- # TODO ?????
138
+ raise NotImplementedError, 'PS to_absI conversion is not yet implemented'
30
139
  end
31
140
 
141
+ # Validates PS structure.
142
+ #
143
+ # Checks that:
144
+ # - :from and :to have compatible types
145
+ # - Arrays have same size
146
+ # - Hashes have same keys
147
+ # - Duration is positive numeric
148
+ #
149
+ # @return [Boolean] true if valid
150
+ #
151
+ # @example Valid array segment
152
+ # ps = { from: [60, 64], to: [72, 76], duration: 1.0 }.extend(PS)
153
+ # ps.valid? # => true
154
+ #
155
+ # @example Invalid - mismatched array sizes
156
+ # ps = { from: [60, 64], to: [72], duration: 1.0 }.extend(PS)
157
+ # ps.valid? # => false
158
+ #
159
+ # @example Invalid - mismatched hash keys
160
+ # ps = { from: { a: 1 }, to: { b: 2 }, duration: 1.0 }.extend(PS)
161
+ # ps.valid? # => false
32
162
  def valid?
33
163
  case self[:from]
34
164
  when Array