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
@@ -4,6 +4,148 @@ require_relative 'base-series'
4
4
 
5
5
  module Musa
6
6
  module Series::Constructors
7
+ # Merges multiple timed series by synchronizing events at each time point.
8
+ #
9
+ # TIMED_UNION combines series with `:time` attributes, emitting events at each
10
+ # unique time where at least one source has a value. Sources without values at
11
+ # a given time emit `nil`. Operates in two distinct modes based on input format.
12
+ #
13
+ # ## Timed Series Format
14
+ #
15
+ # Each event is a hash with `:time` and `:value` keys, extended with AbsTimed:
16
+ #
17
+ # ```ruby
18
+ # { time: 0r, value: 60, duration: 1r }.extend(Musa::Datasets::AbsTimed)
19
+ # ```
20
+ #
21
+ # Additional attributes (`:duration`, `:velocity`, etc.) are preserved and
22
+ # synchronized alongside values.
23
+ #
24
+ # ## Operating Modes
25
+ #
26
+ # **Array Mode**: `TIMED_UNION(s1, s2, s3)`
27
+ # - Anonymous positional sources
28
+ # - Output: `{ time: t, value: [val1, val2, val3] }`
29
+ # - Use for: Ordered tracks without specific names
30
+ #
31
+ # **Hash Mode**: `TIMED_UNION(melody: s1, bass: s2)`
32
+ # - Named sources with keys
33
+ # - Output: `{ time: t, value: { melody: val1, bass: val2 } }`
34
+ # - Use for: Identified voices/tracks for routing
35
+ #
36
+ # ## Value Types and Combination
37
+ #
38
+ # **Direct values** (integers, strings, etc.):
39
+ # ```ruby
40
+ # s1 = S({ time: 0, value: 60 })
41
+ # s2 = S({ time: 0, value: 64 })
42
+ # TIMED_UNION(s1, s2) # => { time: 0, value: [60, 64] }
43
+ # ```
44
+ #
45
+ # **Hash values** (polyphonic events):
46
+ # ```ruby
47
+ # s1 = S({ time: 0, value: { a: 1, b: 2 } })
48
+ # s2 = S({ time: 0, value: { c: 10 } })
49
+ # TIMED_UNION(s1, s2) # => { time: 0, value: { a: 1, b: 2, c: 10 } }
50
+ # ```
51
+ #
52
+ # **Array values** (multi-element events):
53
+ # ```ruby
54
+ # s1 = S({ time: 0, value: [1, 2] })
55
+ # s2 = S({ time: 0, value: [10, 20] })
56
+ # TIMED_UNION(s1, s2) # => { time: 0, value: [1, 2, 10, 20] }
57
+ # ```
58
+ #
59
+ # **Mixed Hash + Direct** (advanced):
60
+ # ```ruby
61
+ # s1 = S({ time: 0, value: { a: 1, b: 2 } })
62
+ # s2 = S({ time: 0, value: 100 })
63
+ # TIMED_UNION(s1, s2) # => { time: 0, value: { a: 1, b: 2, 0 => 100 } }
64
+ # ```
65
+ #
66
+ # ## Synchronization Behavior
67
+ #
68
+ # Events are emitted at each unique time point across all sources:
69
+ #
70
+ # ```ruby
71
+ # s1 = S({ time: 0r, value: 1 }, { time: 2r, value: 3 })
72
+ # s2 = S({ time: 1r, value: 10 })
73
+ # TIMED_UNION(s1, s2).i.to_a
74
+ # # => [{ time: 0r, value: [1, nil] },
75
+ # # { time: 1r, value: [nil, 10] },
76
+ # # { time: 2r, value: [3, nil] }]
77
+ # ```
78
+ #
79
+ # ## Extra Attributes
80
+ #
81
+ # Non-standard attributes (beyond `:time`, `:value`) are synchronized:
82
+ #
83
+ # ```ruby
84
+ # s1 = S({ time: 0, value: 1, velocity: 80 })
85
+ # s2 = S({ time: 0, value: 10, duration: 1r })
86
+ # TIMED_UNION(s1, s2)
87
+ # # => { time: 0, value: [1, 10], velocity: [80, nil], duration: [nil, 1r] }
88
+ # ```
89
+ #
90
+ # @param array_of_timed_series [Array<Serie>] timed series (array mode)
91
+ # @param hash_of_timed_series [Hash{Symbol => Serie}] named timed series (hash mode)
92
+ #
93
+ # @return [TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries] merged serie
94
+ #
95
+ # @raise [ArgumentError] if mixing array and hash modes
96
+ # @raise [RuntimeError] if hash values have duplicate keys across sources
97
+ # @raise [RuntimeError] if mixing incompatible value types (Hash with Array)
98
+ #
99
+ # @example Array mode with direct values
100
+ # s1 = S({ time: 0r, value: 1 }, { time: 1r, value: 2 })
101
+ # s2 = S({ time: 0r, value: 10 }, { time: 2r, value: 20 })
102
+ #
103
+ # union = TIMED_UNION(s1, s2).i
104
+ # union.to_a
105
+ # # => [{ time: 0r, value: [1, 10] },
106
+ # # { time: 1r, value: [2, nil] },
107
+ # # { time: 2r, value: [nil, 20] }]
108
+ #
109
+ # @example Hash mode with named sources
110
+ # melody = S({ time: 0r, value: 60 }, { time: 1r, value: 64 })
111
+ # bass = S({ time: 0r, value: 36 }, { time: 2r, value: 40 })
112
+ #
113
+ # union = TIMED_UNION(melody: melody, bass: bass).i
114
+ # union.to_a
115
+ # # => [{ time: 0r, value: { melody: 60, bass: 36 } },
116
+ # # { time: 1r, value: { melody: 64, bass: nil } },
117
+ # # { time: 2r, value: { melody: nil, bass: 40 } }]
118
+ #
119
+ # @example Hash values with polyphonic events
120
+ # s1 = S({ time: 0r, value: { a: 1, b: 2 } })
121
+ # s2 = S({ time: 0r, value: { c: 10, d: 20 } })
122
+ #
123
+ # union = TIMED_UNION(s1, s2).i
124
+ # union.next_value # => { time: 0r, value: { a: 1, b: 2, c: 10, d: 20 } }
125
+ #
126
+ # @example Extra attributes synchronization
127
+ # s1 = S({ time: 0r, value: 1, velocity: 80, duration: 1r })
128
+ # s2 = S({ time: 0r, value: 10, velocity: 90 })
129
+ #
130
+ # union = TIMED_UNION(s1, s2).i
131
+ # union.next_value
132
+ # # => { time: 0r,
133
+ # # value: [1, 10],
134
+ # # velocity: [80, 90],
135
+ # # duration: [1r, nil] }
136
+ #
137
+ # @example Key conflict detection
138
+ # s1 = S({ time: 0r, value: { a: 1, b: 2 } })
139
+ # s2 = S({ time: 0r, value: { a: 10 } }) # 'a' already used!
140
+ #
141
+ # union = TIMED_UNION(s1, s2).i
142
+ # union.next_value # RuntimeError: Value: key a already used
143
+ #
144
+ # @see flatten_timed Splits compound values into individual timed events
145
+ # @see compact_timed Removes events with all-nil values
146
+ # @see union_timed Instance method for union
147
+ #
148
+ # @api public
7
149
  def TIMED_UNION(*array_of_timed_series, **hash_of_timed_series)
