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
|
@@ -1,21 +1,172 @@
|
|
|
1
1
|
require_relative '../core-ext/smart-proc-binder'
|
|
2
2
|
require_relative '../core-ext/with'
|
|
3
3
|
|
|
4
|
-
# TODO: hacer que pueda funcionar en tiempo real? le vas suministrando seeds y le vas diciendo qué opción has elegido (p.ej. para hacer un armonizador en tiempo real)
|
|
5
|
-
# TODO: esto mismo sería aplicable en otros generadores? variatio/darwin? generative-grammar? markov?
|
|
6
|
-
# TODO: optimizar la llamada a .with que internamente genera cada vez un SmartProcBinder; podría generarse sólo una vez por cada &block
|
|
7
|
-
|
|
8
4
|
module Musa
|
|
5
|
+
# Rule-based production system with growth and pruning.
|
|
6
|
+
#
|
|
7
|
+
# Rules implements a production system that generates tree structures
|
|
8
|
+
# by applying growth rules to produce branches and pruning rules to
|
|
9
|
+
# eliminate invalid paths. Similar to L-systems and production systems
|
|
10
|
+
# in formal grammars, but with validation and constraint satisfaction.
|
|
11
|
+
#
|
|
12
|
+
# ## Core Concepts
|
|
13
|
+
#
|
|
14
|
+
# - **Grow Rules**: Transform objects into new possibilities (branches)
|
|
15
|
+
# - **Cut Rules**: Prune branches that violate constraints
|
|
16
|
+
# - **End Condition**: Mark branches as complete
|
|
17
|
+
# - **Tree**: Hierarchical structure of all valid possibilities
|
|
18
|
+
# - **History**: Path from root to current node
|
|
19
|
+
# - **Combinations**: All valid complete paths through tree
|
|
20
|
+
#
|
|
21
|
+
# ## Generation Process
|
|
22
|
+
#
|
|
23
|
+
# 1. **Seed**: Start with initial object(s)
|
|
24
|
+
# 2. **Grow**: Apply grow rules sequentially to create branches
|
|
25
|
+
# 3. **Validate**: Apply cut rules to prune invalid branches
|
|
26
|
+
# 4. **Check End**: Mark branches meeting end condition
|
|
27
|
+
# 5. **Recurse**: Continue growing non-ended branches
|
|
28
|
+
# 6. **Collect**: Gather all valid complete paths
|
|
29
|
+
#
|
|
30
|
+
# ## Rule Application
|
|
31
|
+
#
|
|
32
|
+
# Rules are applied in definition order. Each grow rule can produce
|
|
33
|
+
# multiple branches via `branch`. Cut rules can prune with `prune`.
|
|
34
|
+
# The system tracks history (path to current node) for context-aware
|
|
35
|
+
# rule application.
|
|
36
|
+
#
|
|
37
|
+
# ## Musical Applications
|
|
38
|
+
#
|
|
39
|
+
# - Generate harmonic progressions with voice leading rules
|
|
40
|
+
# - Create melodic variations with contour constraints
|
|
41
|
+
# - Produce rhythmic patterns following metric rules
|
|
42
|
+
# - Build counterpoint with species rules
|
|
43
|
+
# - Generate chord voicings with spacing constraints
|
|
44
|
+
#
|
|
45
|
+
# @example Basic chord progression rules
|
|
46
|
+
# rules = Musa::Rules::Rules.new do
|
|
47
|
+
# # Generate possible next chords
|
|
48
|
+
# grow 'next chord' do |chord, history|
|
|
49
|
+
# case chord
|
|
50
|
+
# when :I then branch(:ii); branch(:IV); branch(:V)
|
|
51
|
+
# when :ii then branch(:V); branch(:vii)
|
|
52
|
+
# when :IV then branch(:I); branch(:V)
|
|
53
|
+
# when :V then branch(:I); branch(:vi)
|
|
54
|
+
# when :vi then branch(:ii); branch(:IV)
|
|
55
|
+
# when :vii then branch(:I)
|
|
56
|
+
# end
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# # Avoid parallel fifths
|
|
60
|
+
# cut 'parallel fifths' do |chord, history|
|
|
61
|
+
# prune if has_parallel_fifths?(history + [chord])
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# # End after 4 chords
|
|
65
|
+
# ended_when do |chord, history|
|
|
66
|
+
# history.size == 4
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# tree = rules.apply([:I])
|
|
71
|
+
# progressions = tree.combinations
|
|
72
|
+
# # => [[:I, :ii, :V, :I], [:I, :IV, :V, :I], ...]
|
|
73
|
+
#
|
|
74
|
+
# @example Melodic contour rules with parameters
|
|
75
|
+
# rules = Musa::Rules::Rules.new do
|
|
76
|
+
# grow 'next note' do |pitch, history, max_interval:|
|
|
77
|
+
# # Try intervals within max_interval
|
|
78
|
+
# (-max_interval..max_interval).each do |interval|
|
|
79
|
+
# branch pitch + interval unless interval.zero?
|
|
80
|
+
# end
|
|
81
|
+
# end
|
|
82
|
+
#
|
|
83
|
+
# cut 'range limit' do |pitch, history|
|
|
84
|
+
# prune if pitch < 60 || pitch > 84 # C4 to C6
|
|
85
|
+
# end
|
|
86
|
+
#
|
|
87
|
+
# cut 'no large leaps' do |pitch, history|
|
|
88
|
+
# prune if history.last && (pitch - history.last).abs > 7
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
# ended_when do |pitch, history|
|
|
92
|
+
# history.size == 8 # 8-note melody
|
|
93
|
+
# end
|
|
94
|
+
# end
|
|
95
|
+
#
|
|
96
|
+
# tree = rules.apply([60], max_interval: 3)
|
|
97
|
+
# melodies = tree.combinations
|
|
98
|
+
#
|
|
99
|
+
# @example Rhythm pattern generation
|
|
100
|
+
# rules = Musa::Rules::Rules.new do
|
|
101
|
+
# grow 'add duration' do |pattern, history, remaining:|
|
|
102
|
+
# [1/4r, 1/8r, 1/16r].each do |dur|
|
|
103
|
+
# if dur <= remaining
|
|
104
|
+
# branch pattern + [dur]
|
|
105
|
+
# end
|
|
106
|
+
# end
|
|
107
|
+
# end
|
|
108
|
+
#
|
|
109
|
+
# cut 'too many sixteenths' do |pattern, history|
|
|
110
|
+
# sixteenths = pattern.count { |d| d == 1/16r }
|
|
111
|
+
# prune if sixteenths > 4
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# ended_when do |pattern, history, remaining:|
|
|
115
|
+
# pattern.sum >= remaining
|
|
116
|
+
# end
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# tree = rules.apply([], remaining: 1r) # One bar
|
|
120
|
+
# rhythms = tree.combinations
|
|
121
|
+
#
|
|
122
|
+
# @see Rules Main rule-based generator class
|
|
123
|
+
# @see Musa::Extension::SmartProcBinder Smart procedure binding for rule evaluation
|
|
124
|
+
# @see Musa::Extension::Arrayfy Array conversion for seed objects
|
|
125
|
+
# @see Musa::Extension::With DSL context management for rule definitions
|
|
126
|
+
# @see https://en.wikipedia.org/wiki/Production_system_(computer_science) Production systems (Wikipedia)
|
|
127
|
+
# @see https://en.wikipedia.org/wiki/L-system L-systems (Wikipedia)
|
|
9
128
|
module Rules
|
|
129
|
+
# TODO: hacer que pueda funcionar en tiempo real? le vas suministrando seeds y le vas diciendo qué opción has elegido (p.ej. para hacer un armonizador en tiempo real)
|
|
130
|
+
# TODO: esto mismo sería aplicable en otros generadores? variatio/darwin? generative-grammar? markov?
|
|
131
|
+
# TODO: optimizar la llamada a .with que internamente genera cada vez un SmartProcBinder; podría generarse sólo una vez por cada &block
|
|
132
|
+
|
|
10
133
|
using Musa::Extension::Arrayfy
|
|
11
134
|
|
|
135
|
+
# Rule-based generator with growth and pruning.
|
|
136
|
+
#
|
|
137
|
+
# Applies grow/cut rules to generate tree of valid possibilities.
|
|
12
138
|
class Rules
|
|
13
139
|
include Musa::Extension::With
|
|
14
140
|
|
|
141
|
+
# Creates rule system with defined rules.
|
|
142
|
+
#
|
|
143
|
+
# @yield rule definition DSL block
|
|
144
|
+
# @yieldreturn [void]
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# rules = Rules.new do
|
|
148
|
+
# grow 'generate' { |obj| branch new_obj }
|
|
149
|
+
# cut 'validate' { |obj| prune if invalid?(obj) }
|
|
150
|
+
# ended_when { |obj| complete?(obj) }
|
|
151
|
+
# end
|
|
15
152
|
def initialize(&block)
|
|
16
153
|
@dsl = RulesEvalContext.new(&block)
|
|
17
154
|
end
|
|
18
155
|
|
|
156
|
+
# Generates possibility tree from object.
|
|
157
|
+
#
|
|
158
|
+
# Recursively applies grow rules to create branches, cut rules to
|
|
159
|
+
# prune invalid paths, and end conditions to mark complete branches.
|
|
160
|
+
#
|
|
161
|
+
# @param object [Object] object to expand
|
|
162
|
+
# @param confirmed_node [Node, nil] confirmed parent node (for history)
|
|
163
|
+
# @param node [Node, nil] current node being built
|
|
164
|
+
# @param grow_rules [Array<GrowRule>, nil] rules to apply
|
|
165
|
+
# @param parameters [Hash] additional parameters for rules
|
|
166
|
+
#
|
|
167
|
+
# @return [Node] root node of possibility tree
|
|
168
|
+
#
|
|
169
|
+
# @api private
|
|
19
170
|
def generate_possibilities(object, confirmed_node = nil, node = nil, grow_rules = nil, **parameters)
|
|
20
171
|
node ||= Node.new
|
|
21
172
|
grow_rules ||= @dsl._grow_rules
|
|
@@ -53,6 +204,25 @@ module Musa
|
|
|
53
204
|
node
|
|
54
205
|
end
|
|
55
206
|
|
|
207
|
+
# Applies rules to seed objects sequentially.
|
|
208
|
+
#
|
|
209
|
+
# Processes list of seed objects in sequence, generating possibilities
|
|
210
|
+
# from each confirmed endpoint of previous seed. Returns tree of all
|
|
211
|
+
# valid combination paths.
|
|
212
|
+
#
|
|
213
|
+
# @param object_or_list [Object, Array] seed object(s) to process
|
|
214
|
+
# @param node [Node, nil] root node (creates if nil)
|
|
215
|
+
# @param parameters [Hash] additional parameters for rules
|
|
216
|
+
#
|
|
217
|
+
# @return [Node] root node with all combination paths
|
|
218
|
+
#
|
|
219
|
+
# @example Single seed
|
|
220
|
+
# tree = rules.apply(:I)
|
|
221
|
+
# tree.combinations # => [[:I, :ii, :V, :I], ...]
|
|
222
|
+
#
|
|
223
|
+
# @example Multiple seeds
|
|
224
|
+
# tree = rules.apply([:I, :ii, :V])
|
|
225
|
+
# tree.combinations # => combinations starting from each seed
|
|
56
226
|
def apply(object_or_list, node = nil, **parameters)
|
|
57
227
|
list = object_or_list.arrayfy.clone
|
|
58
228
|
|
|
@@ -76,27 +246,51 @@ module Musa
|
|
|
76
246
|
node
|
|
77
247
|
end
|
|
78
248
|
|
|
249
|
+
# DSL context for rule definitions.
|
|
250
|
+
#
|
|
251
|
+
# @api private
|
|
79
252
|
class RulesEvalContext
|
|
80
253
|
include Musa::Extension::With
|
|
81
254
|
|
|
255
|
+
# @return [Array<GrowRule>] grow rules
|
|
256
|
+
# @return [Proc, nil] end condition
|
|
257
|
+
# @return [Array<CutRule>] cut rules
|
|
82
258
|
attr_reader :_grow_rules, :_ended_when, :_cut_rules
|
|
83
259
|
|
|
260
|
+
# @api private
|
|
84
261
|
def initialize(&block)
|
|
85
262
|
@_grow_rules = []
|
|
86
263
|
@_cut_rules = []
|
|
87
264
|
with &block
|
|
88
265
|
end
|
|
89
266
|
|
|
267
|
+
# Defines grow rule.
|
|
268
|
+
#
|
|
269
|
+
# @param name [String] rule name for debugging
|
|
270
|
+
# @yield [object, history, **params] rule block
|
|
271
|
+
# @return [self]
|
|
272
|
+
# @api private
|
|
90
273
|
def grow(name, &block)
|
|
91
274
|
@_grow_rules << GrowRule.new(name, &block)
|
|
92
275
|
self
|
|
93
276
|
end
|
|
94
277
|
|
|
278
|
+
# Defines end condition.
|
|
279
|
+
#
|
|
280
|
+
# @yield [object, history, **params] condition block
|
|
281
|
+
# @return [self]
|
|
282
|
+
# @api private
|
|
95
283
|
def ended_when(&block)
|
|
96
284
|
@_ended_when = block
|
|
97
285
|
self
|
|
98
286
|
end
|
|
99
287
|
|
|
288
|
+
# Defines cut/pruning rule.
|
|
289
|
+
#
|
|
290
|
+
# @param reason [String] rejection reason
|
|
291
|
+
# @yield [object, history, **params] pruning block
|
|
292
|
+
# @return [self]
|
|
293
|
+
# @api private
|
|
100
294
|
def cut(reason, &block)
|
|
101
295
|
@_cut_rules << CutRule.new(reason, &block)
|
|
102
296
|
self
|
|
@@ -191,9 +385,21 @@ module Musa
|
|
|
191
385
|
|
|
192
386
|
private_constant :RulesEvalContext
|
|
193
387
|
|
|
388
|
+
# Tree node representing possibility in generation.
|
|
389
|
+
#
|
|
390
|
+
# Nodes form tree structure of generation possibilities.
|
|
391
|
+
# Each node has object, parent, children, rejection status, and end flag.
|
|
392
|
+
#
|
|
393
|
+
# @attr_reader parent [Node, nil] parent node
|
|
394
|
+
# @attr_reader children [Array<Node>] child nodes
|
|
395
|
+
# @attr_reader object [Object, nil] node object
|
|
396
|
+
# @attr_reader rejected [String, Array<String>, nil] rejection reason(s)
|
|
397
|
+
#
|
|
398
|
+
# @api private
|
|
194
399
|
class Node
|
|
195
400
|
attr_reader :parent, :children, :object, :rejected
|
|
196
401
|
|
|
402
|
+
# @api private
|
|
197
403
|
def initialize(object = nil, parent = nil)
|
|
198
404
|
@parent = parent
|
|
199
405
|
@children = []
|
|
@@ -203,15 +409,31 @@ module Musa
|
|
|
203
409
|
@rejected = nil
|
|
204
410
|
end
|
|
205
411
|
|
|
412
|
+
# Adds child node.
|
|
413
|
+
#
|
|
414
|
+
# @param object [Object] child object
|
|
415
|
+
# @return [Node] created child node
|
|
416
|
+
# @api private
|
|
206
417
|
def add(object)
|
|
207
418
|
Node.new(object, self).tap { |n| @children << n }
|
|
208
419
|
end
|
|
209
420
|
|
|
421
|
+
# Marks node as rejected.
|
|
422
|
+
#
|
|
423
|
+
# @param rejection [String, Array<String>] rejection reason(s)
|
|
424
|
+
# @return [self]
|
|
425
|
+
# @api private
|
|
210
426
|
def reject!(rejection)
|
|
211
427
|
@rejected = rejection
|
|
212
428
|
self
|
|
213
429
|
end
|
|
214
430
|
|
|
431
|
+
# Marks node as ended/complete.
|
|
432
|
+
#
|
|
433
|
+
# Propagates rejection if all children rejected.
|
|
434
|
+
#
|
|
435
|
+
# @return [self]
|
|
436
|
+
# @api private
|
|
215
437
|
def mark_as_ended!
|
|
216
438
|
@children.each(&:update_rejection_by_children!)
|
|
217
439
|
|
|
@@ -224,10 +446,18 @@ module Musa
|
|
|
224
446
|
self
|
|
225
447
|
end
|
|
226
448
|
|
|
449
|
+
# Checks if node is ended.
|
|
450
|
+
#
|
|
451
|
+
# @return [Boolean]
|
|
452
|
+
# @api private
|
|
227
453
|
def ended?
|
|
228
454
|
@ended
|
|
229
455
|
end
|
|
230
456
|
|
|
457
|
+
# Returns path from root to this node.
|
|
458
|
+
#
|
|
459
|
+
# @return [Array<Object>] history of objects
|
|
460
|
+
# @api private
|
|
231
461
|
def history
|
|
232
462
|
objects = []
|
|
233
463
|
n = self
|
|
@@ -239,6 +469,13 @@ module Musa
|
|
|
239
469
|
objects.reverse
|
|
240
470
|
end
|
|
241
471
|
|
|
472
|
+
# Collects objects from ended leaf nodes.
|
|
473
|
+
#
|
|
474
|
+
# Recursively gathers objects from all valid ended branches,
|
|
475
|
+
# excluding rejected paths.
|
|
476
|
+
#
|
|
477
|
+
# @return [Array<Object>] objects from valid endpoints
|
|
478
|
+
# @api private
|
|
242
479
|
def fish
|
|
243
480
|
fished = []
|
|
244
481
|
|
|
@@ -255,6 +492,23 @@ module Musa
|
|
|
255
492
|
fished
|
|
256
493
|
end
|
|
257
494
|
|
|
495
|
+
# Returns all valid combination paths.
|
|
496
|
+
#
|
|
497
|
+
# Recursively builds complete paths from root to all valid
|
|
498
|
+
# leaf nodes, excluding rejected branches.
|
|
499
|
+
#
|
|
500
|
+
# @param parent_combination [Array, nil] parent path
|
|
501
|
+
#
|
|
502
|
+
# @return [Array<Array<Object>>] all valid complete paths
|
|
503
|
+
#
|
|
504
|
+
# @example
|
|
505
|
+
# tree.combinations
|
|
506
|
+
# # => [
|
|
507
|
+
# # [:I, :ii, :V, :I],
|
|
508
|
+
# # [:I, :IV, :V, :I],
|
|
509
|
+
# # ...
|
|
510
|
+
# # ]
|
|
511
|
+
# @api private
|
|
258
512
|
def combinations(parent_combination = nil)
|
|
259
513
|
parent_combination ||= []
|
|
260
514
|
|
|
@@ -2,14 +2,147 @@ require_relative '../core-ext/smart-proc-binder'
|
|
|
2
2
|
require_relative '../core-ext/arrayfy'
|
|
3
3
|
require_relative '../core-ext/with'
|
|
4
4
|
|
|
5
|
-
# TODO: permitir definir un variatio a través de llamadas a métodos y/o atributos, además de a través del block del constructor
|
|
6
|
-
|
|
7
5
|
module Musa
|
|
6
|
+
# Combinatorial variation generator with Cartesian product.
|
|
7
|
+
#
|
|
8
|
+
# Variatio generates all possible combinations of parameter values across
|
|
9
|
+
# defined fields, creating comprehensive variation sets. Uses Cartesian
|
|
10
|
+
# product to produce exhaustive parameter combinations, then constructs
|
|
11
|
+
# objects and applies attribute modifications.
|
|
12
|
+
#
|
|
13
|
+
# ## Core Concepts
|
|
14
|
+
#
|
|
15
|
+
# - **Fields**: Named parameters with option sets
|
|
16
|
+
# - **Fieldsets**: Nested field groups with their own options
|
|
17
|
+
# - **Constructor**: Creates base objects from field values
|
|
18
|
+
# - **with_attributes**: Modifies objects with field/fieldset values
|
|
19
|
+
# - **Finalize**: Post-processes completed objects
|
|
20
|
+
# - **Variations**: All Cartesian product combinations
|
|
21
|
+
#
|
|
22
|
+
# ## Generation Process
|
|
23
|
+
#
|
|
24
|
+
# 1. **Define**: Specify fields, fieldsets, constructor, attributes, finalize
|
|
25
|
+
# 2. **Combine**: Calculate Cartesian product of all field options
|
|
26
|
+
# 3. **Construct**: Create objects using constructor with each combination
|
|
27
|
+
# 4. **Attribute**: Apply with_attributes blocks for each combination
|
|
28
|
+
# 5. **Finalize**: Run finalize block on completed objects
|
|
29
|
+
# 6. **Return**: Array of all generated variations
|
|
30
|
+
#
|
|
31
|
+
# ## Musical Applications
|
|
32
|
+
#
|
|
33
|
+
# - Generate all variations of a musical motif
|
|
34
|
+
# - Create comprehensive parameter sweeps for synthesis
|
|
35
|
+
# - Produce complete harmonic permutations
|
|
36
|
+
# - Build exhaustive rhythm pattern combinations
|
|
37
|
+
#
|
|
38
|
+
# @example Basic field variations
|
|
39
|
+
# variatio = Musa::Variatio::Variatio.new :chord do
|
|
40
|
+
# field :root, [60, 64, 67] # C, E, G
|
|
41
|
+
# field :type, [:major, :minor]
|
|
42
|
+
#
|
|
43
|
+
# constructor do |root:, type:|
|
|
44
|
+
# { root: root, type: type }
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# variations = variatio.run
|
|
49
|
+
# # => [
|
|
50
|
+
# # { root: 60, type: :major },
|
|
51
|
+
# # { root: 60, type: :minor },
|
|
52
|
+
# # { root: 64, type: :major },
|
|
53
|
+
# # { root: 64, type: :minor },
|
|
54
|
+
# # { root: 67, type: :major },
|
|
55
|
+
# # { root: 67, type: :minor }
|
|
56
|
+
# # ]
|
|
57
|
+
# # 3 roots × 2 types = 6 variations
|
|
58
|
+
#
|
|
59
|
+
# @example Override field options at runtime
|
|
60
|
+
# variatio = Musa::Variatio::Variatio.new :object do
|
|
61
|
+
# field :a, 1..10
|
|
62
|
+
# field :b, [:alfa, :beta, :gamma]
|
|
63
|
+
#
|
|
64
|
+
# constructor { |a:, b:| { a: a, b: b } }
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# # Override :a to limit variations
|
|
68
|
+
# variatio.on(a: 1..3)
|
|
69
|
+
# # => 3 × 3 = 9 variations instead of 10 × 3 = 30
|
|
70
|
+
#
|
|
71
|
+
# @example Nested fieldsets with attributes
|
|
72
|
+
# variatio = Musa::Variatio::Variatio.new :synth do
|
|
73
|
+
# field :wave, [:saw, :square]
|
|
74
|
+
# field :cutoff, [500, 1000, 2000]
|
|
75
|
+
#
|
|
76
|
+
# constructor do |wave:, cutoff:|
|
|
77
|
+
# { wave: wave, cutoff: cutoff, lfo: {} }
|
|
78
|
+
# end
|
|
79
|
+
#
|
|
80
|
+
# # Nested fieldset for LFO parameters
|
|
81
|
+
# fieldset :lfo, [:vibrato, :tremolo] do
|
|
82
|
+
# field :rate, [4, 8]
|
|
83
|
+
# field :depth, [0.1, 0.5]
|
|
84
|
+
#
|
|
85
|
+
# with_attributes do |synth:, lfo:, rate:, depth:|
|
|
86
|
+
# synth[:lfo][lfo] = { rate: rate, depth: depth }
|
|
87
|
+
# end
|
|
88
|
+
# end
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
# variations = variatio.run
|
|
92
|
+
# # => 2 waves × 3 cutoffs × 2 lfo types × 2 rates × 2 depths = 48 variations
|
|
93
|
+
#
|
|
94
|
+
# @example With finalize block
|
|
95
|
+
# variatio = Musa::Variatio::Variatio.new :note do
|
|
96
|
+
# field :pitch, [60, 62, 64]
|
|
97
|
+
# field :velocity, [64, 96, 127]
|
|
98
|
+
#
|
|
99
|
+
# constructor { |pitch:, velocity:| { pitch: pitch, velocity: velocity } }
|
|
100
|
+
#
|
|
101
|
+
# finalize do |note:|
|
|
102
|
+
# note[:loudness] = note[:velocity] / 127.0
|
|
103
|
+
# note[:dynamics] = case note[:velocity]
|
|
104
|
+
# when 0..48 then :pp
|
|
105
|
+
# when 49..80 then :mf
|
|
106
|
+
# else :ff
|
|
107
|
+
# end
|
|
108
|
+
# end
|
|
109
|
+
# end
|
|
110
|
+
#
|
|
111
|
+
# @see Variatio Main combinatorial variation generator class
|
|
112
|
+
# @see Musa::Extension::SmartProcBinder Smart procedure binding for constructor/finalize blocks
|
|
113
|
+
# @see Musa::Extension::Arrayfy Array conversion utilities for field options
|
|
114
|
+
# @see Musa::Extension::With DSL context management for field definitions
|
|
115
|
+
# @see https://en.wikipedia.org/wiki/Cartesian_product Cartesian product (Wikipedia)
|
|
116
|
+
# @see https://en.wikipedia.org/wiki/Variation_(mathematics) Variation in mathematics (Wikipedia)
|
|
117
|
+
#
|
|
118
|
+
# @api public
|
|
8
119
|
module Variatio
|
|
9
120
|
using Musa::Extension::Arrayfy
|
|
10
121
|
using Musa::Extension::ExplodeRanges
|
|
11
122
|
|
|
123
|
+
# TODO: permitir definir un variatio a través de llamadas a métodos y/o atributos, además de a través del block del constructor
|
|
124
|
+
|
|
125
|
+
# Combinatorial variation generator.
|
|
126
|
+
#
|
|
127
|
+
# Generates all combinations of field values using Cartesian product,
|
|
128
|
+
# constructs objects, applies attributes, and optionally finalizes.
|
|
12
129
|
class Variatio
|
|
130
|
+
# Creates variation generator with field definitions.
|
|
131
|
+
#
|
|
132
|
+
# @param instance_name [Symbol] name for object parameter in blocks
|
|
133
|
+
#
|
|
134
|
+
# @yield DSL block defining fields, constructor, attributes, finalize
|
|
135
|
+
# @yieldreturn [void]
|
|
136
|
+
#
|
|
137
|
+
# @raise [ArgumentError] if instance_name not a symbol
|
|
138
|
+
# @raise [ArgumentError] if no block given
|
|
139
|
+
#
|
|
140
|
+
# @example
|
|
141
|
+
# variatio = Variatio.new :obj do
|
|
142
|
+
# field :x, [1, 2, 3]
|
|
143
|
+
# field :y, [:a, :b]
|
|
144
|
+
# constructor { |x:, y:| { x: x, y: y } }
|
|
145
|
+
# end
|
|
13
146
|
def initialize(instance_name, &block)
|
|
14
147
|
raise ArgumentError, 'instance_name should be a symbol' unless instance_name.is_a?(Symbol)
|
|
15
148
|
raise ArgumentError, 'block is needed' unless block
|
|
@@ -23,6 +156,29 @@ module Musa
|
|
|
23
156
|
@finalize = main_context._finalize
|
|
24
157
|
end
|
|
25
158
|
|
|
159
|
+
# Generates variations with runtime field value overrides.
|
|
160
|
+
#
|
|
161
|
+
# Allows overriding field options at generation time, useful for
|
|
162
|
+
# limiting variation space or parameterizing generation.
|
|
163
|
+
#
|
|
164
|
+
# @param values [Hash{Symbol => Array, Range}] field overrides
|
|
165
|
+
# Keys are field names, values are option arrays or ranges
|
|
166
|
+
#
|
|
167
|
+
# @return [Array] all generated variation objects
|
|
168
|
+
#
|
|
169
|
+
# @example Override field values
|
|
170
|
+
# variatio = Variatio.new :obj do
|
|
171
|
+
# field :x, 1..10
|
|
172
|
+
# field :y, [:a, :b, :c]
|
|
173
|
+
# constructor { |x:, y:| { x: x, y: y } }
|
|
174
|
+
# end
|
|
175
|
+
#
|
|
176
|
+
# # Default: 10 × 3 = 30 variations
|
|
177
|
+
# variatio.run.size # => 30
|
|
178
|
+
#
|
|
179
|
+
# # Override :x to limit variations
|
|
180
|
+
# variatio.on(x: 1..3).size # => 3 × 3 = 9
|
|
181
|
+
# variatio.on(x: [5], y: [:a]).size # => 1 × 1 = 1
|
|
26
182
|
def on(**values)
|
|
27
183
|
constructor_binder = Musa::Extension::SmartProcBinder::SmartProcBinder.new @constructor
|
|
28
184
|
finalize_binder = Musa::Extension::SmartProcBinder::SmartProcBinder.new @finalize if @finalize
|
|
@@ -60,6 +216,25 @@ module Musa
|
|
|
60
216
|
combinations
|
|
61
217
|
end
|
|
62
218
|
|
|
219
|
+
# Generates all variations with default field values.
|
|
220
|
+
#
|
|
221
|
+
# Equivalent to calling {#on} with no overrides.
|
|
222
|
+
#
|
|
223
|
+
# @return [Array] all generated variation objects
|
|
224
|
+
#
|
|
225
|
+
# @example
|
|
226
|
+
# variatio = Variatio.new :obj do
|
|
227
|
+
# field :x, [1, 2, 3]
|
|
228
|
+
# field :y, [:a, :b]
|
|
229
|
+
# constructor { |x:, y:| { x: x, y: y } }
|
|
230
|
+
# end
|
|
231
|
+
#
|
|
232
|
+
# variations = variatio.run
|
|
233
|
+
# # => [
|
|
234
|
+
# # { x: 1, y: :a }, { x: 1, y: :b },
|
|
235
|
+
# # { x: 2, y: :a }, { x: 2, y: :b },
|
|
236
|
+
# # { x: 3, y: :a }, { x: 3, y: :b }
|
|
237
|
+
# # ]
|
|
63
238
|
def run
|
|
64
239
|
on
|
|
65
240
|
end
|
|
@@ -199,6 +374,12 @@ module Musa
|
|
|
199
374
|
|
|
200
375
|
private_constant :A2
|
|
201
376
|
|
|
377
|
+
# Internal tree node for attribute application phase.
|
|
378
|
+
#
|
|
379
|
+
# Manages execution of `with_attributes` blocks during variation generation.
|
|
380
|
+
# Coordinates attribute application across field hierarchy.
|
|
381
|
+
#
|
|
382
|
+
# @api private
|
|
202
383
|
class B
|
|
203
384
|
attr_reader :parameter_name, :options, :affected_field_names, :blocks, :inner
|
|
204
385
|
|
|
@@ -245,26 +426,46 @@ module Musa
|
|
|
245
426
|
private
|
|
246
427
|
end
|
|
247
428
|
|
|
429
|
+
# DSL context for fieldset definition.
|
|
430
|
+
#
|
|
431
|
+
# @api private
|
|
248
432
|
class FieldsetContext
|
|
249
433
|
include Musa::Extension::With
|
|
250
434
|
|
|
435
|
+
# @return [Fieldset] defined fieldset
|
|
251
436
|
attr_reader :_fieldset
|
|
252
437
|
|
|
438
|
+
# @api private
|
|
253
439
|
def initialize(name, options = nil, &block)
|
|
254
440
|
@_fieldset = Fieldset.new name, options.arrayfy.explode_ranges
|
|
255
441
|
|
|
256
442
|
with &block
|
|
257
443
|
end
|
|
258
444
|
|
|
445
|
+
# Defines a field with options.
|
|
446
|
+
#
|
|
447
|
+
# @param name [Symbol] field name
|
|
448
|
+
# @param options [Array, Range, nil] field option values
|
|
449
|
+
# @api private
|
|
259
450
|
def field(name, options = nil)
|
|
260
451
|
@_fieldset.components << Field.new(name, options.arrayfy.explode_ranges)
|
|
261
452
|
end
|
|
262
453
|
|
|
454
|
+
# Defines nested fieldset.
|
|
455
|
+
#
|
|
456
|
+
# @param name [Symbol] fieldset name
|
|
457
|
+
# @param options [Array, Range, nil] fieldset option values
|
|
458
|
+
# @yield fieldset DSL block
|
|
459
|
+
# @api private
|
|
263
460
|
def fieldset(name, options = nil, &block)
|
|
264
461
|
fieldset_context = FieldsetContext.new name, options, &block
|
|
265
462
|
@_fieldset.components << fieldset_context._fieldset
|
|
266
463
|
end
|
|
267
464
|
|
|
465
|
+
# Adds attribute modification block.
|
|
466
|
+
#
|
|
467
|
+
# @yield attribute modification block
|
|
468
|
+
# @api private
|
|
268
469
|
def with_attributes(&block)
|
|
269
470
|
@_fieldset.with_attributes << block
|
|
270
471
|
end
|
|
@@ -272,9 +473,15 @@ module Musa
|
|
|
272
473
|
|
|
273
474
|
private_constant :FieldsetContext
|
|
274
475
|
|
|
476
|
+
# DSL context for main Variatio configuration.
|
|
477
|
+
#
|
|
478
|
+
# @api private
|
|
275
479
|
class MainContext < FieldsetContext
|
|
480
|
+
# @return [Proc] constructor block
|
|
481
|
+
# @return [Proc, nil] finalize block
|
|
276
482
|
attr_reader :_constructor, :_finalize
|
|
277
483
|
|
|
484
|
+
# @api private
|
|
278
485
|
def initialize(&block)
|
|
279
486
|
@_constructor = nil
|
|
280
487
|
@_finalize = nil
|
|
@@ -282,10 +489,18 @@ module Musa
|
|
|
282
489
|
super :_maincontext, [nil], &block
|
|
283
490
|
end
|
|
284
491
|
|
|
492
|
+
# Defines object constructor.
|
|
493
|
+
#
|
|
494
|
+
# @yield constructor block receiving field values
|
|
495
|
+
# @api private
|
|
285
496
|
def constructor(&block)
|
|
286
497
|
@_constructor = block
|
|
287
498
|
end
|
|
288
499
|
|
|
500
|
+
# Defines finalize block.
|
|
501
|
+
#
|
|
502
|
+
# @yield finalize block receiving completed object
|
|
503
|
+
# @api private
|
|
289
504
|
def finalize(&block)
|
|
290
505
|
@_finalize = block
|
|
291
506
|
end
|