-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathkonduktiva-revised-2.js
executable file
·1003 lines (926 loc) · 37.9 KB
/
konduktiva-revised-2.js
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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//------------------------------------------------------------------------------
//-- konduktiva-revised.js
//-- Wed Feb 2 07:28:37 PM JST 2022
// license not yet decided; please do not distribute yet.
//------------------------------------------------------------------------------
const R = await import('ramda')
const performance = await import('perf_hooks');
performance = performance.performance
let TaskTimerDefault = await import('tasktimer');
const {TaskTimer} = TaskTimerDefault.default
// const {TaskTimer} = require('tasktimer')
//const easymidi = require('easymidi');
const fs = await import('fs')
const path = await import('path')
let oscDefault = await import("osc");
const osc = oscDefault.default
const v8 = await import('v8');
const A = await import('array-toolkit')
// const A = await import('./github-array-toolkit-package/array-toolkit/array-toolkit.mjs')
/**
* Returns the current year, month, and day (yyMMdd)
* @example console.log(ymd()) //'230821'
*/
function ymd () {
let dateObj = new Date();
let y = (""+dateObj.getFullYear()).slice(2);
let m = "" + (dateObj.getMonth() + 1);
if (m.length < 2) {m = "0"+m} else {m = "" + m}
let d = dateObj.getDate();
if (d.length < 2) {d = "0"+d} else {d = "" + d}
return y+m+d
}
const structuredClone = obj => {
return v8.deserialize(v8.serialize(obj));
};
//--------------------------------------------------------------------------
// utility
/**
* Adds the input of the function to a file called test.log
* @param {*} x - The item to add to test.log file.
* @example
* console.log(addLog([0, 1, 2, 3])) //will add: 0,1,2,3
* console.log(addLog('hi')) //will add: hi
*/
function addLog (x) {
let d = new Date ();
fs.appendFile('test.log',
//"--------------------------------------------------------------------------"
// + "\nbegin: "+d+"\n\n"+x+"\n" +
"\n"+x+"\n" +
// + "\n-- end. " + d + "\n" +
"--------------------------------------------------------------------------"
+ "\n"
, function (err) { if (err) throw err; /*console.log('Saved!');*/ })
}
/**
* Adds the item and the item type to the file test.log
* @param {*} x - Item to add.
* @example
* console.log(addLog2([0, 1, 2, 3])) //will add: [ 0, 1, 2, 3 ]
* console.log(addLog2('hi')) //will add: 'hi'
*/
function addLog2 (x) {addLog(util.inspect(x, {maxArrayLength: null, depth: null}))}
/**
* Outputs how long since the nodejs session has started in seconds.
* @requires perf_hooks
* @see {@link https://nodejs.org/api/perf_hooks.html#performancenow} to see how performance.now() works.
* @example now() //23.817596345998346
*/
function now () {
return 0.001 * performance.now()
}
/**
* Returns a whole number between the min and max values.
* @param {number} min - Minimum amount the random number can be.
* @param {number} max - Maximum amount the random number can be.
* @example console.log(randomRangeInt(0, 10)) //9
*/
function randomRangeInt (min, max) {
return Math.floor(randomRange(min, max))
}
/**
* A lerp/linear interpolation function finding a value that is between two other values by a certain fraction, which is represented by the s parameter in your function (partially generated by chatgpt)
* @see {@link https://en.wikipedia.org/wiki/Linear_interpolation}
* @param {number} y1
* @param {number} y2
* @param {number} s
* @example console.log(lerpValues(10, 20, 30)) //310
*/
function lerpValues (y1, y2, s) {return y1 + s * (y2 - y1)}
/**
* Returns an array of numbers of a specific length that are always equally different in amount. This function calculates a specific number of intervals to get from start number to stop number.
* @param {number} start - Number to start from.
* @param {number} stop - Number to stop at.
* @param {number} steps - The number of intervals/steps to take to get from start to stop
* @example
* console.log(lerpedRange(0, 10, 4)) //[ 0, 5, 10 ]
* console.log(lerpedRange(0, 10, 7)) //[ 0, 2, 4, 6.000000000000001, 8, 10 ]
*/
function lerpedRange (start, stop, steps) {
let stepArray = A.integerArray(0,steps-1);
let stepSize = 1/(stepArray.length-1);
let scalars = stepArray.map(x => x * stepSize);
return scalars.map(x => lerpValues (start, stop, x))
}
/** Class for representing a point. */
class Point {
/**
* Creates the point.
* @param {*} x - X value.
* @param {*} y - Y value.
*/
constructor(x,y) {
this.x = x;
this.y = y;
}
/**
* Moves the point.
* @param {*} xDistance - Item that gets added to the x value.
* @param {*} yDistance - Item that gets added to the y value.
*/
move(xDistance, yDistance) {
return new Point(this.x + xDistance, this.y + yDistance)
}
/**
* Moves the point by angle and distance.
* @param {number} angle - Angle point should rotated by.
* @param {number} distance - The amoun the point should be moved along the angle.
*/
moveByAngle (angle, distance) {
let r = angle * Math.PI / 180;
return new Point(this.x + distance*Math.sin(r), this.y + distance*Math.cos(r))
}
}
/**
* Function that calculates slope intercept. It returns the an object with the equation stored as a function and as a string.
* @param {Point} p1 - First Point.
* @param {Point} p2 - Second Point.
*/
function linearFunctionFromPoints(p1,p2) {
let rise = p2.y - p1.y;
let run = p2.x - p1.x;
let slope = rise/run;
// y = mx + b
let b = p1.y - (slope * p1.x);
//console.log('the linear function is: y = ' + slope + 'x + ' + b);
return {func: (x => (x * slope) + b), note: 'y = ' + slope + 'x + ' + b}
}
/**
* Calculates all the slope intercepts in an array of points.
* @param {array} pointArray - The array of Points to sort through.
* @example console.log(linearFunctionArrayFromPoints([new Point(0, 10), new Point(5, 20), new Point(10, 30)])) //[ { func: [Function: func], note: 'y = 2x + 10' }, { func: [Function: func], note: 'y = 2x + 10' } ]
*/
function linearFunctionArrayFromPoints (pointArray) {
return A.safeSplice(pointArray, 1, 0).map((x, i) =>{
return linearFunctionFromPoints(pointArray[i],pointArray[i+1])
})
}
//partially helped by chatgpt
/**
* Calculates all the slope intercepts in an array of points and returns them in form of a QuantizedMap. The keyspan will be the final X value. The keys will will be all the X values in the array.
* @param {array} pointArray - The array of Points to sort through.
* @example
* linearFunctionQuantizedMap([new Point(0, 10), new Point(5, 20), new Point(10, 30)]) //QuantizedMap { keyspan: 10, keys: [ 0, 5, 10 ], values: [ { func: [Function: func], note: 'y = 2x + 10' }, { func: [Function: func], note: 'y = 2x + 10' } ] }
*/
function linearFunctionQuantizedMap (pointArray) {
let times = pointArray.map(t => t.x);
return new QuantizedMap(times[times.length-1], times, linearFunctionArrayFromPoints(pointArray))
}
// knuth shuffle from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
//------------------------------------------------------------------------------
// lsystem functions
function parseItem (input, rules) {
if (Object.keys(rules).includes(input)) {
return rules[input]}
else {
return input
}
}
// let parseItem = (input,rules) => { if (Object.keys(rules).includes(input)) {return rules[input]} else {return input}}
// let parseString = (inputString,rules) => inputString.split("").map(x => parseItem(x,rules)).join().replace(/,/g,"")
function parseString (inputString, rules) {
return inputString.split("").map(x => parseItem(x,rules)).join().replace(/,/g,"")
}
function lsystem (inputString,rules,generations) {
if (generations > 0) {return lsystem (parseString(inputString,rules),rules,generations-1)}
else {return inputString}
}
function rewriteString (inputString, stringMap) {
let splitString = inputString.split('');
return splitString.map(c => stringMap[c])
}
// uses code from Javier Conde https://stackoverflow.com/questions/34929094/how-can-i-get-all-possible-characters-in-javascript
/**
* Returns an array of all the English alphabets.
*/
function getAllAlphabets() {
return Array.from({ length: 26 * 2 }, (_, index) => {
let charCode = index < 26 ? 97 + index : 65 + index - 26;
return String.fromCharCode(charCode);
});
}
//Generated by Chatgpt:
/**
* Assigns each item of an array a letter of the English alphabet.
* @param {array} inputArray - Array of things to assign letter to.
* @example console.log(randomCharacterMapFromArray([0, 1, 2, 10])) //{ a: 1, b: 0, c: 2, d: 10 }
*/
function randomCharacterMapFromArray (inputArray) {
let allChars = getAllAlphabets();
if (allChars.length < inputArray.length) {return "error: inputArray is longer than array of alphabetic characters"};
let outputMap = {};
charactersToUse = A.takeN(allChars,inputArray.length);
let shuffled = A.shuffle(inputArray);
charactersToUse.forEach((x,i) => {outputMap[x] = shuffled[i]})
return outputMap
}
/**
* Assigns an item of one array to one item of the other array.
* @param {array} keyArray - Array of keys.
* @param {array} valueArray - Array of values.
* @example console.log(randomMap(['a', 'b', 'c', 'd'], [0, 1, 2, 3])) //{ a: 1, b: 3, c: 2, d: 0 }
*/
function randomMap (keyArray, valueArray) {
if (keyArray.length < valueArray.length) {return "error: key array is longer than value array"};
let outputMap = {};
charactersToUse = A.takeN(keyArray,valueArray.length);
let shuffled = A.shuffle(valueArray);
charactersToUse.forEach((x,i) => {outputMap[x] = shuffled[i]})
return outputMap
}
function rulesGenerator (inputPool, maxRuleLength) {
let ruleLengths = A.pickN (inputPool.length, A.linearArray(1,1,maxRuleLength));
return (ruleLengths.map(x => A.pickN(x, inputPool) ) )
}
function variousLsystems(baseName,n,patternLength,rules,generations,replacementValues,inputString) {
let replacements = A.buildArray(n, x => randomMap(getAllAlphabets(), replacementValues));
let thisL = lsystem (inputString, rules, generations);
let lsystems = replacements.map(x => A.loopTo(patternLength,rewriteString(thisL,x)))
let names = A.buildArray(n, i => baseName+i);
let outputMap = {};
names.forEach((x,i) => outputMap[x] = lsystems[i]);
return outputMap
}
//--------------------------------------------------------------------------
//midi actions
/**
* Turns off all notes on a specific channel for the output.
* @param {number} channelNum - Channel to send off to.
*/
function allNotesOff(channelNum) {
Array.from({ length: 128 }, (_, noteNumber) => {
output.send('noteoff', {
note: noteNumber,
velocity: 0,
channel: channelNum,
});
});
}
//partially generated by chatgpt
//--------------------------------------------------------------------------
// the easiest way to make a rhythm
/**
* Helps create a simpel rhythm in Konduktiva.
* @param env - Musical Environment
* @param {string} rhythmName - The name of the new rhythm.
* @param {array}
*/
function simpleRhythm (env, rhythmName, deltas) {
env.rhythmMaps[rhythmName] = new QuantizedMap(1,[1],[new QuantizedMap(A.sum(deltas),[0].concat(A.runningSum(0,deltas)),deltas)])
return rhythmName
}
//--------------------------------------------------------------------------
// rhythm pattern with density
// increase density
// revise these so that the algorithm is swappable
/**
* Finds the highest value number and multiplies it by the ratio. The same number is also subtracted by itself after it is multiplied by the ratio. Both of those numbers are added to the array behind that highest value number.
* @param {number} minVal - The highest value number in the inputArray has to be greater than this number.
* @param {number} ratio - The number the highest value number will be multipled by.
* @param {array} inputArray - The array to modify.
* @example
* console.log(increaseDensity(0, 10, [0, 1, 2, 3, 5])) //[ 0, 1, 2, 3, 50, -45 ]
* console.log(increaseDensity(0, 10, [0, 1, 2, 8, 3, 5])) //[ 0, 1, 2, 80, -72, 3, 5 ]
*/
function increaseDensity (minVal,ratio,inputArray) {
let max = A.getMaxIndex(inputArray);
if (max[0][1] <= minVal) {console.log("max is already at minVal");return inputArray}
else {
let toIncrease = A.pick(max);
console.log("this is the max: " + toIncrease);
let outputA = inputArray.slice(0,toIncrease[0]);
console.log("this is outputA: " + outputA);
let outputB = inputArray.slice(toIncrease[0]+1);
let newVals = [toIncrease[1]*ratio,toIncrease[1] - (toIncrease[1]*ratio)];
return outputA.concat(newVals.concat(outputB))
}
}
/**
* Returns an array that has the same total value and the same length but the numbers in the array will be different.
* @param {array} inputArray - Number array.
* @example
* console.log(decreaseDensity([0, 1, 2, 3, 10])) //[ 0, 1, 5, 10 ] //[ 0, 3, 3, 10 ] //[ 1, 2, 3, 10 ]
*/
function decreaseDensity (inputArray) {
let target = (A.pick(A.integerArray(0,inputArray.length - 2)));
let outputA = inputArray.slice(0,target);
let outputB = [inputArray[target] + inputArray[target+1]].concat(inputArray.slice(target+2))
return outputA.concat(outputB)
}
/**
* Uses the increaseDensity function on multiple arrays and picking randomly from an array of ratios.
* @param {number} minVal - The minimum value the the highest value number in the first item of the stack array can be.
* @param {array} ratio -
*/
function recursiveIncreaseDensity (minVal, ratios, stack) {
console.log(minVal, stack)
if (A.getMaxIndex(stack[0])[0][1] > minVal) {
let r = A.pick(ratios);
let addToStack = increaseDensity(minVal,r,stack[0]);
return recursiveIncreaseDensity(minVal, ratios, [addToStack].concat(stack))
return [addToStack].concat(stack)
}
return stack
}
function recursiveDecreaseDensity (stack) {
console.log(stack);
let targetArray = stack[(stack.length -1)];
if (targetArray.length > 1) {
console.log("this is the target array");
return recursiveDecreaseDensity(stack.concat([decreaseDensity(stack[stack.length -1])]))
}
return stack
}
function densityStack (minVal, ratios, inputArray) {
return recursiveDecreaseDensity(recursiveIncreaseDensity(minVal, ratios, [inputArray]))
}
// musical time
function deltaToAbsolute (inputArray) {
let output = [0];
inputArray.forEach((e,i) => output.push(output[i]+e));
return [R.last(output),R.init(output)]
}
function absoluteToDelta (inputArray) {
let output = [];
[head, ...tail] = inputArray;
tail.forEach((e,i) => output.push(e - inputArray[i]))
return output
}
function densityFromDeltas (inputDeltas) {
return inputDeltas.length/A.sum(inputDeltas)
}
// keyspan is max value, keys is an array of absolutes (increasing values), values is an array of anything of the same length as keys
/** A QuantizedMap is a discreet function.
* @see {@link https://www.sparknotes.com/math/algebra2/discretefunctions/summary/}
*/
class QuantizedMap {
/**
* Creates the QuantizedMap. The methods provide different ways to look for things in the quantized map.
* @param {number} limitValue - The keyspan/total of the QuantizedMap.
* @param {array} keys - The keys of the QuantizedMap which will be a number array in ascending order.
* @param {array} vals - The values of the QuantizedMap which will be an array.
* @example
* let qMap = new QuantizedMap(10, [0, 2, 4, 6, 8], ['A', 'B', 'C', 'D']) //QuantizedMap { keyspan: 10, keys: [ 0, 2, 4, 6, 8 ], values: [ 'A', 'B', 'C', 'D' ] }
*/
constructor(limitValue,keys,vals) {
this.keyspan = limitValue;
this.keys = keys;
this.values = vals;
}
/**
* The wrapLookup method is also similar to the nearestLookup method but when the number provided is greater than the keyspan, it does not return the last item in the values array instead it loops back around.
* @param {number} time - The beat number.
* @example
* qMap.wrapLookup(4) //'C'
* qMap.wrapLookup(5) //'C'
* qMap.wrapLookup(6) //'D'
* qMap.wrapLookup(10) //'A'
* qMap.wrapLookup(11) //'A'
* qMap.wrapLookup(12) //'B'
*/
wrapLookup(time) {
let lookupTime = time%this.keyspan;
let filteredTime = this.keys.filter(x => x <= lookupTime);
if (filteredTime[0] == undefined) {filteredTime = [0]};
return this.values[(filteredTime.length - 1)]
}
/**
* The floorLookup method does something similar to nearestLookup but when it looks for the closest it always looks for a number smaller than it.
* @param {number} time - The beat number.
* @example
* qMap.floorLookup(4) //'C'
* qMap.floorLookup(5) //'C'
* qMap.floorLookup(6) //'D'
* qMap.floorLookup(10) //'D'
* qMap.floorLookup(11) //'D'
*/
floorLookup(time) {
let lookupTime = time;
let output = undefined;
if (lookupTime >= this.keyspan)
{output = this.values[this.values.length - 1]}
else if (lookupTime < this.keys[0])
{output = this.values[0]}
else {
let filteredTime = this.keys.filter(x => x <= lookupTime);
if (filteredTime[0] == undefined) {filteredTime = [0]};
output = this.values[(filteredTime.length - 1)]
}
return output
}
/**
* Takes a number as an input and will look for a number in the keys array that is closest to it compared to the others. It will then take the index of that number and return the value array using that index.
* @param {number} time - The beat number.
* @example
* qMap.nearestLookup(4) //'C'
* qMap.nearestLookup(5) //'D'
* qMap.nearestLookup(6) //'D'
* qMap.nearestLookup(10) //'D'
* qMap.nearestLookup(11) //'D'
*/
nearestLookup (time) {
let lookupTime = time;
let output = undefined;
if (lookupTime >= this.keys[this.keys.length - 1])
{output = this.values[this.values.length - 1]}
else if (lookupTime < this.keys[0])
{output = this.values[0]}
else {
let filteredTime = this.keys.filter(x => x <= lookupTime);
if (filteredTime[0] == undefined) {filteredTime = [0]};
let lower = filteredTime[filteredTime.length - 1];
let higher = this.keys[filteredTime.length]
if ((lookupTime - lower) < (higher - lookupTime))
{output = this.values[(filteredTime.length - 1)]}
else {output = this.values[(filteredTime.length)]}
}
return output
}
/**
* nearestLookup but the numbers are wrapped.
* @param {number} time - Number to lookup
*/
nearestWrapLookup (time){
return this.nearestLookup(time % this.keyspan)
}
/**
* floorLookup but the numbers are wrapped.
* @param {number} time - Number to lookup
*/
floorWrapLookup (time){
return this.floorLookup(time % this.keyspan)
}
}
/**
* A helper function for navigating QuantizedMap. It will tell you which key will be used at a specific time when using the wrapLookup method.
* @param {QuantizedMap} qm - QuantizedMap to filter through.
* @param {number} time - The argument for the wrapLookup.
* @example
* console.log(whichWrapKey(new QuantizedMap(10, [0, 2, 4, 6, 8], ['A', 'B', 'C', 'D']), 2)) //2
* console.log(whichWrapKey(new QuantizedMap(10, [0, 2, 4, 6, 8], ['A', 'B', 'C', 'D']), 3)) //2
* console.log(whichWrapKey(new QuantizedMap(10, [0, 2, 4, 6, 8], ['A', 'B', 'C', 'D']), 10)) //0
* console.log(whichWrapKey(new QuantizedMap(10, [0, 2, 4, 6, 8], ['A', 'B', 'C', 'D']), 12) //2
*/
function whichWrapKey(qm, time) {
let lookupTime = time%qm.keyspan;
let filteredTime = qm.keys.filter(x => x <= lookupTime);
if (filteredTime[0] == undefined) {filteredTime = [0]};
return filteredTime[filteredTime.length - 1]
}
// time needs to be remaindered by the keyspan!
function calculateDensity (env, playerName, time) {
let dGraphs = env.currentDensityGraphs.map(x => env.densityGraphs[x]);
//console.log(dGraphs);
let densityFuncMaps = dGraphs.map((x) => {
if (Object.keys(x).includes(playerName)) {return x[playerName]}
else if (Object.keys(x).includes('default')) {return x.default}
// this has to be a QuantizedMap
// else {return x => 1}
else {return linearFunctionQuantizedMap([new Point(0,1), new Point (1,1)]) }
});
let densityFuncs = densityFuncMaps.map(x => [(x.floorLookup(time%x.keyspan)).func,x.keyspan])
let densities = densityFuncs.map(x => x[0](time%x[1]))
return A.mean(densities)
}
/**
* Helps to make a rhythmMap in QuantizedMap form.
* @param {number} minIOI - The minimum value of the IOI
*/
function makeRhythmMap (minIOI, ratios, deltas) {
let stack = densityStack(minIOI,ratios,deltas);
let absolutes = stack.map(deltaToAbsolute);
let densities = stack.map(densityFromDeltas)
let rows = stack.map((x, i) => {
return new QuantizedMap(absolutes[i][0],absolutes[i][1],stack[i])
})
return new QuantizedMap(densities[0],R.reverse(densities),R.reverse(rows))
}
//Might be wrong don't know how to test.
//------------------------------------------------------------------------------
// stochastic rhythmMap functions
function generateSeed (onsetValues, seedLengths) {
let seedLength = A.pick(seedLengths);
return A.pickN(seedLength, onsetValues)
}
function generatePhrase (onsetValues, seedLengths, noOfSeeds, phraseLength) {
let seeds = A.buildArray(noOfSeeds, x => generateSeed(onsetValues, seedLengths));
return A.takeTo(phraseLength,R.flatten(seeds))
}
function generateAndAddRhythms(env, n, baseName, onsetValues, minIOI, ratios, seedLengths, noOfSeeds, phraseLength) {
let names = A.buildArray(n, x => baseName + x);
let rhythms = A.buildArray(n, x => generatePhrase(onsetValues,seedLengths,noOfSeeds,phraseLength));
let rhythmMaps = rhythms.map(r => makeRhythmMap(minIOI, ratios, r));
names.forEach((name,i) => {return env.rhythmMaps[name] = rhythmMaps[i]})
}
//------------------------------------------------------------------------------
// getting IOIs
function getRemainingDelta (densityMap, density, beat) {
let wrappedBeat = beat % densityMap.floorLookup(density).keyspan;
let currentDelta = densityMap.floorLookup(density).floorLookup(wrappedBeat);
let currentKey = whichWrapKey(densityMap.floorLookup(density),beat);
let remainingDelta = currentDelta - (wrappedBeat - currentKey);
return remainingDelta
}
function getNextOnset (densityMap, density, beat) {
return beat + getRemainingDelta(densityMap, density, beat)
}
function getDelta (densityMap, density, beat) {
return densityMap.floorLookup(density).wrapLookup(beat)
}
function getNextOnsetFromRhythmMap (densityMap, density, beat) {
return beat + getRemainingDelta(densityMap, density, beat)
}
function getIOI (env, player, beat) {
let density = calculateDensity (env, player, beat);
let rhythmMap = env.rhythmMaps[env.players[player].rhythmMap]
let onset = getNextOnsetFromRhythmMap(rhythmMap,density,beat)
return onset
}
// write a generalized version of this that can accept any IOIFunc
function getNextOnsets (densityMap, density, beat, limitBeat, output) {
if (beat > limitBeat) {
return output
}
else {
// replace getNextOnset with generalized IOIFunc here?
let nextOnset = getNextOnset(densityMap, density, beat);
output = output.concat(nextOnset)
return getNextOnsets (densityMap, density, beat+nextOnset, limitBeat, output)
}
}
function getNextOnsets2 (ioiFunc, beat, limitBeat, output) {
if (beat > limitBeat) {
return output
}
else {
// replace getNextOnset with generalized IOIFunc here?
let nextOnset = ioiFunc(beat);
output = output.concat(nextOnset)
return getNextOnsets2 (ioiFunc, beat+nextOnset, limitBeat, output)
}
}
function getNextOnsets3 (ioiFunc, player, beat, limitBeat, output) {
if (beat > limitBeat) {
return output
}
else {
// replace getNextOnset with generalized IOIFunc here?
let nextOnset = ioiFunc(player, beat);
output = output.concat(nextOnset)
return getNextOnsets3 (ioiFunc, player, beat+nextOnset, limitBeat, output)
}
}
// probability of masking
function mask (player, maskMap, beat, probability) {
let maskVal = maskMap.wrapLookup(beat);
if (maskVal == true && (Math.random() < probability)) {maskVal = true} else {maskVal = false};
//if (maskVal == true) {addLog(''+player+' function was masked at beat '+beat+'.')}
return maskVal
}
/** Class representing MusicalEnvironments */
class MusicalEnvironment {
/**
* Creates MusicalEnvironments. Remember to call setupScheduler(e)
* @example let e = new MusicalEnvironment()
*/
constructor (){
this.players = {};
this.actions = {};
this.IOIs = {};
this.densityGraphs = {};
this.rhythmMaps = {};
this.maskMaps = {};
this.superDirtPath = undefined;
this.samples = undefined;
this.sampleKits = {};
this.samplePatterns = {};
this.samplePatternCount = 0;
this.samplePatternStore = {};
this.currentDensityGraphs = [];
this.currentBeatsPerMeasure = 4;
this.currentTempo = 120;
this.beatOfChangeToCurrentTempo = undefined;
this.timeOfChangeToCurrentTempo = undefined;
this.scheduler = new TaskTimer(20);
this.lookahead = 0.1;
this.scheduledPlayers = [];
this.root = "A";
}
/**
* Returns the current beat of the MusicalEnvironment.
*/
currentBeat () {
let elapsed = now() - this.timeOfChangeToCurrentTempo;
return timeToBeats(this.currentTempo, elapsed) + this.beatOfChangeToCurrentTempo
}
/**
* Changes the tempo of the MusicalEnvironment.
* @param {number} tempo - New tempo of the current MusicalEnvironment
*/
changeTempo(tempo) {
this.beatOfChangeToCurrentTempo = this.currentBeat ();
this.timeOfChangeToCurrentTempo = now();
// this.beatOfChangeToCurrentTempo = beatOfChangeToCurrentTempo + timeToBeats(timeSinceTempoChange())
console.log("TEMPO CHANGE! time: " + this.timeOfChangeToCurrentTempo + "; beat: " + this.beatOfChangeToCurrentTempo);
this.currentTempo = tempo;
}
/**
* Returns the action function of a specific player in this MusicalEnvironment
* @param {string} player - Player name.
*/
getAction (player) {
// console.log("running action for player " + player + " at beat " + this.currentBeat())
return this.actions[(this.players[player].action)];
}
/**
* Returns the IOI function of a specific player in this MusicalEnvironment.
* @param {string} player - Player name.
*/
getIOIFunc (player) {
return this.IOIs[(this.players[player].IOIFunc)];
}
/**
* Schedules events for the a specific player in this MusicalEnvironment.
* @param {string} player - Player name.
*/
scheduleEvents (player) {
//console.log("scheduling " + player);
let ioiFunc = this.getIOIFunc (player);
let onsets = getNextOnsets3(ioiFunc,player, this.currentBeat(), this.currentBeat() + timeToBeats(this.currentTempo,this.lookahead),[]);
let onsetsAfterLastScheduled = onsets.filter(x => x > this.players[player].lastScheduledTime);
if (player.verbose == true) {
console.log(" -------------------------------------------------------------------------- " );
console.log("current beat: " + this.currentBeat());
console.log("onsets after last scheduled: " + onsetsAfterLastScheduled)};
if (onsetsAfterLastScheduled[0] !== undefined) {
this.players[player].lastScheduledTime = R.last(onsetsAfterLastScheduled);
// run the masking here
//let currentMaskMap = this.maskMaps[this.players[player].maskMap];
//let unmaskedOnsets = onsetsAfterLastScheduled.filter(t => (mask(player,currentMaskMap,(t),1)) != true);
let unmaskedOnsets = onsetsAfterLastScheduled;
let times = unmaskedOnsets.map(x => beatsToTime(this.currentTempo, x - (this.currentBeat())));
if (player.verbose == true) { console.log("these are the times for events of player " + player + ": " + times)};
//if (player == 'kick') {console.log(unmaskedOnsets)}
times.forEach(
(t,i) => {
setTimeout(x => (this.getAction(player))(player,unmaskedOnsets[i]),
Math.max(1000 * (t - now()),0))
}
);
};
if (player.verbose == true) { console.log("last scheduled time: " + this.lastScheduledTime)}
}
/**
* Starts the scheduler for the MusicalEnvironment.
*/
startScheduler () {
this.timeOfChangeToCurrentTempo = now();
this.beatOfChangeToCurrentTempo = 0;
this.scheduler.start()
}
/**
* Stops the scheduler for the MusicalEnvironment.
*/
stopScheduler () {
this.timeOfChangeToCurrentTempo = undefined;
this.beatOfChangeToCurrentTempo = undefined;
//this.lastScheduledTime = 0;
this.scheduler.stop()
}
/**
* MusicalEnvironment starts playing a specific player.
* @param {string} player - Player name.
*/
play (player) {
if (this.players[player].status == "playing")
{console.log("Player " + this.players[player].name + " is already playing!")}
else {
this.scheduledPlayers = this.scheduledPlayers.concat(player);
this.players[player].status = "playing";
this.players[player].startTime = now();
}
}
/**
* MusicalEnvironment stops a a specific player from playing.
* @param {string} player - Player name.
*/
stop (player) {
if (this.players[player].status == "stopped")
{console.log("Player " + this.players[player].name + " is not playing!")}
else {
// this.scheduledPlayers = A.removeItem(this.scheduledPlayers,player)
this.scheduledPlayers = A.removeAllInstance(this.scheduledPlayers,player)
this.players[player].status = "stopped";
this.players[player].startTime = undefined;
this.players[player].lastScheduledTime = 0;
}
}
/**
* Returns an array of all the player names.
*/
allPlayers () {
return Object.keys(this.players)
}
/** Returns an array full of arrays. Each sub array contains the player name and their status. All the player names and their status also gets logged into the console.
*/
allPlayerStatus () {
Object.keys(this.players).forEach(x => console.log(x,this.players[x].status))
return Object.keys(this.players).map(x => [x,this.players[x].status])
}
/**
* Returns an array of all the names of players that are currently playing.
*/
playingPlayers () {
let ps = this.allPlayerStatus ();
let withStatus = ps.filter(p => p[1] == 'playing')
return withStatus.map(p => p[0])
}
/**
* Starts playing all the player names in the array.
* @param {array} ps - An array of player names.
*/
playN (ps) {
ps.forEach(p => this.play(p))
}
/**
* Stops playing all the player names in the array.
* @param {array} ps - An array of player names.
*/
stopN (ps) {
ps.forEach(p => this.stop(p))
}
/**
* All players start playing.
*/
playAll () {
this.allPlayers().forEach(p => this.play(p))
}
/**
* All players stop playing.
*/
stopAll () {
this.allPlayers().forEach(p => this.stop(p))
}
/**
* Stops playing all the player names in the array after checking if the players exist inside the MusicalEnvironment.
* @param {array} ps - An array of player names.
*/
solo (ps) {
this.stopN(this.allPlayers().filter(p => !A.matchesOneOf(ps,p)))
}
/**
* Toggles the state of a specific player. If that player is playing it will be stopped. If that player is stopped, it will start playing.
* @param {string} p - Player name.
*/
togglePlayer (p) {
if (this.players[p].status == 'playing') {this.stop(p)} else {this.play(p)}
}
}
/**
* Sets up the scheduler for the MusicalEnvironment.
* @param musicalEnv - Variable name of MusicalEnvironment class.
*/
function setupScheduler (musicalEnv) {
musicalEnv.scheduler.add([
{
id: 'schedulePlayers', // unique ID of the task
//tickInterval: musicalEnv.lookahead * 1000/20,
tickInterval: 1,
totalRuns: 0, // (set to 0 for unlimited times)
callback(task) {
// code to be executed on each run
//console.log("scheduled players " + musicalEnv.scheduledPlayers);
musicalEnv.scheduledPlayers.map(p => musicalEnv.scheduleEvents(p))
}
}
]);
}
/** Class representing a Player. */
class Player {
/**
* Creates the player.
* @param {string} name - Name of the player.
*/
constructor(name) {
this.name = name;
this.status = "stopped";
this.verbose = false;
this.IOIFunc = "default";
this.rhythmMap = "default";
this.maskMap = "default";
this.density = 1;
this.densityGraph = "defaultTechno";
this.action = "default";
this.velocityMap = "default";
this.samplePattern = undefined;
this.cut = 0;
this.counter = 0;
this.interrupt = "default";
this.startTime = undefined;
this.lastScheduledTime = 0;
}
}
/**
* Converts beats to time.
* @param {number} tempo - Current tempo. The current tempo of the MusicalEnvironment can be found out by running e.currentTempo
* @param {number} beats - The beat to convert.
* @example console.log(beatsToTime(100, 10)) //6
*/
function beatsToTime (tempo, beats) {
let beatsPerSecond = (tempo/60);
return beats/beatsPerSecond
}
/**
* Converts time to beats.
* @param {number} tempo - Current tempo. The current tempo of the MusicalEnvironment can be found out by running e.currentTempo
* @param {number} time - The time to convert.
* @example console.log(timeToBeats(80, 120)) //160
*/
function timeToBeats (tempo, time) {
let beatsPerSecond = (tempo/60);
return time * beatsPerSecond
}
//--------------------------------------------------------------------------
/**
* Sets up Player for superDirt
* @param env - MusicalEnvironment
* @param {string} playerName - Name of a player in the MusicalEnvironment.
*/
function setupSuperDirtPlayer (env, playerName) {
env.players[playerName] = new Player(playerName);
env.players[playerName].maskMap = 'default'
env.players[playerName].samplePattern = playerName;
env.players[playerName].action = "superDirt";
return playerName
}
/**
* Sets up a simple sample pattern.
* @param env - MusicalEnvironment
* @param {string} playerName - Name of a player in the MusicalEnvironment.
* @param {string} sampleName - Name of the sample.
* @param {number} sampleIndex - Number of the sample.
*/
function simpleSamplePattern (env, playerName, sampleName, sampleIndex) {
addSamplePattern (e, playerName, new QuantizedMap(4,[0],[{name: sampleName, index: sampleIndex}]));
return playerName
}
/** Class representing a rhythm patter. */
class RhythmPattern {
/**
* Creates the rhythmPattern.
* @param {string} n - Name of the rhythmPattern.
* @param {number} l - Length of the the rhythmPattern.
* @param {array} i - A number array representing IOIs.
* @param {array} b - An array filled with the booleans "true" and "false".
*/
constructor (n,l,i,b) {
this.patternName = n;
this.patternLength = l;
this.IOIs = i;
this.bools = b;
return this
}
/**
* Adds this RhythmPattern to a player.
* @param env - MusicalEnvironment.
* @param {string} playerName - Name of the player to add to.
*/
addToPlayer (env, playerName) {
this.playerName = playerName
simpleRhythm (env, this.patternName, A.loopTo (this.patternLength,this.IOIs))
env.players[this.playerName].rhythmMap = this.patternName;
let mask = A.flipBooleans(this.bools);
env.players[this.playerName].maskMap = this.patternName;
env.maskMaps[this.patternName] = new QuantizedMap(this.patternLength,[0].concat(A.runningSum(0,this.IOIs)),mask)
}
/**
* Adds to MusicalEnvironment but does not add to a player.
* @param env - MusicalEnvironment.
* @param {string} playerName - Name of the player to add to.
*/
add (env, playerName) {
this.playerName = playerName
simpleRhythm (env, this.patternName, A.loopTo (this.patternLength,this.IOIs))
let mask = A.flipBooleans(this.bools);
env.maskMaps[this.patternName] = new QuantizedMap(this.patternLength,[0].concat(A.runningSum(0,this.IOIs)),mask)
}
}
/**
* Creates a RhythmPattern
* @param {object} argObj
* @example createRhythmPattern({patternName: 'examplePattern', patternLength: 10, IOIs: [0, 1, 2, 3, 4], bools: [true, true, true, true]})
*/
function createRhythmPattern (argObj) {
let pattern = new RhythmPattern (
argObj.patternName,
argObj.patternLength,
argObj.IOIs,
argObj.bools)
return pattern
}
function simpleRhythmPattern (env, rhythmPatternArgObj) {
let pattern = createRhythmPattern (rhythmPatternArgObj);
pattern.add(env)