8
150
  raise ArgumentError, 'Can\'t union an array of series with a hash of series' if array_of_timed_series.any? && hash_of_timed_series.any?
9
151
 
@@ -16,23 +158,63 @@ module Musa
16
158
  end
17
159
  end
18
160
 
161
+ # Array-mode timed union implementation.
162
+ #
163
+ # Combines anonymous positional timed series, emitting events synchronized by time.
164
+ # Values are combined into arrays or hashes depending on source value types.
165
+ #
166
+ # ## Value Combination Logic
167
+ #
168
+ # - **Direct values**: `[val1, val2, val3]`
169
+ # - **Hash values**: `{ key1: val1, key2: val2 }` (merged from all sources)
170
+ # - **Array values**: `[elem1, elem2, elem3, elem4]` (concatenated)
171
+ # - **Mixed Hash + Direct**: `{ hash_keys..., 0 => direct_val }` (advanced)
172
+ #
173
+ # ## Component Mapping
174
+ #
175
+ # On first value, infers structure via `infer_components` which creates extraction
176
+ # map: `{ attribute => { target_key => [source_i, attr, source_key] } }`
177
+ #
178
+ # This map guides extraction from sources and placement in result for all future values.
179
+ #
180
+ # @api private
19
181
  class TimedUnionOfArrayOfTimedSeries
