musa-dsl 0.14.32 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/README.md +5 -1
  4. data/lib/musa-dsl.rb +54 -11
  5. data/lib/musa-dsl/core-ext.rb +7 -13
  6. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +15 -23
  7. data/lib/musa-dsl/core-ext/arrayfy.rb +30 -12
  8. data/lib/musa-dsl/core-ext/attribute-builder.rb +194 -0
  9. data/lib/musa-dsl/core-ext/deep-copy.rb +185 -0
  10. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +44 -40
  11. data/lib/musa-dsl/core-ext/inspect-nice.rb +40 -22
  12. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +108 -0
  13. data/lib/musa-dsl/core-ext/with.rb +26 -0
  14. data/lib/musa-dsl/datasets.rb +8 -3
  15. data/lib/musa-dsl/datasets/dataset.rb +3 -0
  16. data/lib/musa-dsl/datasets/delta-d.rb +12 -0
  17. data/lib/musa-dsl/datasets/e.rb +61 -0
  18. data/lib/musa-dsl/datasets/gdv.rb +51 -411
  19. data/lib/musa-dsl/datasets/gdvd.rb +179 -0
  20. data/lib/musa-dsl/datasets/helper.rb +41 -0
  21. data/lib/musa-dsl/datasets/p.rb +68 -0
  22. data/lib/musa-dsl/datasets/packed-v.rb +19 -0
  23. data/lib/musa-dsl/datasets/pdv.rb +22 -15
  24. data/lib/musa-dsl/datasets/ps.rb +113 -0
  25. data/lib/musa-dsl/datasets/score.rb +210 -0
  26. data/lib/musa-dsl/datasets/score/queriable.rb +48 -0
  27. data/lib/musa-dsl/datasets/score/render.rb +31 -0
  28. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +160 -0
  29. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +51 -0
  30. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +153 -0
  31. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +158 -0
  32. data/lib/musa-dsl/datasets/v.rb +23 -0
  33. data/lib/musa-dsl/generative.rb +5 -5
  34. data/lib/musa-dsl/generative/backboner.rb +274 -0
  35. data/lib/musa-dsl/generative/darwin.rb +102 -96
  36. data/lib/musa-dsl/generative/generative-grammar.rb +182 -187
  37. data/lib/musa-dsl/generative/markov.rb +56 -53
  38. data/lib/musa-dsl/generative/variatio.rb +234 -222
  39. data/lib/musa-dsl/logger.rb +1 -0
  40. data/lib/musa-dsl/logger/logger.rb +31 -0
  41. data/lib/musa-dsl/matrix.rb +1 -0
  42. data/lib/musa-dsl/matrix/matrix.rb +210 -0
  43. data/lib/musa-dsl/midi.rb +2 -2
  44. data/lib/musa-dsl/midi/midi-recorder.rb +54 -52
  45. data/lib/musa-dsl/midi/midi-voices.rb +183 -182
  46. data/lib/musa-dsl/music.rb +5 -5
  47. data/lib/musa-dsl/music/chord-definition.rb +54 -50
  48. data/lib/musa-dsl/music/chord-definitions.rb +13 -9
  49. data/lib/musa-dsl/music/chords.rb +236 -238
  50. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +187 -183
  51. data/lib/musa-dsl/music/scales.rb +331 -332
  52. data/lib/musa-dsl/musicxml.rb +1 -0
  53. data/lib/musa-dsl/musicxml/builder/attributes.rb +155 -0
  54. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +45 -0
  55. data/lib/musa-dsl/musicxml/builder/direction.rb +322 -0
  56. data/lib/musa-dsl/musicxml/builder/helper.rb +90 -0
  57. data/lib/musa-dsl/musicxml/builder/measure.rb +137 -0
  58. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +152 -0
  59. data/lib/musa-dsl/musicxml/builder/note.rb +577 -0
  60. data/lib/musa-dsl/musicxml/builder/part-group.rb +44 -0
  61. data/lib/musa-dsl/musicxml/builder/part.rb +67 -0
  62. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +126 -0
  63. data/lib/musa-dsl/musicxml/builder/rest.rb +117 -0
  64. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +120 -0
  65. data/lib/musa-dsl/musicxml/builder/typed-text.rb +43 -0
  66. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +112 -0
  67. data/lib/musa-dsl/neumalang.rb +1 -1
  68. data/lib/musa-dsl/neumalang/datatypes.citrus +79 -0
  69. data/lib/musa-dsl/neumalang/neuma.citrus +165 -0
  70. data/lib/musa-dsl/neumalang/neumalang.citrus +32 -242
  71. data/lib/musa-dsl/neumalang/neumalang.rb +373 -142
  72. data/lib/musa-dsl/neumalang/process.citrus +21 -0
  73. data/lib/musa-dsl/neumalang/terminals.citrus +67 -0
  74. data/lib/musa-dsl/neumalang/vectors.citrus +23 -0
  75. data/lib/musa-dsl/neumas.rb +5 -0
  76. data/lib/musa-dsl/neumas/array-to-neumas.rb +34 -0
  77. data/lib/musa-dsl/neumas/neuma-decoder.rb +63 -0
  78. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +57 -0
  79. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +15 -0
  80. data/lib/musa-dsl/neumas/neumas.rb +37 -0
  81. data/lib/musa-dsl/neumas/string-to-neumas.rb +33 -0
  82. data/lib/musa-dsl/repl.rb +1 -1
  83. data/lib/musa-dsl/repl/repl.rb +103 -110
  84. data/lib/musa-dsl/sequencer.rb +1 -1
  85. data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +163 -136
  86. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +301 -286
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +548 -321
  88. data/lib/musa-dsl/sequencer/base-sequencer-public.rb +198 -176
  89. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +77 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +75 -0
  91. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +105 -85
  92. data/lib/musa-dsl/sequencer/timeslots.rb +34 -0
  93. data/lib/musa-dsl/series.rb +1 -1
  94. data/lib/musa-dsl/{core-ext → series}/array-to-serie.rb +1 -1
  95. data/lib/musa-dsl/series/base-series.rb +171 -168
  96. data/lib/musa-dsl/series/hash-serie-splitter.rb +134 -132
  97. data/lib/musa-dsl/series/holder-serie.rb +1 -1
  98. data/lib/musa-dsl/series/main-serie-constructors.rb +6 -1
  99. data/lib/musa-dsl/series/main-serie-operations.rb +807 -797
  100. data/lib/musa-dsl/series/proxy-serie.rb +6 -6
  101. data/lib/musa-dsl/series/queue-serie.rb +5 -5
  102. data/lib/musa-dsl/series/series.rb +2 -0
  103. data/lib/musa-dsl/transcription.rb +4 -0
  104. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +227 -0
  105. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +36 -0
  106. data/lib/musa-dsl/transcription/from-gdv.rb +17 -0
  107. data/lib/musa-dsl/transcription/transcription.rb +55 -0
  108. data/lib/musa-dsl/transport.rb +6 -6
  109. data/lib/musa-dsl/transport/clock.rb +26 -26
  110. data/lib/musa-dsl/transport/dummy-clock.rb +32 -30
  111. data/lib/musa-dsl/transport/external-tick-clock.rb +21 -20
  112. data/lib/musa-dsl/transport/input-midi-clock.rb +82 -80
  113. data/lib/musa-dsl/transport/timer-clock.rb +72 -71
  114. data/lib/musa-dsl/transport/timer.rb +28 -26
  115. data/lib/musa-dsl/transport/transport.rb +100 -95
  116. data/musa-dsl.gemspec +3 -3
  117. metadata +73 -24
  118. data/lib/musa-dsl/core-ext/array-apply-get.rb +0 -18
  119. data/lib/musa-dsl/core-ext/array-to-neumas.rb +0 -28
  120. data/lib/musa-dsl/core-ext/as-context-run.rb +0 -44
  121. data/lib/musa-dsl/core-ext/duplicate.rb +0 -134
  122. data/lib/musa-dsl/core-ext/key-parameters-procedure-binder.rb +0 -85
  123. data/lib/musa-dsl/core-ext/proc-nice.rb +0 -13
  124. data/lib/musa-dsl/core-ext/send-nice.rb +0 -21
  125. data/lib/musa-dsl/core-ext/string-to-neumas.rb +0 -27
  126. data/lib/musa-dsl/datasets/gdv-decorators.rb +0 -221
  127. data/lib/musa-dsl/generative/rules.rb +0 -282
  128. data/lib/musa-dsl/neuma.rb +0 -1
  129. data/lib/musa-dsl/neuma/neuma.rb +0 -181
