musa-dsl 0.30.2 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +0 -1
  6. data/README.md +227 -6
  7. data/docs/README.md +83 -0
  8. data/docs/api-reference.md +86 -0
  9. data/docs/getting-started/quick-start.md +93 -0
  10. data/docs/getting-started/tutorial.md +58 -0
  11. data/docs/subsystems/core-extensions.md +316 -0
  12. data/docs/subsystems/datasets.md +465 -0
  13. data/docs/subsystems/generative.md +290 -0
  14. data/docs/subsystems/matrix.md +63 -0
  15. data/docs/subsystems/midi.md +123 -0
  16. data/docs/subsystems/music.md +544 -0
  17. data/docs/subsystems/musicxml-builder.md +264 -0
  18. data/docs/subsystems/neumas.md +71 -0
  19. data/docs/subsystems/repl.md +135 -0
  20. data/docs/subsystems/sequencer.md +98 -0
  21. data/docs/subsystems/series.md +302 -0
  22. data/docs/subsystems/transcription.md +152 -0
  23. data/docs/subsystems/transport.md +177 -0
  24. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  25. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  26. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  27. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  28. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  29. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  30. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  31. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  32. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  33. data/lib/musa-dsl/core-ext/with.rb +114 -0
  34. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  35. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  36. data/lib/musa-dsl/datasets/e.rb +186 -2
  37. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  38. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  39. data/lib/musa-dsl/datasets/helper.rb +75 -0
  40. data/lib/musa-dsl/datasets/p.rb +177 -2
  41. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  42. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  43. data/lib/musa-dsl/datasets/ps.rb +134 -4
  44. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  45. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  48. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  49. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  50. data/lib/musa-dsl/datasets/score.rb +279 -0
  51. data/lib/musa-dsl/datasets/v.rb +88 -0
  52. data/lib/musa-dsl/generative/darwin.rb +215 -1
  53. data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
  54. data/lib/musa-dsl/generative/markov.rb +135 -3
  55. data/lib/musa-dsl/generative/rules.rb +312 -4
  56. data/lib/musa-dsl/generative/variatio.rb +286 -2
  57. data/lib/musa-dsl/logger/logger.rb +267 -2
  58. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  59. data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
  60. data/lib/musa-dsl/midi/midi-voices.rb +275 -4
  61. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  62. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  63. data/lib/musa-dsl/music/chords.rb +353 -2
  64. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
  65. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  66. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  67. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  68. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  69. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  70. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  71. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  72. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  73. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  74. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  75. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  76. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  77. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  78. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  79. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  80. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  81. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  82. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  83. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  84. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  85. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  86. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  87. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  88. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  89. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  90. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  91. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  92. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  93. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  94. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  95. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  96. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  97. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  98. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  99. data/lib/musa-dsl/music/scales.rb +1384 -40
  100. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  101. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  102. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  103. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  104. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  105. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  106. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  107. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  108. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  109. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  110. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  111. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  112. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  113. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  114. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  115. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  116. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  117. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  118. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  119. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  120. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  121. data/lib/musa-dsl/repl/repl.rb +550 -0
  122. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  123. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  124. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  125. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  126. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  127. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  128. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  129. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  130. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  131. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  132. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  133. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  134. data/lib/musa-dsl/series/base-series.rb +843 -5
  135. data/lib/musa-dsl/series/buffer-serie.rb +54 -0
  136. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
  137. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  138. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  139. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  140. data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
  141. data/lib/musa-dsl/series/queue-serie.rb +78 -0
  142. data/lib/musa-dsl/series/series-composer.rb +701 -0
  143. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  144. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  145. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  146. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  147. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  148. data/lib/musa-dsl/transport/clock.rb +125 -0
  149. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  150. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  151. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  152. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  153. data/lib/musa-dsl/transport/timer.rb +83 -0
  154. data/lib/musa-dsl/transport/transport.rb +318 -0
  155. data/lib/musa-dsl/version.rb +2 -1
  156. data/lib/musa-dsl.rb +132 -25
  157. data/musa-dsl.gemspec +25 -18
  158. metadata +158 -16
@@ -1,13 +1,120 @@
1
1
  require_relative '../series'
2
2
 
3
3
  module Musa