20
182
  include Series::Serie.with(sources: true)
21
183
 
184
+ # Creates array-mode union from series array.
185
+ #
186
+ # @param series [Array<Serie>] source timed series
187
+ # @api private
22
188
  def initialize(series)
23
189
  self.sources = series
24
190
  init
25
191
  end
26
192
 
193
+ # Initializes buffering and component inference state.
194
+ # @api private
27
195
  private def _init
28
196
  @sources_next_values = Array.new(@sources.size)
29
197
  @components = nil
30
198
  end
31
199
 
200
+ # Restarts all source series.
201
+ # @api private
32
202
  private def _restart
33
203
  @sources.each(&:restart)
34
204
  end
35
205
 
206
+ # Generates next synchronized timed event.
207
+ #
208
+ # Algorithm:
209
+ # 1. Buffer next value from each source
210
+ # 2. Infer component structure (first call only)
211
+ # 3. Find minimum time across all sources
212
+ # 4. Extract values at that time
213
+ # 5. Build result using component map
214
+ # 6. Clear consumed values from buffer
215
+ #
216
+ # @return [Hash, nil] timed event or nil when exhausted
217
+ # @api private
36
218
  private def _next_value
37
219
  sources_values = @sources_next_values.each_index.collect do |i|
38
220
  @sources_next_values[i] || (@sources_next_values[i] = @sources[i].next_value)
@@ -73,10 +255,30 @@ module Musa
73
255
  end
74
256
  end
75
257
 
258
+ # Checks if any source is infinite.
259
+ # @return [Boolean] true if any source infinite
260
+ # @api private
76
261
  def infinite?
77
262
  !!@sources.find(&:infinite?)
78
263
  end
79
264
 
265
+ # Infers component extraction and placement map from first values.
266
+ #
267
+ # Analyzes source value types to create extraction map for all future values.
268
+ # Map structure: `{ attribute => { target_key => [source_i, attr, source_key] } }`
269
+ #
270
+ # **Hash values**: Keys map directly to target keys
271
+ # **Array/Direct values**: `target_index` (0, 1, 2...) assigns positions
272
+ #
273
+ # Also detects and validates:
274
+ # - Duplicate keys across sources (raises RuntimeError)
275
+ # - Incompatible type mixing (Hash with Array, raises RuntimeError)
276
+ #
277
+ # @param sources_values [Array<Hash>] first value from each source
278
+ # @return [Array(Hash, Boolean, Boolean)] components map, hash_mode flag, array_mode flag
279
+ # @raise [RuntimeError] if duplicate keys found
280
+ # @raise [RuntimeError] if incompatible types (Hash + Array)
281
+ # @api private
80
282
  private def infer_components(sources_values)
81
283
  other_attributes = Set[]
82
284
 
@@ -142,28 +344,57 @@ module Musa
142
344
 
143
345
  private_constant :TimedUnionOfArrayOfTimedSeries
144
346
 
347
+ # Hash-mode timed union implementation.
348
+ #
349
+ # Combines named timed series with explicit keys, emitting synchronized events
350
+ # with hash-structured values preserving source names.
351
+ #
352
+ # Output structure: `{ time: t, value: { key1: val1, key2: val2 } }`
353
+ #
354
+ # Simpler than array mode since component names are predetermined by source keys.
355
+ # No inference needed - directly uses hash keys from initialization.
356
+ #
357
+ # @api private
145
358
  class TimedUnionOfHashOfTimedSeries