@@ -0,0 +1,51 @@
1
+ module Musa::Datasets::Score::ToMXML
2
+ private
3
+
4
+ DynamicsContext = Struct.new(:last_dynamics)
5
+ private_constant :DynamicsContext
6
+
7
+ def process_ps(measure, element, context, logger, do_log)
8
+ context ||= DynamicsContext.new
9
+
10
+ logger.debug "\nprocess_ps #{element}" if do_log
11
+
12
+ case element[:dataset][:type]
13
+ when :crescendo, :diminuendo
14
+ if element[:change] == :start
15
+ dynamics = dynamics_to_string(element[:dataset][:from])
16
+
17
+ if dynamics != context.last_dynamics
18
+ measure.add_dynamics dynamics, placement: 'below' if dynamics && element[:dataset][:from] > 0
19
+ context.last_dynamics = dynamics
20
+ end
21
+
22
+ measure.add_wedge element[:dataset][:type],
23
+ niente: element[:dataset][:type] == :crescendo && element[:dataset][:from] == 0,
24
+ placement: 'below'
25
+ else
26
+ measure.add_wedge 'stop',
27
+ niente: element[:dataset][:type] == :diminuendo && element[:dataset][:to] == 0,
28
+ placement: 'below'
29
+
30
+ dynamics = dynamics_to_string(element[:dataset][:to])
31
+
32
+ measure.add_dynamics dynamics, placement: 'below' if dynamics && element[:dataset][:to] > 0
33
+ context.last_dynamics = dynamics
34
+ end
35
+
36
+ when :dynamics
37
+ dynamics = dynamics_to_string(element[:dataset][:from])
38
+
39
+ if dynamics != context.last_dynamics
40
+ measure.add_dynamics dynamics, placement: 'below'
41
+ context.last_dynamics = dynamics
42
+ end
43
+
44
+ else
45
+ # ignored
46
+ end
47
+
48
+ context
49
+ end
50
+
51
+ end
@@ -0,0 +1,153 @@
1
+ require 'prime'
2
+
3
+ module Musa::Datasets::Score::ToMXML
4
+ private
5
+
6
+ class ElementDurationDecomposition
7
+ def initialize(element, bar, bar_size = 1r) # TODO remove (unused because of bad strategy to time groups)
8
+ @continue_from_previous_bar = element[:start] < bar
9
+ @continue_to_next_bar = element[:finish] >= bar + bar_size
10
+
11
+ @start = continue_from_previous_bar ? 0r : element[:start] - bar
12
+
13
+ @duration = continue_to_next_bar ?
14
+ (1r - @start) :
15
+ (element[:start] + element[:dataset][:duration] - (bar + start))
16
+
17
+ @duration_decomposition = integrate_as_dotteable_durations(decompose_as_sum_of_simple_durations(@duration))
18
+ end
19
+
20
+ attr_reader :continue_from_previous_bar, :continue_to_next_bar, :start, :duration, :duration_decomposition
21
+
22
+ def to_s
23
+ "ElementDurationDecomposition(#{@duration}) = [#{@duration_decomposition}]"
24
+ end
25
+
26
+ alias inspect to_s
27
+ end
28
+
29
+ private_constant :ElementDurationDecomposition
30
+
31
+ def time_and_tuplet_optimize(elements, bar, bar_size = 1r) # TODO remove (unused because of bad strategy to time groups)
32
+ decompositions = elements.collect { |pdv| ElementDurationDecomposition.new(pdv, bar, bar_size) }
33
+
34
+ denominators = decompositions.collect { |g| g.duration_decomposition.collect { |d| d.to_r.denominator } }.flatten.uniq
35
+
36
+ lcm_denominators = denominators.reduce(:lcm)
37
+
38
+ primes = Prime.prime_division(lcm_denominators)
39
+
40
+ factors = primes.collect { |base, exp| [base] * exp }.flatten
41
+
42
+ refactors = all_combinations(factors).collect { |a| a.reduce(&:*) }
43
+
44
+ # Y no se puede seguir con la descomposición
45
+
46
+ nil
47
+ end
48
+
49
+ def decompose_as_sum_of_simple_durations(duration)
50
+ return [] if duration.zero?
51
+
52
+ # TODO mejorar esta descomposición para que tenga menos factores redundantes
53
+ pd = Prime.prime_division(duration.to_r.denominator).collect { |base, exp| (1..exp).collect { |i| base ** i } }.flatten
54
+
55
+ divisors = ([[1]] + all_combinations(pd)).collect { |combination| combination.inject(:*) }
56
+
57
+ summands = []
58
+
59
+ while divisor = divisors.shift
60
+ c = Rational(1, divisor)
61
+ f = (duration / c).floor
62
+ n = f * c
63
+ summands << n unless n.zero?
64
+ duration -= n
65
+ end
66
+
67
+ raise ArgumentError, "#{duration} cannot be further decomposed" unless duration.zero?
68
+
69
+ summands
70
+ end
71
+
72
+ def all_combinations(numbers)
73
+ all_combinations = []
74
+ i = 1
75
+ until (combinations = numbers.combination(i).to_a).empty?
76
+ all_combinations += combinations
77
+ i += 1
78
+ end
79
+
80
+ all_combinations.uniq
81
+ end
82
+
83
+ def integrate_as_dotteable_durations(simple_durations)
84
+ integrated_durations = []
85
+ last = nil
86
+ simple_durations.each do |duration|
87
+ if last && duration == last / 2
88
+ integrated_durations[integrated_durations.size-1] += duration
89
+ else
90
+ integrated_durations << duration
91
+ end
92
+ last = duration
93
+ end
94
+ integrated_durations
95
+ end
96
+
97
+ def type_and_dots_and_tuplet_ratio(noteable_duration)
98
+ r = decompose_as_sum_of_simple_durations(noteable_duration)
99
+ n = r.shift
100
+
101
+ tuplet_ratio = Rational(n.denominator, nearest_lower_power_of_2(n.denominator))
102
+
103
+ type = type_of(nearest_upper_power_of_2(n))
104
+ dots = 0
105
+
106
+ while nn = r.shift
107
+ if nn == n / 2
108
+ dots += 1
109
+ n = nn
110
+ else
111
+ break
112
+ end
113
+ end
114
+
115
+ raise ArgumentError, "#{noteable_duration} cannot be decomposed as a duration with dots" unless r.empty?
116
+
117
+ return type, dots, tuplet_ratio
118
+ end
119
+
120
+ def nearest_upper_power_of_2(number)
121
+ return 0 if number.zero?
122
+
123
+ exp = Math.log2(number)
124
+ exp_floor = exp.floor
125
+ plus = exp > exp_floor ? 1 : 0
126
+
127
+ 2 ** (exp_floor + plus)
128
+ end
129
+
130
+ def nearest_lower_power_of_2(number)
131
+ return 0 if number.zero?
132
+
133
+ exp_floor = Math.log2(number).floor
134
+
135
+ 2 ** exp_floor
136
+ end
137
+
138
+ def type_of(base_type_duration)
139
+ duration_log2i = Math.log2(base_type_duration)
140
+
141
+ raise ArgumentError, "#{base_type_duration} is not a inverse power of 2 (i.e. 2, 1, 1/2, 1/4, 1/8, 1/64, etc)" \
142
+ unless base_type_duration == 2 ** duration_log2i
143
+
144
+ raise ArgumentError, "#{base_type_duration} is not between 1024th and maxima accepted durations" \
145
+ unless duration_log2i >= -10 && duration_log2i <= 3
146
+
147
+ ['1024th', '512th', '256th', '128th',
148
+ '64th', '32nd', '16th', 'eighth',
149
+ 'quarter', 'half', 'whole', 'breve',
150
+ 'long', 'maxima'][duration_log2i + 10]
151
+ end
152
+ end
153
+
@@ -0,0 +1,158 @@
1
+ require_relative '../../../logger'
2
+ require_relative '../../../musicxml'
3
+
4
+ require_relative 'process-time'
5
+ require_relative 'process-pdv'
6
+ require_relative 'process-ps'
7
+
8
+ module Musa::Datasets; class Score
9
+ module ToMXML
10
+ include Musa::MusicXML::Builder
11
+ include Musa::Datasets
12
+
13
+ def to_mxml(beats_per_bar, ticks_per_beat,
14
+ bpm: nil,
15
+ title: nil,
16
+ creators: nil,
17
+ encoding_date: nil,
18
+ parts:,
19
+ logger: nil,
20
+ do_log: nil)
21
+
22
+ bpm ||= 90
23
+ title ||= 'Untitled'
24
+ creators ||= { composer: 'Unknown' }
25
+ logger ||= Musa::Logger::Logger.new
26
+ do_log ||= nil
27
+
28
+ mxml = ScorePartwise.new do |_|
29
+ _.work_title title
30
+ _.creators **creators
31
+ _.encoding_date encoding_date if encoding_date
32
+
33
+ parts.each_pair do |id, part_info|
34
+ _.part id,
35
+ name: part_info&.[](:name),
36
+ abbreviation: part_info&.[](:abbreviation) do |_|
37
+
38
+ _.measure do |_|
39
+ _.attributes do |_|
40
+ _.divisions ticks_per_beat
41
+
42
+ i = 0
43
+ (part_info&.[](:clefs) || { g: 2 }).each_pair do |clef, line|
44
+ i += 1
45
+ _.clef i, sign: clef.upcase, line: line
46
+ _.time i, beats: beats_per_bar, beat_type: 4
47
+ end
48
+ end
49
+
50
+ _.metronome placement: 'above', beat_unit: 'quarter', per_minute: bpm
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ if do_log
57
+ logger.debug"\nscore.to_mxl log:"
58
+ logger.debug "-----------------"
59
+ end
60
+
61
+ parts.each_key do |part_id|
62
+ fill_part mxml.parts[part_id], beats_per_bar * ticks_per_beat, (parts.size > 1 ? part_id : nil), logger, do_log
63
+ end
64
+
65
+ mxml
66
+ end
67
+
68
+ private
69
+
70
+ def fill_part(part, divisions_per_bar, instrument, logger, do_log)
71
+ measure = nil
72
+ dynamics_context = nil
73
+
74
+ (1..finish || 0).each do |bar|
75
+ if do_log
76
+ logger.debug ""
77
+ logger.debug msg = "filling part #{part.name} (#{instrument}): processing bar #{bar}"
78
+ logger.debug "-" * msg.size
79
+ end
80
+
81
+ measure = part.add_measure if measure
82
+ measure ||= part.measures.last
83
+
84
+ pointer = 0r
85
+
86
+ instrument_score = subset { |dataset| dataset[:instrument] == instrument }
87
+
88
+ bar_elements = \
89
+ (instrument_score.changes_between(bar, bar + 1).select { |p| p[:dataset].is_a?(PS) } +
90
+ (pdvs = instrument_score.between(bar, bar + 1).select { |p| p[:dataset].is_a?(PDV) }))
91
+ .sort_by { |e| [ e[:time_in_interval] || e[:start_in_interval],
92
+ e[:dataset].is_a?(PS) ? 0 : 1 ] }
93
+
94
+ if pdvs.empty?
95
+ logger.debug "\nadded full bar silence" if do_log
96
+
97
+ process_pdv(measure, bar, divisions_per_bar,
98
+ { start: bar,
99
+ finish: bar + 1,
100
+ dataset: { pitch: :silence, duration: 1 }.extend(PDV) },
101
+ pointer,
102
+ logger,
103
+ do_log)
104
+ else
105
+ first = bar_elements.first
106
+
107
+ logger.debug "\nfirst element #{first}" if do_log
108
+
109
+ # TODO habrá que arreglar el cálculo de pointer cuando haya avances y retrocesos para que
110
+ # TODO no añada silencios incorrectos al principio o al final
111
+
112
+ if (first[:time_in_interval] || first[:start_in_interval]) > bar
113
+
114
+ silence_duration = first[:start_in_interval] - bar
115
+
116
+ logger.debug "\nadded initial silence for duration #{silence_duration}" if do_log
117
+
118
+ pointer = process_pdv(measure, bar, divisions_per_bar,
119
+ { start: bar,
120
+ finish: first[:start_in_interval],
121
+ dataset: { pitch: :silence, duration: silence_duration }.extend(PDV) },
122
+ pointer,
123
+ logger,
124
+ do_log)
125
+ end
126
+
127
+ bar_elements.each do |element|
128
+ case element[:dataset]
129
+ when PDV
130
+ pointer = process_pdv(measure, bar, divisions_per_bar, element, pointer, logger, do_log)
131
+
132
+ when PS
133
+ dynamics_context = process_ps(measure, element, dynamics_context, logger, do_log)
134
+
135
+ else
136
+ # ignored
137
+ end
138
+ end
139
+
140
+ if pointer < 1r
141
+ silence_duration = 1r - pointer
142
+
143
+ logger.debug "\nadded ending silence for duration #{silence_duration}" if do_log
144
+
145
+ process_pdv(measure, bar, divisions_per_bar,
146
+ { start: bar + pointer,
147
+ finish: bar + 1 - Rational(1, divisions_per_bar),
148
+ dataset: { pitch: :silence, duration: silence_duration }.extend(PDV) },
149
+ pointer,
150
+ logger,
151
+ do_log)
152
+
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end; end
@@ -0,0 +1,23 @@
1
+ require_relative 'dataset'
2
+ require_relative 'packed-v'
3
+
4
+ module Musa::Datasets
5
+ module V
6
+ include AbsI
7
+
8
+ def to_packed_V(mapper)
9
+ case mapper
10
+ when Hash
11
+ pv = {}.extend(PackedV)
12
+ each_index { |i| pv[mapper.keys[i]] = self[i] unless self[i] == mapper.values[i] }
13
+ pv
14
+ when Array
15
+ pv = {}.extend(PackedV)
16
+ each_index { |i| pv[mapper[i]] = self[i] if mapper[i] && self[i] }
17
+ pv
18
+ else
19
+ raise ArgumentError, "Expected Hash or Array as mapper but got a #{mapper.class.name}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
- require 'musa-dsl/generative/variatio'
2
- require 'musa-dsl/generative/darwin'
3
- require 'musa-dsl/generative/rules'
4
- require 'musa-dsl/generative/markov'
5
- require 'musa-dsl/generative/generative-grammar'
1
+ require_relative 'generative/variatio'
2
+ require_relative 'generative/darwin'
3
+ require_relative 'generative/backboner'
4
+ require_relative 'generative/markov'
5
+ require_relative 'generative/generative-grammar'
@@ -0,0 +1,274 @@
1
+ require_relative '../core-ext/smart-proc-binder'
2
+ require_relative '../core-ext/with'
3
+
4
+ using Musa::Extension::Arrayfy
5
+
6
+ # incluir With -> hecho
7
+ # eliminar method_missing
8
+ # crear rama tb debe recibir la serie de la history -> ya lo hace
9
+ # crear rama puede repetirse (hasta terminar según ended_when) -> no
10
+ #
11
+ # 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)
12
+ # esto mismo sería aplicable en otros generadores? variatio/darwin? generative-grammar? markov?
13
+
14
+ module Musa
15
+ module Backboner
16
+ class Backboner
17
+ include Musa::Extension::With
18
+
19
+ def initialize(&block)
20
+ @context = RulesEvalContext.new(&block)
21
+ end
22
+
23
+ def generate_possibilities(object, confirmed_node = nil, node = nil, grow_rules = nil)
24
+ node ||= Node.new
25
+ grow_rules ||= @context._grow_rules
26
+
27
+ history = confirmed_node.history if confirmed_node
28
+ history ||= []
29
+
30
+ grow_rules = grow_rules.clone
31
+ grow_rule = grow_rules.shift
32
+
33
+ if grow_rule
34
+ grow_rule.generate_possibilities(object, history).each do |new_object|
35
+ new_node = Node.new new_object, node
36
+ new_node.mark_as_ended! if @context._ended? new_object
37
+
38
+ rejection = @context._cut_rules.find { |cut_rule| cut_rule.rejects?(new_object, history) }
39
+ # TODO: include rejection secondary reasons in rejection message
40
+
41
+ new_node.reject! rejection if rejection
42
+
43
+ node.children << new_node
44
+ end
45
+ end
46
+
47
+ unless grow_rules.empty?
48
+ node.children.each do |node|
49
+ generate_possibilities node.object, confirmed_node, node, grow_rules unless node.rejected || node.ended?
50
+ end
51
+ end
52
+
53
+ node
54
+ end
55
+
56
+ def apply(object_or_list, node = nil)
57
+ list = object_or_list.arrayfy.clone
58
+
59
+ node ||= Node.new
60
+
61
+ seed = list.shift
62
+
63
+ if seed
64
+ result = generate_possibilities seed, node
65
+
66
+ fished = result.fish
67
+
68
+ node.reject! 'All children are rejected' if fished.empty?
69
+
70
+ fished.each do |object|
71
+ subnode = node.add(object).mark_as_ended!
72
+ apply list, subnode
73
+ end
74
+ end
75
+
76
+ node
77
+ end
78
+
79
+ class RulesEvalContext
80
+ include Musa::Extension::With
81
+
82
+ attr_reader :_grow_rules, :_ended_when, :_cut_rules
83
+
84
+ def initialize(&block)
85
+ with &block
86
+ end
87
+
88
+ def grow(name, &block)
89
+ @_grow_rules ||= []
90
+ @_grow_rules << GrowRule.new(name, &block)
91
+ self
92
+ end
93
+
94
+ def ended_when(&block)
95
+ @_ended_when = block
96
+ self
97
+ end
98
+
99
+ def cut(reason, &block)
100
+ @_cut_rules ||= []
101
+ @_cut_rules << CutRule.new(reason, &block)
102
+ self
103
+ end
104
+
105
+ def _ended?(object)
106
+ instance_exec object, &@_ended_when
107
+ end
108
+
109
+ class GrowRule
110
+ attr_reader :name
111
+
112
+ def initialize(name, &block)
113
+ @name = name
114
+ @block = block
115
+ end
116
+
117
+ def generate_possibilities(object, history)
118
+ # TODO: optimize context using only one instance for all genereate_possibilities calls
119
+ context = GrowRuleEvalContext.new
120
+ context.with object, history, &@block
121
+
122
+ context._branches
123
+ end
124
+
125
+ class GrowRuleEvalContext
126
+ include Musa::Extension::With
127
+
128
+ attr_reader :_branches
129
+
130
+ def initialize
131
+ @_branches = []
132
+ end
133
+
134
+ def branch(object)
135
+ @_branches << object
136
+ self
137
+ end
138
+ end
139
+
140
+ private_constant :GrowRuleEvalContext
141
+ end
142
+
143
+ private_constant :GrowRule
144
+
145
+ class CutRule
146
+ attr_reader :reason
147
+
148
+ def initialize(reason, &block)
149
+ @reason = reason
150
+ @block = block
151
+ end
152
+
153
+ def rejects?(object, history)
154
+ # TODO: optimize context using only one instance for all rejects? checks
155
+ context = CutRuleEvalContext.new
156
+ context.with object, history, &@block
157
+
158
+ reasons = context._secondary_reasons.collect { |_| ("#{@reason} (#{_})" if _) || @reason }
159
+
160
+ reasons.empty? ? nil : reasons
161
+ end
162
+
163
+ class CutRuleEvalContext
164
+ include Musa::Extension::With
165
+
166
+ attr_reader :_secondary_reasons
167
+
168
+ def initialize
169
+ @_secondary_reasons = []
170
+ end
171
+
172
+ def prune(secondary_reason = nil)
173
+ @_secondary_reasons << secondary_reason
174
+ self
175
+ end
176
+ end
177
+
178
+ private_constant :CutRuleEvalContext
179
+ end
180
+
181
+ private_constant :CutRule
182
+ end
183
+
184
+ private_constant :RulesEvalContext
185
+
186
+ class Node
187
+ attr_reader :parent, :children, :object, :rejected
188
+
189
+ def initialize(object = nil, parent = nil)
190
+ @parent = parent
191
+ @children = []
192
+ @object = object
193
+
194
+ @ended = false
195
+ @rejected = nil
196
+ end
197
+
198
+ def add(object)
199
+ Node.new(object, self).tap { |n| @children << n }
200
+ end
201
+
202
+ def reject!(rejection)
203
+ @rejected = rejection
204
+ self
205
+ end
206
+
207
+ def mark_as_ended!
208
+ @children.each(&:update_rejection_by_children!)
209
+
210
+ if !@children.empty? && !@children.find { |n| !n.rejected }
211
+ reject! "Node rejected because all children are rejected"
212
+ end
213
+
214
+ @ended = true
215
+
216
+ self
217
+ end
218
+
219
+ def ended?
220
+ @ended
221
+ end
222
+
223
+ def history
224
+ objects = []
225
+ n = self
226
+ while n && n.object
227
+ objects << n.object
228
+ n = n.parent
229
+ end
230
+
231
+ objects.reverse
232
+ end
233
+
234
+ def fish
235
+ fished = []
236
+
237
+ @children.each do |node|
238
+ unless node.rejected
239
+ if node.ended?
240
+ fished << node.object
241
+ else
242
+ fished += node.fish
243
+ end
244
+ end
245
+ end
246
+
247
+ fished
248
+ end
249
+
250
+ def combinations(parent_combination = nil)
251
+ parent_combination ||= []
252
+
253
+ combinations = []
254
+
255
+ unless rejected
256
+ if @children.empty?
257
+ combinations << parent_combination
258
+ else
259
+ @children.each do |node|
260
+ node.combinations(parent_combination + [node.object]).each do |object|
261
+ combinations << object
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ combinations
268
+ end
269
+ end
270
+
271
+ private_constant :Node
272
+ end
273
+ end
274
+ end