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
@@ -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
@@ -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