146
359
  include Series::Serie.with(sources: true)
147
360
 
361
+ # Creates hash-mode union from named series hash.
362
+ #
363
+ # @param series [Hash{Symbol => Serie}] named source series
364
+ # @api private
148
365
  def initialize(series)
149
366
  self.sources = series
150
367
  init
151
368
  end
152
369
 
370
+ # Stores sources and captures component keys.
371
+ # @param series [Hash{Symbol => Serie}] named sources
372
+ # @api private
153
373
  def sources=(series)
154
374
  super
155
375
  @components = series.keys
156
376
  end
157
377
 
378
+ # Initializes buffering for named sources.
379
+ # @api private
158
380
  private def _init
159
381
  @sources_next_values = @components.collect { |k| [k, nil] }.to_h
160
382
  @other_attributes = nil
161
383
  end
162
384
 
385
+ # Restarts all source series.
386
+ # @api private
163
387
  private def _restart
164
388
  @sources.each_value(&:restart)
165
389
  end
166
390
 
391
+ # Generates next synchronized timed event with named values.
392
+ #
393
+ # Similar to array mode but uses predetermined component keys instead of
394
+ # inferring structure from values.
395
+ #
396
+ # @return [Hash, nil] timed event with named values
397
+ # @api private
167
398
  private def _next_value
168
399
  sources_values = {}
169
400
 
@@ -204,10 +435,20 @@ module Musa
204
435
  end
205
436
  end
206
437
 
438
+ # Checks if any source is infinite.
439
+ # @return [Boolean] true if any source infinite
440
+ # @api private
207
441
  def infinite?
208
- !!@sources.find(&:infinite?)
442
+ !!@sources.values.find(&:infinite?)
209
443
  end
210
444
 
445
+ # Discovers extra attributes from first source values.
446
+ #
447
+ # Collects all attribute names beyond `:time` and `:value` for synchronization.
448
+ #
449
+ # @param sources_values [Hash{Symbol => Hash}] first values by source key
450
+ # @return [Set<Symbol>] extra attribute names
451
+ # @api private
211
452
  private def infer_other_attributes(sources_values)
212
453
  other_attributes = Set[]
213
454
 
@@ -225,14 +466,153 @@ module Musa
225
466
  end
226
467
 
227
468
  module Series::Operations
469
+ # Splits compound timed values into individual timed events.
470
+ #
471
+ # Converts events with Hash or Array values into separate timed events per element,
472
+ # preserving time and extra attributes. Direct values pass through unchanged.
473
+ #
474
+ # **Hash values** → Hash of timed events (keyed by original keys):
475
+ # ```ruby
476
+ # { time: 0, value: { a: 1, b: 2 }, velocity: { a: 80, b: 90 } }
477
+ # # becomes:
478
+ # { a: { time: 0, value: 1, velocity: 80 },
479
+ # b: { time: 0, value: 2, velocity: 90 } }
480
+ # ```
481
+ #
482
+ # **Array values** → Array of timed events (indexed):
483
+ # ```ruby
484
+ # { time: 0, value: [1, 2], velocity: [80, 90] }
485
+ # # becomes:
486
+ # [{ time: 0, value: 1, velocity: 80 },
487
+ # { time: 0, value: 2, velocity: 90 }]
488
+ # ```
489
+ #
490
+ # **Direct values** → Pass through unchanged (already flat)
491
+ #
492
+ # ## Use Cases
493
+ #
494
+ # - Separate polyphonic events into individual voices
495
+ # - Split multi-track sequences for independent processing
496
+ # - Prepare for voice-specific routing via `split`
497
+ # - Enable per-voice filtering with `compact_timed`
498
+ #
499
+ # @return [TimedFlattener] flattened timed serie
500
+ #
501
+ # @example Hash values to individual voices
502
+ # s = S({ time: 0r, value: { a: 60, b: 64 }, velocity: { a: 80, b: 90 } })
503
+ #
504
+ # flat = s.flatten_timed.i
505
+ # flat.next_value
506
+ # # => { a: { time: 0r, value: 60, velocity: 80 },
507
+ # # b: { time: 0r, value: 64, velocity: 90 } }
508
+ #
509
+ # @example Array values to indexed events
510
+ # s = S({ time: 0r, value: [60, 64], velocity: [80, 90] })
511
+ #
512
+ # flat = s.flatten_timed.i
513
+ # flat.next_value
514
+ # # => [{ time: 0r, value: 60, velocity: 80 },
515
+ # # { time: 0r, value: 64, velocity: 90 }]
516
+ #
517
+ # @example Direct values pass through
518
+ # s = S({ time: 0r, value: 60, velocity: 80 })
519
+ #
520
+ # flat = s.flatten_timed.i
521
+ # flat.next_value # => { time: 0r, value: 60, velocity: 80 }
522
+ #
523
+ # @see compact_timed Remove nil-only events
524
+ # @see TIMED_UNION Combine multiple timed series
525
+ #
526
+ # @api public
228
527
  def flatten_timed
