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,9 +1,126 @@
|
|
|
1
1
|
module Musa
|
|
2
|
+
# Generative grammar system for creating formal grammars with combinatorial generation.
|
|
3
|
+
#
|
|
4
|
+
# GenerativeGrammar provides a DSL for defining formal grammars that can generate
|
|
5
|
+
# all possible combinations of terminal and non-terminal symbols according to
|
|
6
|
+
# production rules. Similar to context-free grammars in formal language theory.
|
|
7
|
+
#
|
|
8
|
+
# ## Core Concepts
|
|
9
|
+
#
|
|
10
|
+
# - **Nodes (N)**: Terminal or block nodes with content and attributes
|
|
11
|
+
# - **Proxy Nodes (PN)**: Placeholder nodes for recursive grammar definitions
|
|
12
|
+
# - **Operators**: Combine nodes to form production rules
|
|
13
|
+
# - `|` (or): Alternative/choice between nodes
|
|
14
|
+
# - `+` (next): Concatenation/sequence of nodes
|
|
15
|
+
# - **Modifiers**:
|
|
16
|
+
# - `repeat(min:, max:)`: Repeat node multiple times
|
|
17
|
+
# - `limit(&block)`: Filter options by condition
|
|
18
|
+
# - **Options**: Generate all valid combinations matching the grammar
|
|
19
|
+
#
|
|
20
|
+
# ## Grammar Definition Process
|
|
21
|
+
#
|
|
22
|
+
# 1. Define terminal nodes with N(content, **attributes)
|
|
23
|
+
# 2. Combine nodes using operators (|, +)
|
|
24
|
+
# 3. Add repetition and limits as needed
|
|
25
|
+
# 4. Use PN() for recursive grammars
|
|
26
|
+
# 5. Call .options to generate all valid combinations
|
|
27
|
+
#
|
|
28
|
+
# ## Musical Applications
|
|
29
|
+
#
|
|
30
|
+
# - Generate melodic patterns with rhythmic constraints
|
|
31
|
+
# - Create harmonic progressions with voice leading rules
|
|
32
|
+
# - Produce variations of musical motifs
|
|
33
|
+
# - Build algorithmic composition structures
|
|
34
|
+
#
|
|
35
|
+
# @example Simple sequence with alternatives
|
|
36
|
+
# include Musa::GenerativeGrammar
|
|
37
|
+
#
|
|
38
|
+
# a = N('a', size: 1)
|
|
39
|
+
# b = N('b', size: 1)
|
|
40
|
+
# c = N('c', size: 1)
|
|
41
|
+
#
|
|
42
|
+
# # Grammar: (a or b) repeated 3 times, then c
|
|
43
|
+
# grammar = (a | b).repeat(3) + c
|
|
44
|
+
#
|
|
45
|
+
# # Generate all possibilities
|
|
46
|
+
# grammar.options(content: :join)
|
|
47
|
+
# # => ["aaac", "aabc", "abac", "abbc", "baac", "babc", "bbac", "bbbc"]
|
|
48
|
+
#
|
|
49
|
+
# @example Grammar with constraints
|
|
50
|
+
# a = N('a', size: 1)
|
|
51
|
+
# b = N('b', size: 1)
|
|
52
|
+
#
|
|
53
|
+
# # Limit: total size must equal 3
|
|
54
|
+
# grammar = (a | b).repeat.limit { |o| o.collect { |_| _.attributes[:size] }.sum == 3 }
|
|
55
|
+
#
|
|
56
|
+
# # Filter options where size <= 4
|
|
57
|
+
# grammar.options(content: :join) { |o| o.collect { |e| e.attributes[:size] }.sum <= 4 }
|
|
58
|
+
# # => ["aaa", "aab", "aba", "abb", "baa", "bab", "bba", "bbb"]
|
|
59
|
+
#
|
|
60
|
+
# @example Recursive grammar using proxy nodes
|
|
61
|
+
# a = N('a', size: 1)
|
|
62
|
+
# b = N('b', size: 1)
|
|
63
|
+
# c = N('c', size: 1)
|
|
64
|
+
#
|
|
65
|
+
# # Create proxy for recursion
|
|
66
|
+
# dp = PN()
|
|
67
|
+
#
|
|
68
|
+
# # Grammar: (c + dp) or (a or b), limit size to 3
|
|
69
|
+
# d = (c + dp | (a | b)).repeat.limit(:size, :sum, :==, 3)
|
|
70
|
+
#
|
|
71
|
+
# # Assign recursive reference
|
|
72
|
+
# dp.node = d
|
|
73
|
+
#
|
|
74
|
+
# d.options(:size, :sum, :<=, 4, content: :join)
|
|
75
|
+
# # => ["cca", "ccb", "caa", "cab", "cba", "cbb", "aca", "acb", ...]
|
|
76
|
+
#
|
|
77
|
+
# @example Block nodes for dynamic content
|
|
78
|
+
# a = N(color: :blue) { |parent| 'hola' }
|
|
79
|
+
# b = N(color: :red) { |parent, attributes|
|
|
80
|
+
# OptionElement.new('adios', final: true, **attributes)
|
|
81
|
+
# }
|
|
82
|
+
#
|
|
83
|
+
# grammar = (a | b).repeat(2)
|
|
84
|
+
# grammar.options
|
|
85
|
+
# # => [["hola", "hola"], ["hola", "adios"], ["adios", "hola"], ["adios", "adios"]]
|
|
86
|
+
#
|
|
87
|
+
# @see N Method to create terminal/block nodes
|
|
88
|
+
# @see PN Method to create proxy nodes for recursion
|
|
89
|
+
# @see Musa::Series::Serie Series conversion for grammar options
|
|
90
|
+
# @see https://en.wikipedia.org/wiki/Formal_grammar Formal grammar (Wikipedia)
|
|
91
|
+
# @see https://en.wikipedia.org/wiki/Context-free_grammar Context-free grammar (Wikipedia)
|
|
92
|
+
# @see https://en.wikipedia.org/wiki/Generative_grammar Generative grammar (Wikipedia)
|
|
2
93
|
module GenerativeGrammar
|
|
3
94
|
# TODO: refactor & reorganize regarding use of include Musa::GenerativeGrammar problems as default consumption mode (it forces the consumer to have new public methods -P, PN- and class names -OptionElement-)
|
|
4
95
|
|
|
5
96
|
extend self
|
|
6
97
|
|
|
98
|
+
# Creates a terminal or block node.
|
|
99
|
+
#
|
|
100
|
+
# Nodes are the basic building blocks of grammars. They can be:
|
|
101
|
+
#
|
|
102
|
+
# - **Terminal nodes**: Fixed content with attributes
|
|
103
|
+
# - **Block nodes**: Dynamic content generated by block
|
|
104
|
+
#
|
|
105
|
+
# @param content [Object, nil] terminal content (if no block given)
|
|
106
|
+
# @param attributes [Hash] attributes attached to node
|
|
107
|
+
#
|
|
108
|
+
# @yieldparam parent [Array<OptionElement>] parent elements in generation
|
|
109
|
+
# @yieldparam attributes [Hash] node attributes
|
|
110
|
+
# @yieldreturn [Object, OptionElement] generated content or element
|
|
111
|
+
#
|
|
112
|
+
# @return [Implementation::FinalNode, Implementation::BlockNode] created node
|
|
113
|
+
#
|
|
114
|
+
# @example Terminal node
|
|
115
|
+
# n = N('a', size: 1)
|
|
116
|
+
#
|
|
117
|
+
# @example Block node
|
|
118
|
+
# n = N(color: :blue) { |parent| "hello" }
|
|
119
|
+
#
|
|
120
|
+
# @example Block returning OptionElement
|
|
121
|
+
# n = N(type: :special) { |parent, attrs|
|
|
122
|
+
# OptionElement.new('value', **attrs)
|
|
123
|
+
# }
|
|
7
124
|
def N(content = nil, **attributes, &block)
|
|
8
125
|
if block_given? && content.nil?
|
|
9
126
|
Implementation::BlockNode.new(attributes, &block)
|
|
@@ -12,13 +129,43 @@ module Musa
|
|
|
12
129
|
end
|
|
13
130
|
end
|
|
14
131
|
|
|
132
|
+
# Creates a proxy node for recursive grammars.
|
|
133
|
+
#
|
|
134
|
+
# Proxy nodes act as placeholders that can be assigned later,
|
|
135
|
+
# enabling recursive and self-referential grammar definitions.
|
|
136
|
+
#
|
|
137
|
+
# @return [Implementation::ProxyNode] proxy node
|
|
138
|
+
#
|
|
139
|
+
# @example Recursive grammar
|
|
140
|
+
# a = N('a')
|
|
141
|
+
# b = N('b')
|
|
142
|
+
#
|
|
143
|
+
# # Create proxy
|
|
144
|
+
# proxy = PN()
|
|
145
|
+
#
|
|
146
|
+
# # Define grammar referencing itself through proxy
|
|
147
|
+
# grammar = a + proxy | b
|
|
148
|
+
#
|
|
149
|
+
# # Assign recursive reference
|
|
150
|
+
# proxy.node = grammar
|
|
15
151
|
def PN
|
|
16
152
|
Implementation::ProxyNode.new
|
|
17
153
|
end
|
|
18
154
|
|
|
155
|
+
# Container for generated option elements.
|
|
156
|
+
#
|
|
157
|
+
# Wraps content and attributes for each element in a generated option.
|
|
158
|
+
# Used internally when generating grammar options.
|
|
159
|
+
#
|
|
160
|
+
# @attr_reader content [Object] element content
|
|
161
|
+
# @attr_reader attributes [Hash] element attributes
|
|
19
162
|
class OptionElement
|
|
20
163
|
attr_reader :content, :attributes
|
|
21
164
|
|
|
165
|
+
# Creates option element.
|
|
166
|
+
#
|
|
167
|
+
# @param content [Object] element content
|
|
168
|
+
# @param attributes [Hash, nil] element attributes
|
|
22
169
|
def initialize(content, attributes = nil)
|
|
23
170
|
@content = content
|
|
24
171
|
@attributes = attributes || {}
|
|
@@ -26,13 +173,54 @@ module Musa
|
|
|
26
173
|
end
|
|
27
174
|
|
|
28
175
|
module Implementation
|
|
176
|
+
# Base node class for grammar elements.
|
|
177
|
+
#
|
|
178
|
+
# Provides core operations for combining and transforming nodes.
|
|
179
|
+
# All node types (FinalNode, BlockNode, OrNode, etc.) inherit from this.
|
|
180
|
+
#
|
|
181
|
+
# @api private
|
|
29
182
|
class Node
|
|
183
|
+
# Creates alternation between this node and another.
|
|
184
|
+
#
|
|
185
|
+
# Generates options where either this node or the other is chosen.
|
|
186
|
+
#
|
|
187
|
+
# @param other [Node] alternative node
|
|
188
|
+
#
|
|
189
|
+
# @return [OrNode] alternation node
|
|
190
|
+
#
|
|
191
|
+
# @example
|
|
192
|
+
# a = N('a')
|
|
193
|
+
# b = N('b')
|
|
194
|
+
# ab = a.or(b) # or a | b
|
|
30
195
|
def or(other)
|
|
31
196
|
OrNode.new(self, other)
|
|
32
197
|
end
|
|
33
198
|
|
|
34
199
|
alias_method :|, :or
|
|
35
200
|
|
|
201
|
+
# Repeats this node with optional min/max bounds.
|
|
202
|
+
#
|
|
203
|
+
# Generates options with different repetition counts of this node.
|
|
204
|
+
# Without bounds, generates infinite options (use with limit).
|
|
205
|
+
#
|
|
206
|
+
# @param exactly [Integer, nil] exact repetition count
|
|
207
|
+
# @param min [Integer, nil] minimum repetitions
|
|
208
|
+
# @param max [Integer, nil] maximum repetitions
|
|
209
|
+
#
|
|
210
|
+
# @return [Node] node with repetition
|
|
211
|
+
#
|
|
212
|
+
# @raise [ArgumentError] if both exactly and min/max are specified
|
|
213
|
+
#
|
|
214
|
+
# @example Fixed repetition
|
|
215
|
+
# a = N('a')
|
|
216
|
+
# aaa = a.repeat(3) # exactly 3 times
|
|
217
|
+
# aaa = a.repeat(exactly: 3) # same
|
|
218
|
+
#
|
|
219
|
+
# @example Bounded repetition
|
|
220
|
+
# a_range = a.repeat(min: 2, max: 4) # 2, 3, or 4 times
|
|
221
|
+
#
|
|
222
|
+
# @example Unbounded (use with limit)
|
|
223
|
+
# a_any = a.repeat.limit { |o| o.size <= 5 }
|
|
36
224
|
def repeat(exactly = nil, min: nil, max: nil)
|
|
37
225
|
raise ArgumentError, 'Only exactly value or min/max values are allowed' if exactly && (min || max)
|
|
38
226
|
|
|
@@ -55,6 +243,30 @@ module Musa
|
|
|
55
243
|
end
|
|
56
244
|
end
|
|
57
245
|
|
|
246
|
+
# Limits generated options by condition.
|
|
247
|
+
#
|
|
248
|
+
# Filters options to only those satisfying the given condition.
|
|
249
|
+
# Can use simplified arguments or custom block.
|
|
250
|
+
#
|
|
251
|
+
# @param attribute [Symbol, nil] attribute to check (simplified form)
|
|
252
|
+
# @param after_collect_operation [Symbol, nil] operation on collected values
|
|
253
|
+
# @param comparison_method [Symbol, nil] comparison method to apply
|
|
254
|
+
# @param comparison_value [Object, nil] value to compare against
|
|
255
|
+
#
|
|
256
|
+
# @yieldparam option [Array<OptionElement>] option to evaluate
|
|
257
|
+
# @yieldreturn [Boolean] true if option should be included
|
|
258
|
+
#
|
|
259
|
+
# @return [ConditionNode] limited node
|
|
260
|
+
#
|
|
261
|
+
# @raise [ArgumentError] if both simplified arguments and block given
|
|
262
|
+
#
|
|
263
|
+
# @example Block form
|
|
264
|
+
# grammar = (a | b).repeat.limit { |o|
|
|
265
|
+
# o.collect { |e| e.attributes[:size] }.sum == 3
|
|
266
|
+
# }
|
|
267
|
+
#
|
|
268
|
+
# @example Simplified form
|
|
269
|
+
# grammar = (a | b).repeat.limit(:size, :sum, :==, 3)
|
|
58
270
|
def limit(attribute = nil, after_collect_operation = nil, comparison_method = nil, comparison_value = nil, &block)
|
|
59
271
|
raise ArgumentError, 'Cannot use simplified arguments and yield block at the same time' if (attribute || after_collect_operation || comparison_method || comparison_value) && @block
|
|
60
272
|
|
|
@@ -63,12 +275,61 @@ module Musa
|
|
|
63
275
|
ConditionNode.new(self, &block)
|
|
64
276
|
end
|
|
65
277
|
|
|
278
|
+
# Creates sequence of this node followed by another.
|
|
279
|
+
#
|
|
280
|
+
# Generates options with this node's content followed by the other's.
|
|
281
|
+
#
|
|
282
|
+
# @param other [Node] node to follow
|
|
283
|
+
#
|
|
284
|
+
# @return [NextNode] sequence node
|
|
285
|
+
#
|
|
286
|
+
# @example
|
|
287
|
+
# a = N('a')
|
|
288
|
+
# b = N('b')
|
|
289
|
+
# ab = a.next(b) # or a + b
|
|
66
290
|
def next(other)
|
|
67
291
|
NextNode.new(self, other)
|
|
68
292
|
end
|
|
69
293
|
|
|
70
294
|
alias_method :+, :next
|
|
71
295
|
|
|
296
|
+
# Generates all options from this grammar node.
|
|
297
|
+
#
|
|
298
|
+
# Produces all valid combinations matching the grammar definition.
|
|
299
|
+
# Can filter with condition and control output format.
|
|
300
|
+
#
|
|
301
|
+
# @param attribute [Symbol, nil] attribute for filtering (simplified)
|
|
302
|
+
# @param after_collect_operation [Symbol, nil] operation on collected values
|
|
303
|
+
# @param comparison_method [Symbol, nil] comparison method
|
|
304
|
+
# @param comparison_value [Object, nil] value to compare against
|
|
305
|
+
# @param raw [Boolean] if true, return raw OptionElement arrays
|
|
306
|
+
# @param content [Symbol] how to extract content (:itself, :join, etc.)
|
|
307
|
+
#
|
|
308
|
+
# @yieldparam option [Array<OptionElement>] option to evaluate
|
|
309
|
+
# @yieldreturn [Boolean] true if option should be included
|
|
310
|
+
#
|
|
311
|
+
# @return [Array] generated options
|
|
312
|
+
#
|
|
313
|
+
# @raise [ArgumentError] if simplified arguments and block both given
|
|
314
|
+
# @raise [ArgumentError] if raw and content both specified
|
|
315
|
+
#
|
|
316
|
+
# @example Basic usage
|
|
317
|
+
# grammar = (a | b).repeat(2)
|
|
318
|
+
# grammar.options
|
|
319
|
+
# # => [["a", "a"], ["a", "b"], ["b", "a"], ["b", "b"]]
|
|
320
|
+
#
|
|
321
|
+
# @example With content formatting
|
|
322
|
+
# grammar.options(content: :join)
|
|
323
|
+
# # => ["aa", "ab", "ba", "bb"]
|
|
324
|
+
#
|
|
325
|
+
# @example With filtering
|
|
326
|
+
# grammar.options { |o| o.size == 2 }
|
|
327
|
+
#
|
|
328
|
+
# @example Simplified filtering
|
|
329
|
+
# grammar.options(:size, :sum, :<=, 4, content: :join)
|
|
330
|
+
#
|
|
331
|
+
# @example Raw OptionElements
|
|
332
|
+
# grammar.options(raw: true)
|
|
72
333
|
def options(attribute = nil,
|
|
73
334
|
after_collect_operation = nil,
|
|
74
335
|
comparison_method = nil,
|
|
@@ -92,16 +353,34 @@ module Musa
|
|
|
92
353
|
end
|
|
93
354
|
end
|
|
94
355
|
|
|
356
|
+
# Counts number of options generated by this node.
|
|
357
|
+
#
|
|
358
|
+
# @return [Integer] option count
|
|
95
359
|
def size
|
|
96
360
|
options.size
|
|
97
361
|
end
|
|
98
362
|
|
|
99
363
|
alias_method :length, :size
|
|
100
364
|
|
|
365
|
+
# Gets option at index as series node.
|
|
366
|
+
#
|
|
367
|
+
# @param index [Integer] option index
|
|
368
|
+
#
|
|
369
|
+
# @return [Node] option converted to node
|
|
101
370
|
def [](index)
|
|
102
371
|
options[index].to_serie.to_node
|
|
103
372
|
end
|
|
104
373
|
|
|
374
|
+
# Converts options to series.
|
|
375
|
+
#
|
|
376
|
+
# Generates options and converts them to a {Musa::Series::Serie}.
|
|
377
|
+
#
|
|
378
|
+
# @param flatten [Boolean] flatten nested series
|
|
379
|
+
#
|
|
380
|
+
# @yieldparam option [Array<OptionElement>] option to evaluate
|
|
381
|
+
# @yieldreturn [Boolean] true if option should be included
|
|
382
|
+
#
|
|
383
|
+
# @return [Musa::Series::Serie] series of options
|
|
105
384
|
def to_serie(flatten: true, &condition)
|
|
106
385
|
serie = _options(&condition).collect { |o| o.collect(&:content) }.to_serie(of_series: true).merge
|
|
107
386
|
serie = serie.flatten if flatten
|
|
@@ -111,6 +390,16 @@ module Musa
|
|
|
111
390
|
|
|
112
391
|
alias_method :s, :to_serie
|
|
113
392
|
|
|
393
|
+
# Internal method to generate option arrays.
|
|
394
|
+
#
|
|
395
|
+
# @param parent [Array<OptionElement>, nil] parent elements
|
|
396
|
+
#
|
|
397
|
+
# @yieldparam option [Array<OptionElement>] option to evaluate
|
|
398
|
+
# @yieldreturn [Boolean] true if option should be included
|
|
399
|
+
#
|
|
400
|
+
# @return [Array<Array<OptionElement>>] option arrays
|
|
401
|
+
#
|
|
402
|
+
# @api private
|
|
114
403
|
def _options(parent: nil, &condition)
|
|
115
404
|
raise NotImplementedError
|
|
116
405
|
end
|
|
@@ -132,23 +421,43 @@ module Musa
|
|
|
132
421
|
end
|
|
133
422
|
end
|
|
134
423
|
|
|
424
|
+
# Proxy node for recursive grammar references.
|
|
425
|
+
#
|
|
426
|
+
# Acts as placeholder that delegates to assigned node.
|
|
427
|
+
# Enables recursive and self-referential grammars.
|
|
428
|
+
#
|
|
429
|
+
# @api private
|
|
135
430
|
class ProxyNode < Node
|
|
431
|
+
# @return [Node, nil] assigned node
|
|
136
432
|
attr_accessor :node
|
|
137
433
|
|
|
434
|
+
# @api private
|
|
138
435
|
def _options(parent: nil, &condition)
|
|
139
436
|
@node._options parent: parent, &condition
|
|
140
437
|
end
|
|
141
438
|
end
|
|
142
439
|
|
|
440
|
+
# Terminal node with fixed content.
|
|
441
|
+
#
|
|
442
|
+
# Represents a leaf node in the grammar tree with constant content.
|
|
443
|
+
#
|
|
444
|
+
# @attr_reader content [Object] node content
|
|
445
|
+
# @attr_reader attributes [Hash] node attributes
|
|
446
|
+
#
|
|
447
|
+
# @api private
|
|
143
448
|
class FinalNode < Node
|
|
144
449
|
attr_reader :content
|
|
145
450
|
attr_reader :attributes
|
|
146
451
|
|
|
452
|
+
# @param content [Object] node content
|
|
453
|
+
# @param attributes [Hash] node attributes
|
|
454
|
+
# @api private
|
|
147
455
|
def initialize(content, attributes)
|
|
148
456
|
super()
|
|
149
457
|
@element = OptionElement.new(content, attributes)
|
|
150
458
|
end
|
|
151
459
|
|
|
460
|
+
# @api private
|
|
152
461
|
def _options(parent: nil, &condition)
|
|
153
462
|
parent ||= []
|
|
154
463
|
|
|
@@ -164,12 +473,22 @@ module Musa
|
|
|
164
473
|
end
|
|
165
474
|
end
|
|
166
475
|
|
|
476
|
+
# Node with dynamic content generated by block.
|
|
477
|
+
#
|
|
478
|
+
# Executes block at generation time to produce content.
|
|
479
|
+
# Block receives parent elements and attributes.
|
|
480
|
+
#
|
|
481
|
+
# @api private
|
|
167
482
|
class BlockNode < Node
|
|
483
|
+
# @param attributes [Hash] node attributes
|
|
484
|
+
# @yield [parent, attributes] block to generate content
|
|
485
|
+
# @api private
|
|
168
486
|
def initialize(attributes, &block)
|
|
169
487
|
@attributes = attributes
|
|
170
488
|
@block = block
|
|
171
489
|
end
|
|
172
490
|
|
|
491
|
+
# @api private
|
|
173
492
|
def _options(parent: nil, &condition)
|
|
174
493
|
parent ||= []
|
|
175
494
|
|
|
@@ -188,12 +507,22 @@ module Musa
|
|
|
188
507
|
end
|
|
189
508
|
end
|
|
190
509
|
|
|
510
|
+
# Node that filters child node options by condition.
|
|
511
|
+
#
|
|
512
|
+
# Applies condition block to filter generated options.
|
|
513
|
+
# Created by {Node#limit}.
|
|
514
|
+
#
|
|
515
|
+
# @api private
|
|
191
516
|
class ConditionNode < Node
|
|
517
|
+
# @param node [Node] node to filter
|
|
518
|
+
# @yield [option] condition block
|
|
519
|
+
# @api private
|
|
192
520
|
def initialize(node, &block)
|
|
193
521
|
@node = node
|
|
194
522
|
@block = block
|
|
195
523
|
end
|
|
196
524
|
|
|
525
|
+
# @api private
|
|
197
526
|
def _options(parent: nil, &condition)
|
|
198
527
|
parent ||= []
|
|
199
528
|
|
|
@@ -207,13 +536,23 @@ module Musa
|
|
|
207
536
|
end
|
|
208
537
|
end
|
|
209
538
|
|
|
539
|
+
# Node representing alternation between two nodes.
|
|
540
|
+
#
|
|
541
|
+
# Generates options from either node1 or node2.
|
|
542
|
+
# Created by {Node#or} or `|` operator.
|
|
543
|
+
#
|
|
544
|
+
# @api private
|
|
210
545
|
class OrNode < Node
|
|
546
|
+
# @param node1 [Node] first alternative
|
|
547
|
+
# @param node2 [Node] second alternative
|
|
548
|
+
# @api private
|
|
211
549
|
def initialize(node1, node2)
|
|
212
550
|
@node1 = node1
|
|
213
551
|
@node2 = node2
|
|
214
552
|
super()
|
|
215
553
|
end
|
|
216
554
|
|
|
555
|
+
# @api private
|
|
217
556
|
def _options(parent: nil, &condition)
|
|
218
557
|
parent ||= []
|
|
219
558
|
|
|
@@ -231,13 +570,23 @@ module Musa
|
|
|
231
570
|
end
|
|
232
571
|
end
|
|
233
572
|
|
|
573
|
+
# Node representing sequence of two nodes.
|
|
574
|
+
#
|
|
575
|
+
# Generates options with node followed by after.
|
|
576
|
+
# Created by {Node#next} or `+` operator.
|
|
577
|
+
#
|
|
578
|
+
# @api private
|
|
234
579
|
class NextNode < Node
|
|
580
|
+
# @param node [Node] first node in sequence
|
|
581
|
+
# @param after [Node] node to follow
|
|
582
|
+
# @api private
|
|
235
583
|
def initialize(node, after)
|
|
236
584
|
@node = node
|
|
237
585
|
@after = after
|
|
238
586
|
super()
|
|
239
587
|
end
|
|
240
588
|
|
|
589
|
+
# @api private
|
|
241
590
|
def _options(parent: nil, &condition)
|
|
242
591
|
parent ||= []
|
|
243
592
|
|
|
@@ -251,7 +600,16 @@ module Musa
|
|
|
251
600
|
end
|
|
252
601
|
end
|
|
253
602
|
|
|
603
|
+
# Node representing repetition of a node.
|
|
604
|
+
#
|
|
605
|
+
# Generates options with different repetition counts.
|
|
606
|
+
# Created by {Node#repeat}.
|
|
607
|
+
#
|
|
608
|
+
# @api private
|
|
254
609
|
class RepeatNode < Node
|
|
610
|
+
# @param node [Node] node to repeat
|
|
611
|
+
# @param max [Integer, nil] maximum repetitions (nil = infinite)
|
|
612
|
+
# @api private
|
|
255
613
|
def initialize(node, max = nil)
|
|
256
614
|
@node = node
|
|
257
615
|
@max = max
|
|
@@ -259,6 +617,7 @@ module Musa
|
|
|
259
617
|
super()
|
|
260
618
|
end
|
|
261
619
|
|
|
620
|
+
# @api private
|
|
262
621
|
def _options(parent: nil, depth: nil, &condition)
|
|
263
622
|
parent ||= []
|
|
264
623
|
depth ||= 0
|
|
@@ -1,13 +1,118 @@
|
|
|
1
1
|
require_relative '../series'
|
|
2
2
|
|
|
3
3
|
module Musa
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Markov chain generator for stochastic sequence generation.
|
|
7
|
+
#
|
|
8
|
+
# Implements Markov chains that generate sequences of states based on
|
|
9
|
+
# probabilistic transition rules. Each state transitions to the next
|
|
10
|
+
# based on defined probabilities, creating pseudo-random but structured
|
|
11
|
+
# sequences.
|
|
12
|
+
#
|
|
13
|
+
# ## Theory
|
|
14
|
+
#
|
|
15
|
+
# A Markov chain is a stochastic model describing a sequence of possible
|
|
16
|
+
# events where the probability of each event depends only on the state
|
|
17
|
+
# attained in the previous event (memoryless property).
|
|
18
|
+
#
|
|
19
|
+
# ## Transition Types
|
|
20
|
+
#
|
|
21
|
+
# - **Array**: Equal probability between all options
|
|
22
|
+
# - `{ a: [:b, :c] }` → 50% chance of :b, 50% chance of :c
|
|
23
|
+
#
|
|
24
|
+
# - **Hash**: Weighted probabilities
|
|
25
|
+
# - `{ a: { b: 0.2, c: 0.8 } }` → 20% :b, 80% :c
|
|
26
|
+
# - Probabilities are normalized (don't need to sum to 1.0)
|
|
27
|
+
#
|
|
28
|
+
# - **Proc**: Algorithmic transitions based on history
|
|
29
|
+
# - `{ a: proc { |history| history.size.even? ? :b : :c } }`
|
|
30
|
+
# - Proc receives full history and returns next state
|
|
31
|
+
#
|
|
32
|
+
# ## Musical Applications
|
|
33
|
+
#
|
|
34
|
+
# - Generate melodic sequences with style-based transitions
|
|
35
|
+
# - Create rhythmic patterns with probabilistic variation
|
|
36
|
+
# - Produce chord progressions with weighted likelihood
|
|
37
|
+
# - Build dynamic musical structures with emergent behavior
|
|
38
|
+
#
|
|
39
|
+
# @example Equal probability transitions
|
|
40
|
+
# markov = Musa::Markov::Markov.new(
|
|
41
|
+
# start: :a,
|
|
42
|
+
# finish: :x,
|
|
43
|
+
# transitions: {
|
|
44
|
+
# a: [:b, :c], # 50/50 chance
|
|
45
|
+
# b: [:a, :c],
|
|
46
|
+
# c: [:a, :b, :x]
|
|
47
|
+
# }
|
|
48
|
+
# ).i
|
|
49
|
+
#
|
|
50
|
+
# markov.to_a # => [:a, :c, :b, :a, :b, :c, :x]
|
|
51
|
+
#
|
|
52
|
+
# @example Weighted probability transitions
|
|
53
|
+
# markov = Musa::Markov::Markov.new(
|
|
54
|
+
# start: :a,
|
|
55
|
+
# finish: :x,
|
|
56
|
+
# transitions: {
|
|
57
|
+
# a: { b: 0.2, c: 0.8 }, # 20% b, 80% c
|
|
58
|
+
# b: { a: 0.3, c: 0.7 }, # 30% a, 70% c
|
|
59
|
+
# c: [:a, :b, :x] # Equal probability
|
|
60
|
+
# }
|
|
61
|
+
# ).i
|
|
62
|
+
#
|
|
63
|
+
# @example Algorithmic transitions with history
|
|
64
|
+
# markov = Musa::Markov::Markov.new(
|
|
65
|
+
# start: :a,
|
|
66
|
+
# finish: :x,
|
|
67
|
+
# transitions: {
|
|
68
|
+
# a: { b: 0.2, c: 0.8 },
|
|
69
|
+
# # Transition based on history length
|
|
70
|
+
# b: proc { |history| history.size.even? ? :a : :c },
|
|
71
|
+
# c: [:a, :b, :x]
|
|
72
|
+
# }
|
|
73
|
+
# ).i
|
|
74
|
+
#
|
|
75
|
+
# @example Musical pitch transitions
|
|
76
|
+
# # Create melodic sequence with style-based transitions
|
|
77
|
+
# melody = Musa::Markov::Markov.new(
|
|
78
|
+
# start: 60, # Middle C
|
|
79
|
+
# finish: nil, # Infinite
|
|
80
|
+
# transitions: {
|
|
81
|
+
# 60 => { 62 => 0.4, 64 => 0.3, 59 => 0.3 }, # C → D/E/B
|
|
82
|
+
# 62 => { 60 => 0.3, 64 => 0.4, 67 => 0.3 }, # D → C/E/G
|
|
83
|
+
# 64 => [60, 62, 65, 67], # E → C/D/F/G
|
|
84
|
+
# # ... more transitions
|
|
85
|
+
# }
|
|
86
|
+
# ).i.max_size(16).to_a
|
|
87
|
+
#
|
|
88
|
+
# @see Musa::Series::Serie Series interface for chaining operations
|
|
89
|
+
# @see Musa::Extension::SmartProcBinder Smart procedure binding for history-based transitions
|
|
90
|
+
# @see https://en.wikipedia.org/wiki/Markov_chain Markov chain (Wikipedia)
|
|
91
|
+
# @see https://en.wikipedia.org/wiki/Stochastic_process Stochastic process (Wikipedia)
|
|
92
|
+
# @see https://en.wikipedia.org/wiki/Markov_chain#Music Markov chains in music (Wikipedia)
|
|
7
93
|
module Markov
|
|
94
|
+
# Markov chain serie generator.
|
|
95
|
+
#
|
|
96
|
+
# Generates sequences of states following probabilistic transition rules.
|
|
97
|
+
# Implements {Musa::Series::Serie} interface for integration with series operations.
|
|
8
98
|
class Markov
|
|
99
|
+
# TODO: adapt to series prototyping
|
|
9
100
|
include Musa::Series::Serie.base
|
|
10
101
|
|
|
102
|
+
# Creates Markov chain generator.
|
|
103
|
+
#
|
|
104
|
+
# @param transitions [Hash] state transition rules
|
|
105
|
+
# Keys are states, values are next state definitions (Array, Hash, or Proc)
|
|
106
|
+
# @param start [Object] initial state
|
|
107
|
+
# @param finish [Object, nil] terminal state (nil for infinite)
|
|
108
|
+
# @param random [Random, Integer, nil] random number generator or seed
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# markov = Markov.new(
|
|
112
|
+
# transitions: { a: [:b, :c], b: [:a, :c], c: [:a, :b, :x] },
|
|
113
|
+
# start: :a,
|
|
114
|
+
# finish: :x
|
|
115
|
+
# )
|
|
11
116
|
def initialize(transitions:, start:, finish: nil, random: nil)
|
|
12
117
|
@transitions = transitions.clone.freeze
|
|
13
118
|
|
|
@@ -23,17 +128,39 @@ module Musa
|
|
|
23
128
|
init
|
|
24
129
|
end
|
|
25
130
|
|
|
131
|
+
# @return [Object] starting state
|
|
26
132
|
attr_accessor :start
|
|
133
|
+
|
|
134
|
+
# @return [Object, nil] finishing state (nil for infinite)
|
|
27
135
|
attr_accessor :finish
|
|
136
|
+
|
|
137
|
+
# @return [Random] random number generator
|
|
28
138
|
attr_accessor :random
|
|
139
|
+
|
|
140
|
+
# @return [Hash] transition rules (frozen)
|
|
29
141
|
attr_accessor :transitions
|
|
30
142
|
|
|
143
|
+
# Initializes serie instance state.
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
31
146
|
private def _init
|
|
32
147
|
@current = nil
|
|
33
148
|
@finished = false
|
|
34
149
|
@history = []
|
|
35
150
|
end
|
|
36
151
|
|
|
152
|
+
# Generates next value in Markov chain.
|
|
153
|
+
#
|
|
154
|
+
# Selects next state based on current state's transition rules.
|
|
155
|
+
# Handles Array (equal probability), Hash (weighted), and Proc (algorithmic)
|
|
156
|
+
# transitions.
|
|
157
|
+
#
|
|
158
|
+
# @return [Object, nil] next state, or nil if finished
|
|
159
|
+
#
|
|
160
|
+
# @raise [RuntimeError] if no transition defined for current state
|
|
161
|
+
# @raise [ArgumentError] if transition type is not Array, Hash, or Proc
|
|
162
|
+
#
|
|
163
|
+
# @api private
|
|
37
164
|
private def _next_value
|
|
38
165
|
if @finished
|
|
39
166
|
@current = nil
|
|
@@ -75,6 +202,9 @@ module Musa
|
|
|
75
202
|
@current
|
|
76
203
|
end
|
|
77
204
|
|
|
205
|
+
# Checks if Markov chain is infinite.
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean] true if no finish state defined
|
|
78
208
|
def infinite?
|
|
79
209
|
@finish.nil?
|
|
80
210
|
end
|