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
|
import * as Tone from "tone";
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,
};
/**
* Relabi generator
*/
export default class Relabi {
/**
* Initialize generator
*/
constructor({ waves, bounds }) {
this.updateTime = 0.5;
this.steps = 50;
this.waves = waves || [
{ type: "sine", frequency: randrange(0.5, 1.5) },
{ type: "sine", frequency: randrange(0.75, 2.25) },
{ type: "sine", frequency: randrange(1, 3) },
{ type: "sine", frequency: randrange(2, 4) },
];
this.bounds = bounds;
}
/**
* 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.level && bound.level < previousValue) {
// Going down
this.trigger(offset, bound.sounds[0]);
noteCount += 1;
} else if (value > bound.level && bound.level > previousValue) {
// Going up
this.trigger(offset, bound.sounds[1]);
noteCount += 1;
}
}
// Update the previous value
previousValue = value;
}
// Store the latest value
this.previousValue = value;
}
/**
* Trigger an event
*/
trigger(time, sound) {
// console.log("trigger index", index, time);
sound.instrument.play(time, sound);
}
}
|