229
528
  TimedFlattener.new(self)
230
529
  end
231
530
 
531
+ # Removes timed events where all values are nil.
532
+ #
533
+ # Filters out temporal "gaps" where no sources have active values. Useful after
534
+ # union operations that create nil placeholders, or for cleaning sparse sequences.
535
+ #
536
+ # **Removal logic**:
537
+ # - **Direct nil**: `{ time: t, value: nil }` → removed
538
+ # - **All-nil Hash**: `{ time: t, value: { a: nil, b: nil } }` → removed
539
+ # - **Partial Hash**: `{ time: t, value: { a: 1, b: nil } }` → kept (has non-nil)
540
+ # - **All-nil Array**: `{ time: t, value: [nil, nil] }` → removed
541
+ # - **Partial Array**: `{ time: t, value: [1, nil] }` → kept (has non-nil)
542
+ #
543
+ # @return [TimedCompacter] compacted serie
544
+ #
545
+ # @example Remove direct nil events
546
+ # s = S({ time: 0r, value: 1 },
547
+ # { time: 1r, value: nil },
548
+ # { time: 2r, value: 3 })
549
+ #
550
+ # s.compact_timed.i.to_a
551
+ # # => [{ time: 0r, value: 1 },
552
+ # # { time: 2r, value: 3 }]
553
+ #
554
+ # @example Remove all-nil hash events
555
+ # s = S({ time: 0r, value: { a: 1, b: 2 } },
556
+ # { time: 1r, value: { a: nil, b: nil } },
557
+ # { time: 2r, value: { a: 3, b: nil } })
558
+ #
559
+ # s.compact_timed.i.to_a
560
+ # # => [{ time: 0r, value: { a: 1, b: 2 } },
561
+ # # { time: 2r, value: { a: 3, b: nil } }] # Kept: has non-nil 'a'
562
+ #
563
+ # @example Clean sparse union results
564
+ # s1 = S({ time: 0r, value: 1 }, { time: 2r, value: 3 })
565
+ # s2 = S({ time: 1r, value: 10 })
566
+ #
567
+ # union = TIMED_UNION(melody: s1, bass: s2).i.to_a
568
+ # # => [{ time: 0r, value: { melody: 1, bass: nil } },
569
+ # # { time: 1r, value: { melody: nil, bass: 10 } },
570
+ # # { time: 2r, value: { melody: 3, bass: nil } }]
571
+ #
572
+ # # All events have at least one non-nil, so none removed
573
+ #
574
+ # @see flatten_timed Split compound values
575
+ # @see TIMED_UNION Combine series (may introduce nils)
576
+ #
577
+ # @api public
232
578
  def compact_timed
233
579
  TimedCompacter.new(self)
234
580
  end
235
581
 
