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.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- 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 +544 -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 +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -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 +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -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 +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -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 +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -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 +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -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 +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- metadata +158 -16
|
@@ -1,13 +1,120 @@
|
|
|
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
|
+
# )
|
|
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
|
|