summaryrefslogtreecommitdiff
path: root/src/relabi/index.js
blob: ad5b11d15bdeb53e8a7503650fdfaaa5c3c19130 (plain)
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);
  }
}