582
+ # Combines this timed serie with others via TIMED_UNION.
583
+ #
584
+ # Convenience method for unioning series, supporting both array and hash modes.
585
+ # Calls {TIMED_UNION} constructor with appropriate parameters.
586
+ #
587
+ # **Array mode**: `s1.union_timed(s2, s3)`
588
+ # **Hash mode**: `s1.union_timed(key: :melody, bass: s2, drums: s3)`
589
+ #
590
+ # @param other_timed_series [Array<Serie>] additional series (array mode)
591
+ # @param key [Symbol, nil] key name for this serie (hash mode)
592
+ # @param other_key_timed_series [Hash{Symbol => Serie}] named series (hash mode)
593
+ #
594
+ # @return [TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries] union
595
+ #
596
+ # @raise [ArgumentError] if mixing array and hash modes
597
+ #
598
+ # @example Array mode
599
+ # melody = S({ time: 0r, value: 60 })
600
+ # bass = S({ time: 0r, value: 36 })
601
+ #
602
+ # melody.union_timed(bass).i.next_value
603
+ # # => { time: 0r, value: [60, 36] }
604
+ #
605
+ # @example Hash mode
606
+ # melody = S({ time: 0r, value: 60 })
607
+ # bass = S({ time: 0r, value: 36 })
608
+ # drums = S({ time: 0r, value: 38 })
609
+ #
610
+ # melody.union_timed(key: :melody, bass: bass, drums: drums).i.next_value
611
+ # # => { time: 0r, value: { melody: 60, bass: 36, drums: 38 } }
612
+ #
613
+ # @see TIMED_UNION Constructor version
614
+ #
615
+ # @api public
236
616
  def union_timed(*other_timed_series, key: nil, **other_key_timed_series)
237
617
  if key && other_key_timed_series.any?
238
618
  Series::Constructors.TIMED_UNION(key => self, **other_key_timed_series)
@@ -245,18 +625,43 @@ module Musa
245
625
  end
246
626
  end
247
627
 
628
+ # Internal implementation for flattening timed values.
629
+ #
630
+ # Transforms compound timed events into collections of individual timed events,
631
+ # distributing extra attributes to corresponding elements.
632
+ #
633
+ # @api private
248
634
  class TimedFlattener
249
635
  include Series::Serie.with(source: true)
250
636
 
637
+ # Creates flattener wrapping source serie.
638
+ #
639
+ # @param serie [Serie] source timed serie
640
+ # @api private
251
641
  def initialize(serie)
252
642
  self.source = serie
253
643
  init
254
644
  end
255
645
 
646
+ # Restarts source serie.
647
+ # @api private
256
648
  private def _restart
257
649
  @source.restart
258
650
  end
259
651
 
652
+ # Generates next flattened value from source.
653
+ #
654
+ # Algorithm:
655
+ # 1. Get next timed event from source
656
+ # 2. Extract time and extra attributes
657
+ # 3. Based on value type:
658
+ # - **Hash**: Create hash of timed events (key → timed event)
659
+ # - **Array**: Create array of timed events (index → timed event)
660
+ # - **Direct**: Clone and pass through unchanged
661
+ # 4. Distribute extra attributes to corresponding elements
662
+ #
663
+ # @return [Hash, Array, Hash, nil] flattened structure or nil
664
+ # @api private
260
665
  private def _next_value
261
666
  source_value = @source.next_value
262
667
 
@@ -264,31 +669,37 @@ module Musa
264
669
  time = source_value[:time]
265
670
  source_value_value = source_value[:value]
266
671
 
672
+ # Extract all attributes beyond :time and :value
267
673
  source_value_extra = (source_value.keys - [:time, :value]).collect do |attribute_name|
268
674
  [attribute_name, source_value[attribute_name]]
269
675
  end.to_h
270
676
 
271
677
  case source_value_value
272
678
  when Hash
679
+ # Hash values: { key => timed_event }
273
680
  result = {}
274
681
  source_value_value.each_pair do |key, value|
275
682
  result[key] = { time: time, value: value }.extend(Musa::Datasets::AbsTimed)
276
683
 
684
+ # Distribute extra attributes by key
277
685
  source_value_extra.each do |attribute_name, attribute_value|
278
686
  result[key][attribute_name] = attribute_value[key]
