-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathideation.rb
394 lines (352 loc) · 12.9 KB
/
ideation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
module Polyphony
#
# Data and logic for musical ideas in the form of notes and motifs.
#
module Ideation
extend self
# pure functions
# motif hashes
#
# Calculates a Markov chain on the displacements of the given notes. The last displacement is linked back to the first displacement.
#
# @param [Array<Hash>] pZeroedNotes zeroed notes
#
# @return [Hash] displacement Markov chain
#
def calculateDisplacementMarkovChain(pZeroedNotes)
# @type [Hash]
displacementMarkovChain = {}
# @type [Array<Hash>]
notes = pZeroedNotes + [pZeroedNotes.first] # loop around
# @type [Array<Integer>]
d0s = notes[0..-2].map { |note| note[:displacement] }.uniq
d0s.each { |key| displacementMarkovChain[key] = {} }
notes[1..-1].each_with_index do |n, i|
# @type [Integer]
d0 = notes[i][:displacement]
# @type [Integer]
d1 = n[:displacement]
# @type [Integer]
wt = displacementMarkovChain[d0].fetch(d1, 0)
wt += 1
displacementMarkovChain[d0][d1] = wt
end
displacementMarkovChain.each_value { |wts| wts.freeze }
return displacementMarkovChain.freeze
end
#
# Creates a random motif by dividing and subdividing the given number of measure units, with displacement intervals chosen by the given settings, keeping within the given displacement bounds.
#
# @param [RangePairI] pBounds displacement bounds
# @param [Integer] pNumUnitsPerMeasure number of units in motif
# @param [Hash] pSettingsCreation creation settings
#
# @return [Hash] a random motif respecting the givens
#
def createBoundMotif(pBounds, pNumUnitsPerMeasure, pSettingsCreation)
# @type [Array<Hash>]
notes = []
while notes.all? { |note| note[:displacement].nil? }
# @type [Array<Integer>]
divisions = divideUnitsRhythmically(pNumUnitsPerMeasure, pSettingsCreation[:weightForSpans])
# @type [Array<Integer>]
subdivisions = divisions.map { |division| subdivideUnitsRhythmically(division, pSettingsCreation[:weightForSpans]) }.flatten.freeze
subdivisions = mergeBriefStart(subdivisions) if pSettingsCreation[:shouldMergeBriefStart]
# @type [Array<Hash>]
notes = [createInitialNote(subdivisions.first, pSettingsCreation)]
subdivisions[1...subdivisions.length].each do |subdivision|
notes.push(createNextBoundNote(notes, pBounds,subdivision, pSettingsCreation))
end
end
return makeMotif(notes)
end
#
# Creates a motif bound by the given settings and taking up the given number of units.
#
# @param [Integer] pNumUnitsPerMeasure number of units in motif
# @param [Hash] pSettingsCreation creation settings
#
# @return [Hash] a random motif respecting the given settings
#
def createMotif(pNumUnitsPerMeasure, pSettingsCreation)
return createBoundMotif(RangePairI.new(pSettingsCreation[:displacementLimit]).freeze, pNumUnitsPerMeasure, pSettingsCreation)
end
#
# @param [Array<Hash>] pMotifs motif hashes
#
# @return [Integer] max peak of given motif hashes
#
def getPeakOfMotifs(pMotifs)
return pMotifs.max { |m0, m1| m0[:peak] <=> m1[:peak] }[:peak]
end
#
# @param [Array<Hash>] pMotifs motif hashes
#
# @return [Integer] min trough of given motif hashes
#
def getTroughOfMotifs(pMotifs)
return pMotifs.min { |m0, m1| m0[:trough] <=> m1[:trough] }[:trough]
end
#
# Returns a motif hash with the inverse properties of the given motif.
#
# @param [Hash] pMotif motif
#
# @return [Hash] inverted motif
#
def invertMotif(pMotif)
return makeMotif(pMotif[:notes].map { |note| invertNote(note) })
end
#
# Used for diagnosis.
#
# @param [Hash] pMotif motif hash
#
# @return [Array<Array<Integer>>] array of arrays containing displacement and span at indices 0 and 1 respectively
#
def makeArraysFromMotif(pMotif)
# @type [Array<Array<Integer>>]
arrays = pMotif[:notes].map { |note| makeArrayFromNote(note) }.freeze
return arrays
end
#
# Used for custom motifs.
#
# @param [Array<Array<Integer>>] pArrays array of arrays containing displacement and span at indices 0 and 1 respectively
#
# @return [Hash] motif hash
#
def makeMotifFromArrays(pArrays)
# @type [Array<Hash>]
notes = pArrays.map { |array| makeNote(array[0], array[1]) }.freeze
return makeMotif(notes)
end
#
# Makes a motif hash from the given notes. A motif is a musical molecule. It has an array of featureful notes, a peak, and a trough.
#
# @param [Array<Hash>] pNotes array of note hashes
#
# @return [Hash] a motif hash
#
def makeMotif(pNotes)
# @type [Array<Hash>]
zeroedNotes = zeroNotes(pNotes)
return {
notes: zeroedNotes,
peak: getPeakOfNotes(zeroedNotes),
trough: getTroughOfNotes(zeroedNotes),
displacementMarkovChain: calculateDisplacementMarkovChain(zeroedNotes),
}.freeze
end
#
# Returns a motif hash with the retrograde properties of the given motif.
#
# @param [Hash] pMotif motif
#
# @return [Hash] retrograde motif
#
def retrogradeMotif(pMotif)
return makeMotif(pMotif[:notes].reverse)
end
# note hashes
#
# Creates an initial note hash, which can have a nil displacement if the set chance of such evaluates true.
#
# @param [Integer] pSpan span
# @param [Hash] pSettingsCreation creation settings
#
# @return [Hash] initial note hash
#
def createInitialNote(pSpan, pSettingsCreation)
if pSettingsCreation[:chanceCreateNilFeature].evalChance?
return makeNote(nil, pSpan)
else
return makeNote(0, pSpan)
end
end
#
# Creates a random note whose displacement is bound by preceding note hashes so as to not exceed a set displacement interval limit.
#
# @param [Array<Hash>] pNotes preceding note hashes
# @param [RangePairI] pBounds max and min displacements
# @param [Integer] pSpan span of note hash to create
# @param [Hash] pSettingsCreation creation settings
#
# @return [Hash] a random note hash bound by preceding note hashes so as to not exceed the set displacement interval limit
#
def createNextBoundNote(pNotes, pBounds, pSpan, pSettingsCreation)
return createInitialNote(pSpan, pSettingsCreation) if pNotes.all? { |note| note[:displacement].nil? }
# @type [Integer]
previousDisplacement = getLastFeatureOfNotes(pNotes)
# @type [Array<Integer>]
displacementIntervals = RangePairI.new(pSettingsCreation[:displacementIntervalLimit]).toRangeA.freeze
displacementIntervals = filterDisplacementIntervalsForCompatibility(displacementIntervals, pNotes, pBounds, pSettingsCreation)
displacementIntervals -= [0] if pSettingsCreation[:chanceCreateNoConsecutivelyRepeatedDisplacements].evalChance?
# @type [Integer]
displacement = nil
unless displacementIntervals.empty? || pSettingsCreation[:chanceCreateNilFeature].evalChance?
if pNotes.last[:displacement].nil?
displacement = previousDisplacement + displacementIntervals.choose
else
displacement = previousDisplacement + chooseAbsIntWithWeight(pSettingsCreation[:weightForDisplacementIntervals], displacementIntervals)
end
end
return makeNote(displacement, pSpan)
end
#
# Filters the given displacement intervals so that the remaining intervals are within the set displacement limit when taken together with the given preceding note hashes.
#
# @param [Array<Integer>] pDisplacementIntervals displacement intervals
# @param [Array<Hash>] pNotes preceding note hashes
# @param [RangePairI] pBounds max and min displacements
# @param [Hash] pSettingsCreation creation settings
#
# @return [Array<Integer>] intervals that are within the set displacement limit when taken together with the given preceding note hashes
#
def filterDisplacementIntervalsForCompatibility(pDisplacementIntervals, pNotes, pBounds, pSettingsCreation)
# @type [Integer]
peak = getPeakOfNotes(pNotes)
# @type [Integer]
trough = getTroughOfNotes(pNotes)
# @type [Integer]
distance = peak - trough
# @type [Integer]
leeway = pSettingsCreation[:displacementLimit] - distance
# @type [Integer]
maxPeak = getMin((peak + leeway), pBounds.max)
# @type [Integer]
minTrough = getMax((trough - leeway), pBounds.min)
# @type [Integer]
previousDisplacement = getLastFeatureOfNotes(pNotes)
return pDisplacementIntervals.select { |di| (previousDisplacement + di).between?(minTrough, maxPeak) }.freeze
end
#
# @param [Array<Hash>] pNotes note hashes
#
# @return [Array<Integer>] displacements of given note hashes
#
def getDisplacementsOfNotes(pNotes)
return pNotes.map { |note| note[:displacement] }
end
#
# @param [Array<Hash>] pNotes note hashes
#
# @return [Integer] last non-nil displacement of note hashes
#
def getLastFeatureOfNotes(pNotes)
return pNotes.reverse_each.detect { |note| !note[:displacement].nil? }[:displacement]
end
#
# @param [Array<Hash>] pNotes note hashes
#
# @return [Integer] max displacement of the given note hashes
#
def getPeakOfNotes(pNotes)
return getDisplacementsOfNotes(pNotes).compact.max
end
#
# @param [Array<Hash>] pNotes note hashes
#
# @return [Integer] min displacement of the given note hashes
#
def getTroughOfNotes(pNotes)
return getDisplacementsOfNotes(pNotes).compact.min
end
#
# Returns a note whose displacement is flipped from the given note.
#
# @param [Hash] pNote note hash
#
# @return [Hash] inverted note
#
def invertNote(pNote)
return pNote if pNote[:displacement].nil?
return makeNote(-pNote[:displacement], pNote[:span])
end
#
# Used for diagnosis.
#
# @param [Hash] pNote note hash
#
# @return [Array<Integer>] array containing displacement and span at indices 0 and 1 respectively
#
def makeArrayFromNote(pNote)
# @type [Array<Integer>]
array = [pNote[:displacement], pNote[:span]].freeze
return array
end
#
# Creates a note hash. A note is a musical atom. It has a displacement and a span.
#
# @param [Integer, NilClass] pDisplacement spatial property
# @param [Integer, NilClass] pSpan temporal property
#
# @return [Hash] note hash
#
def makeNote(pDisplacement, pSpan)
return {
displacement: pDisplacement,
span: pSpan,
}.freeze
end
#
# @param [Hash] pNote note hash
# @param [Integer] pDistance distance to transpose
#
# @return [Hash] note hash transposed by the given distance
#
def transposeNote(pNote, pDistance)
return pNote if pNote[:displacement].nil? || pDistance.zero?
return makeNote((pNote[:displacement] + pDistance), pNote[:span])
end
#
# Transposes all the given note hashes by the given distance.
#
# @param [Array<Hash>] pNotes note hashes
# @param [Integer] pDistance transposition distance
#
# @return [Array<Hash>] transposed note hashes
#
def transposeNotes(pNotes, pDistance)
if pDistance.zero?
return pNotes
else
return pNotes.map { |note| transposeNote(note, pDistance) }.freeze
end
end
#
# Transposes the given note hashes so that the first encountered feature has a displacement of zero.
#
# @param [Array<Hash>] pNotes note hashes
#
# @return [Array<Hash>] zeroed note hashes
#
def zeroNotes(pNotes)
return transposeNotes(pNotes, -pNotes.detect { |note| !note[:displacement].nil? }[:displacement]).freeze
end
# impure functions
#
# Either creates a motif hash if none are in the time-state or the chance to create a one time motif evaluates true, or retrieves a time-state motif hash and transforms it if the chance to do so evaluates true.
#
# @return [Hash] original motif hash or possibly transformed time-state hash
#
def ideate
if get(-"motifs").empty? || Settings::IDEATION[:chanceCreateOneTimeMotif].evalChance?
return createMotif(Settings::TIMEKEEPING[:numUnitsPerMeasure], Settings::CREATION)
else
# @type [Hash]
motif = get(-"motifs").choose
if Settings::IDEATION[:chanceInvertMotif].evalChance?
motif = invertMotif(motif)
end
if Settings::IDEATION[:chanceRetrogradeMotif].evalChance?
motif = retrogradeMotif(motif)
end
return motif
end
end
# constants
# Motif with one note, which note is zeroed and indefinite.
INFINITUM = makeMotif([makeNote(0, nil)].freeze)
end
end