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
|
import * as Tone from "tone";
import kalimba from "../lib/kalimba";
import { randrange } from "../lib/util";
const TWO_PI = 2 * Math.PI;
/**
* Wave functions
*/
const WAVE_FUNCTIONS = {
sine: Math.cos,
triangle: (time) =>
(4 / TWO_PI) *
Math.abs(
((((time - TWO_PI / 4) % TWO_PI) + TWO_PI) % TWO_PI) - TWO_PI / 2
) -
1,
square: (time) => (time % TWO_PI < Math.PI ? 1 : -1),
saw: (time) => ((time % TWO_PI) - Math.PI) / Math.PI,
};
class Relabi {
/**
* Initialize relabi generator
*/
constructor() {
this.updateTime = 1.0;
this.steps = 100;
this.waves = [
{ type: "sine", frequency: randrange(0.5, 2) },
{ type: "sine", frequency: randrange(0.5, 2) },
{ type: "sine", frequency: randrange(1, 10) },
{ type: "sine", frequency: randrange(5, 10) },
];
this.bounds = [-0.5, 0.5];
this.frequencies = [220, (220 * 3) / 2, 440, (440 * 3) / 2];
}
/**
* Start the generator
*/
start() {
console.log("Start Relabi");
this.stop();
this.clock = new Tone.Clock((time) => this.step(time), this.updateTime);
this.clock.start();
}
/**
* Stop the generator and reset it
*/
stop() {
if (this.clock) {
this.clock.stop();
this.clock.dispose();
}
}
/**
* Generate relabi events
*/
step(time) {
const waveCount = this.waves.length;
const boundsCount = this.bounds.length;
let previousValue = this.previousValue;
let index;
let step;
let value;
let noteCount = 0;
// Generate several events per second
for (step = 0; step < this.steps; step += 1) {
// Time offset for this event
const offset = time + (step * this.updateTime) / this.steps;
// Initialize value
value = 0;
// Compute the wave functions for this event
for (index = 0; index < waveCount; index += 1) {
const wave = this.waves[index];
value += WAVE_FUNCTIONS[wave.type](offset * wave.frequency);
}
// Scale to [-1, 1]
value /= waveCount / Math.PI;
// Compute whether we crossed a boundary, and which direction
for (index = 0; index < boundsCount; index += 1) {
const bound = this.bounds[index];
if (value < bound && bound < previousValue) {
// Going down
this.trigger(offset, index * 2);
noteCount += 1;
} else if (value > bound && bound > previousValue) {
// Going up
this.trigger(offset, index * 2 + 1);
noteCount += 1;
}
}
// Update the previous value
previousValue = value;
}
// Store the latest value
this.previousValue = value;
console.log(`Tick ${Math.floor(time)}, played ${noteCount} notes`);
}
/**
* Trigger an event
*/
trigger(time, index) {
// console.log("trigger index", index, time);
kalimba.play(this.frequencies[index], time);
}
}
export default Relabi;
|