279
687
  end
280
688
  end
281
689
 
282
690
  when Array
691
+ # Array values: [timed_event, timed_event, ...]
283
692
  result = []
284
693
  source_value_value.each_index do |index|
285
694
  result[index] = { time: time, value: source_value_value[index] }.extend(Musa::Datasets::AbsTimed)
286
695
 
696
+ # Distribute extra attributes by index
287
697
  source_value_extra.each do |attribute_name, attribute_value|
288
698
  result[index][attribute_name] = attribute_value[index]
289
699
  end
290
700
  end
291
701
  else
702
+ # Direct values: pass through unchanged
292
703
  result = source_value.clone.extend(Musa::Datasets::AbsTimed)
293
704
  end
294
705
 
@@ -298,47 +709,81 @@ module Musa
298
709
  end
299
710
  end
300
711
 
712
+ # Checks if source is infinite.
713
+ # @return [Boolean] true if source infinite
714
+ # @api private
301
715
  def infinite?
302
716
  @source.infinite?
303
717
  end
304
718
  end
305
719
 
306
720
  private_constant :TimedFlattener
307
- end
308
721
 
309
- class TimedCompacter
310
- include Series::Serie.with(source: true)
722
+ # Internal implementation for compacting timed series.
723
+ #
724
+ # Filters out events where all values are nil, removing temporal gaps.
725
+ # Checks value structure to determine if entire event should be skipped.
726
+ #
727
+ # @api private
728
+ class TimedCompacter
729
+ include Series::Serie.with(source: true)
311
730
 
312
- def initialize(serie)
313
- self.source = serie
314
- init
315
- end
731
+ # Creates compacter wrapping source serie.
732
+ #
733
+ # @param serie [Serie] source timed serie
734
+ # @api private
735
+ def initialize(serie)
736
+ self.source = serie
737
+ init
738
+ end
316
739
 
317
- private def _restart
318
- @source.restart
319
- end
740
+ # Restarts source serie.
741
+ # @api private
742
+ private def _restart
743
+ @source.restart
744
+ end
320
745
 
321
- private def _next_value
322
- while (source_value = @source.next_value) && skip_value?(source_value[:value]); end
323
- source_value
324
- end
746
+ # Generates next non-nil value from source.
747
+ #
748
+ # Skips source values while they contain only nil values (direct nil,
749
+ # all-nil hash, or all-nil array). Returns first event with any non-nil.
750
+ #
751
+ # @return [Hash, nil] timed event with non-nil values, or nil when exhausted
752
+ # @api private
753
+ private def _next_value
754
+ while (source_value = @source.next_value) && skip_value?(source_value[:value]); end
755
+ source_value
756
+ end
325
757
 
326
- def infinite?
327
- @source.infinite?
328
- end
758
+ # Checks if source is infinite.
759
+ # @return [Boolean] true if source infinite
760
+ # @api private
761
+ def infinite?
762
+ @source.infinite?
763
+ end
329
764
 
330
- private def skip_value?(timed_value)
331
- case timed_value
332
- when Hash
333
- timed_value.all? { |_, v| v.nil? }
334
- when Array
335
- timed_value.all?(&:nil?)
336
- else
337
- timed_value.nil?
765
+ # Determines if value should be skipped (all-nil check).
766
+ #
767
+ # **Hash**: All values nil → skip
768
+ # **Array**: All elements nil → skip
769
+ # **Direct**: Value is nil → skip
770
+ #
771
+ # @param timed_value [Hash, Array, Object] value to check
772
+ # @return [Boolean] true if should skip
773
+ # @api private
774
+ private def skip_value?(timed_value)
775
+ case timed_value
776
+ when Hash
777
+ timed_value.all? { |_, v| v.nil? }
778
+ when Array
779
+ timed_value.all?(&:nil?)
780
+ else
781
+ timed_value.nil?
782
+ end
338
783
  end
339
784
  end
340
- end
341
785
 
342
- private_constant :TimedCompacter
786
+ private_constant :TimedCompacter
787
+ end
343
788
  end
344
789