4
-
5
- # TODO: adapt to series prototyping
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
+ # )
116
+ #
117
+ # @return [void]
11
118
  def initialize(transitions:, start:, finish: nil, random: nil)
12
119
  @transitions = transitions.clone.freeze
13
120
 
@@ -23,17 +130,39 @@ module Musa
23
130
  init
24
131
  end
25
132
 
133
+ # @return [Object] starting state
26
134
  attr_accessor :start
135
+
136
+ # @return [Object, nil] finishing state (nil for infinite)
27
137
  attr_accessor :finish
138
+
139
+ # @return [Random] random number generator
28
140
  attr_accessor :random
141
+
142
+ # @return [Hash] transition rules (frozen)
29
143
  attr_accessor :transitions
30
144
 
145
+ # Initializes serie instance state.
146
+ #
147
+ # @api private
31
148
  private def _init
32
149
  @current = nil
33
150
  @finished = false
34
151
  @history = []
35
152
  end
36
153
 
154
+ # Generates next value in Markov chain.
155
+ #
156
+ # Selects next state based on current state's transition rules.
157
+ # Handles Array (equal probability), Hash (weighted), and Proc (algorithmic)
158
+ # transitions.
159
+ #
160
+ # @return [Object, nil] next state, or nil if finished
161
+ #
162
+ # @raise [RuntimeError] if no transition defined for current state
163
+ # @raise [ArgumentError] if transition type is not Array, Hash, or Proc
164
+ #
165
+ # @api private
37
166
  private def _next_value
38
167
  if @finished
39
168
  @current = nil
@@ -75,6 +204,9 @@ module Musa
75
204
  @current
76
205
  end
77
206
 
207
+ # Checks if Markov chain is infinite.
208
+ #
209
+ # @return [Boolean] true if no finish state defined
78
210
  def infinite?
79
211
  @finish.nil?
80
212
  end
@@ -1,21 +1,174 @@
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
152
+ #
153
+ # @return [void]
15
154
  def initialize(&block)
16
155
  @dsl = RulesEvalContext.new(&block)
17
156
  end
18
157
 
158
+ # Generates possibility tree from object.
159
+ #
160
+ # Recursively applies grow rules to create branches, cut rules to
161
+ # prune invalid paths, and end conditions to mark complete branches.
162
+ #
163
+ # @param object [Object] object to expand
164
+ # @param confirmed_node [Node, nil] confirmed parent node (for history)
165
+ # @param node [Node, nil] current node being built
166
+ # @param grow_rules [Array<GrowRule>, nil] rules to apply
167
+ # @param parameters [Hash] additional parameters for rules
168
+ #
169
+ # @return [Node] root node of possibility tree
170
+ #
171
+ # @api private
19
172
  def generate_possibilities(object, confirmed_node = nil, node = nil, grow_rules = nil, **parameters)
20
173
  node ||= Node.new
21
174
  grow_rules ||= @dsl._grow_rules
@@ -53,6 +206,25 @@ module Musa
53
206
  node
54
207
  end
55
208
 
209
+ # Applies rules to seed objects sequentially.
210
+ #
211
+ # Processes list of seed objects in sequence, generating possibilities
212
+ # from each confirmed endpoint of previous seed. Returns tree of all
213
+ # valid combination paths.
214
+ #
215
+ # @param object_or_list [Object, Array] seed object(s) to process
216
+ # @param node [Node, nil] root node (creates if nil)
217
+ # @param parameters [Hash] additional parameters for rules
218
+ #
219
+ # @return [Node] root node with all combination paths
220
+ #
221
+ # @example Single seed
222
+ # tree = rules.apply(:I)
223
+ # tree.combinations # => [[:I, :ii, :V, :I], ...]
224
+ #
225
+ # @example Multiple seeds
226
+ # tree = rules.apply([:I, :ii, :V])
227
+ # tree.combinations # => combinations starting from each seed
56
228
  def apply(object_or_list, node = nil, **parameters)
57
229
  list = object_or_list.arrayfy.clone
58
230
 
@@ -76,36 +248,75 @@ module Musa
76
248
  node
77
249
  end
78
250
 
251
+ # DSL context for rule definitions.
252
+ #
253
+ # @api private
79
254
  class RulesEvalContext
80
255
  include Musa::Extension::With
