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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- 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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
@
|
|
319
|
-
|
|
740
|
+
# Restarts source serie.
|
|
741
|
+
# @api private
|
|
742
|
+
private def _restart
|
|
743
|
+
@source.restart
|
|
744
|
+
end
|
|
320
745
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
@source
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
786
|
+
private_constant :TimedCompacter
|
|
787
|
+
end
|
|
343
788
|
end
|
|
344
789
|
|