/** * Relabi event generator */ import * as Tone from "tone"; import RelabiCanvas from "./canvas"; import * as Instruments from "../lib/instruments"; const TWO_PI = 2 * Math.PI; /** * Wave functions */ const WAVE_SHAPES = { 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 - 1, reverse_saw: (time) => 1 - (time % TWO_PI) / Math.PI, noise: (time) => Math.random() * Math.random() * 2 - 1, }; /** * Relabi generator */ export default class Relabi { /** * Initialize generator */ constructor({ waves, bounds, settings, parent }) { this.updateTime = 1; this.steps = 50; this.waves = waves; this.bounds = bounds; this.settings = settings; this.previousValue = null; this.canvas = new RelabiCanvas({ relabi: this, parent }); } /** * Start the generator */ start() { console.log("Start Relabi"); this.stop(); this.clock = new Tone.Clock((time) => { const values = this.generate(time); const notes = this.play(values); this.canvas.append(time, values, notes); }, this.updateTime); this.clock.start(); } /** * Stop the generator and reset it */ stop() { if (this.clock) { this.clock.stop(); this.clock.dispose(); } } /** * Generate relabi events */ generate(time) { let index; let step; let value; let values = []; let previousWaveValue = this.previousWaveValue || 0; // Weight individual waves rather than simply averaging them let totalWeight = this.waves.reduce((sum, wave) => sum + wave.weight, 0.0); // Overshoot the line slightly each time let stepCount = this.steps; // Generate several events per second for (step = 0; step < stepCount; step += 1) { // Time offset for this event const timeOffset = time + (step * this.updateTime) / this.steps; // Initialize value value = 0; // Compute the wave functions for this event for (const wave of this.waves) { const waveOffset = (wave.offset || 0) + (wave.frequency * this.settings.speed) / this.steps + previousWaveValue * this.settings.chaos; const waveValue = WAVE_SHAPES[wave.shape](waveOffset); value += waveValue * wave.weight; previousWaveValue = waveValue; wave.offset = waveOffset; } // Scale to [-1, 1] value /= totalWeight; previousWaveValue = value; values.push([timeOffset, value]); } this.previousWaveValue = previousWaveValue; return values; } /** * Schedule relabi events */ play(values) { const boundsCount = this.bounds.length; let previousValue = this.previousValue; let index; let step; let noteCount = 0; const notes = []; for (step = 0; step < this.steps; step += 1) { // Get the next value const [time, value] = values[step]; // 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 && previousValue !== null ) { // Going down this.trigger(time, bound.sounds[0]); notes.push([time, bound.level, true]); noteCount += 1; } else if ( value > bound.level && bound.level > previousValue && previousValue !== null ) { // Going up this.trigger(time, bound.sounds[1]); notes.push([time, bound.level, false]); noteCount += 1; } } // Update the previous value previousValue = value; } // Store the latest value this.previousValue = previousValue; // Return the notes return notes; } /** * Trigger an event */ trigger(time, sound) { // console.log("trigger index", index, time); if (sound.instrument in Instruments) { Instruments[sound.instrument].play(time, sound); } else { sound.instrument.play(time, sound); } } }