81
256
 
257
+ # @return [Array<GrowRule>] grow rules
258
+ # @return [Proc, nil] end condition
259
+ # @return [Array<CutRule>] cut rules
82
260
  attr_reader :_grow_rules, :_ended_when, :_cut_rules
83
261
 
262
+ # @return [void]
263
+ # @api private
84
264
  def initialize(&block)
85
265
  @_grow_rules = []
86
266
  @_cut_rules = []
87
267
  with &block
88
268
  end
89
269
 
270
+ # Defines grow rule.
271
+ #
272
+ # @param name [String] rule name for debugging
273
+ # @yield [object, history, **params] rule block
274
+ # @return [self]
275
+ # @api private
90
276
  def grow(name, &block)
91
277
  @_grow_rules << GrowRule.new(name, &block)
92
278
  self
93
279
  end
94
280
 
281
+ # Defines end condition.
282
+ #
283
+ # @yield [object, history, **params] condition block
284
+ # @return [self]
285
+ # @api private
95
286
  def ended_when(&block)
96
287
  @_ended_when = block
97
288
  self
98
289
  end
99
290
 
291
+ # Defines cut/pruning rule.
292
+ #
293
+ # @param reason [String] rejection reason
294
+ # @yield [object, history, **params] pruning block
295
+ # @return [self]
296
+ # @api private
100
297
  def cut(reason, &block)
101
298
  @_cut_rules << CutRule.new(reason, &block)
102
299
  self
103
300
  end
104
301
 
302
+ # Checks if end condition is defined.
303
+ #
304
+ # @return [Boolean] true if ended_when was called
305
+ #
306
+ # @api private
105
307
  def _has_ending?
106
308
  !@_ended_when.nil?
107
309
  end
108
310
 
311
+ # Evaluates end condition for object.
312
+ #
313
+ # @param object [Object] object to check
314
+ # @param history [Array] object history
315
+ # @param parameters [Hash] additional parameters
316
+ #
317
+ # @return [Boolean] true if object should end
318
+ #
319
+ # @api private
109
320
  def _ended?(object, history, **parameters)
110
321
  if @_ended_when
111
322
  with object, history, **parameters, &@_ended_when
@@ -117,11 +328,21 @@ module Musa
117
328
  class GrowRule
118
329
  attr_reader :name
119
330
 
331
+ # @param name [String] rule name
332
+ #
333
+ # @return [void]
120
334
  def initialize(name, &block)
121
335
  @name = name
122
336
  @block = block
123
337
  end
124
338
 
339
+ # Generates possible branched objects.
340
+ #
341
+ # @param object [Object] object to branch
342
+ # @param history [Array] object history
343
+ # @param parameters [Hash] additional parameters
344
+ #
345
+ # @return [Array<Object>] branched objects
125
346
  def generate_possibilities(object, history, **parameters)
126
347
  # TODO: optimize context using only one instance for all genereate_possibilities calls
127
348
  context = GrowRuleEvalContext.new
@@ -135,10 +356,16 @@ module Musa
135
356
 
136
357
  attr_reader :_branches
137
358
 
359
+ # @return [void]
138
360
  def initialize
139
361
  @_branches = []
140
362
  end
141
363
 
364
+ # Records a branched object.
365
+ #
366
+ # @param object [Object] branched object
367
+ #
368
+ # @return [self]
142
369
  def branch(object)
143
370
  @_branches << object
144
371
  self
@@ -153,11 +380,21 @@ module Musa
153
380
  class CutRule
154
381
  attr_reader :reason
155
382
 
383
+ # @param reason [String] rejection reason
384
+ #
385
+ # @return [void]
156
386
  def initialize(reason, &block)
157
387
  @reason = reason
158
388
  @block = block
159
389
  end
160
390
 
391
+ # Checks if object should be rejected.
392
+ #
393
+ # @param object [Object] object to check
394
+ # @param history [Array] object history
395
+ # @param parameters [Hash] additional parameters
396
+ #
397
+ # @return [Array<String>, nil] rejection reasons or nil if not rejected
161
398
  def rejects?(object, history, **parameters)
162
399
  # TODO: optimize context using only one instance for all rejects? checks
163
400
  context = CutRuleEvalContext.new
@@ -173,10 +410,16 @@ module Musa
173
410
 
174
411
  attr_reader :_secondary_reasons
175
412
 
413
+ # @return [void]
176
414
  def initialize
177
415
  @_secondary_reasons = []
178
416
  end
179
417
 
418
+ # Marks object for pruning.
419
+ #
420
+ # @param secondary_reason [String, nil] additional reason detail
421
+ #
422
+ # @return [self]
180
423
  def prune(secondary_reason = nil)
181
424
  @_secondary_reasons << secondary_reason
182
425
  self
@@ -191,9 +434,26 @@ module Musa
191
434
 
192
435
  private_constant :RulesEvalContext
193
436
 
437
+ # Tree node representing possibility in generation.
438
+ #
439
+ # Nodes form tree structure of generation possibilities.
440
+ # Each node has object, parent, children, rejection status, and end flag.
441
+ #
442
+ # @attr_reader parent [Node, nil] parent node
443
+ # @attr_reader children [Array<Node>] child nodes
444
+ # @attr_reader object [Object, nil] node object
445
+ # @attr_reader rejected [String, Array<String>, nil] rejection reason(s)
446
+ #
447
+ # @api private
194
448
  class Node
195
449
  attr_reader :parent, :children, :object, :rejected
196
450
 
451
+ # @param object [Object, nil] node object
452
+ # @param parent [Node, nil] parent node
453
+ #
454
+ # @return [void]
455
+ #
456
+ # @api private
197
457
  def initialize(object = nil, parent = nil)
198
458
  @parent = parent
199
459
  @children = []
@@ -203,15 +463,31 @@ module Musa
203
463
  @rejected = nil
204
464
  end
205
465
 
466
+ # Adds child node.
467
+ #
468
+ # @param object [Object] child object
469
+ # @return [Node] created child node
470
+ # @api private
206
471
  def add(object)
207
472
  Node.new(object, self).tap { |n| @children << n }
208
473
  end
209
474
 
475
+ # Marks node as rejected.
476
+ #
477
+ # @param rejection [String, Array<String>] rejection reason(s)
478
+ # @return [self]
479
+ # @api private
210
480
  def reject!(rejection)
211
481
  @rejected = rejection
212
482
  self
213
483
  end
214
484
 
485
+ # Marks node as ended/complete.
486
+ #
487
+ # Propagates rejection if all children rejected.
488
+ #
489
+ # @return [self]
490
+ # @api private
215
491
  def mark_as_ended!
216
492
  @children.each(&:update_rejection_by_children!)
217
493
 
@@ -224,10 +500,18 @@ module Musa
224
500
  self
225
501
  end
226
502
 
503
+ # Checks if node is ended.
504
+ #
505
+ # @return [Boolean]
506
+ # @api private
227
507
  def ended?
228
508
  @ended
229
509
  end
230
510
 
511
+ # Returns path from root to this node.
512
+ #
513
+ # @return [Array<Object>] history of objects
514
+ # @api private
231
515
  def history
232
516
  objects = []
233
517
  n = self
@@ -239,6 +523,13 @@ module Musa
239
523
  objects.reverse
240
524
  end
241
525
 
526
+ # Collects objects from ended leaf nodes.
527
+ #
528
+ # Recursively gathers objects from all valid ended branches,
529
+ # excluding rejected paths.
530
+ #
531
+ # @return [Array<Object>] objects from valid endpoints
532
+ # @api private
242
533
  def fish
243
534
  fished = []
244
535
 
@@ -255,6 +546,23 @@ module Musa
255
546
  fished
256
547
  end
257
548
 
549
+ # Returns all valid combination paths.
550
+ #
551
+ # Recursively builds complete paths from root to all valid
552
+ # leaf nodes, excluding rejected branches.
553
+ #
554
+ # @param parent_combination [Array, nil] parent path
555
+ #
556
+ # @return [Array<Array<Object>>] all valid complete paths
557
+ #
558
+ # @example
559
+ # tree.combinations
560
+ # # => [
561
+ # # [:I, :ii, :V, :I],
562
+ # # [:I, :IV, :V, :I],
563
+ # # ...
564
+ # # ]
565
+ # @api private
258
566
  def combinations(parent_combination = nil)
259
567
  parent_combination ||= []
260
568