diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/index.jsx | 49 | ||||
| -rw-r--r-- | src/lib/startAudioContext.js | 58 | ||||
| -rw-r--r-- | src/relabi/canvas.js | 155 | ||||
| -rw-r--r-- | src/relabi/index.js | 55 | ||||
| -rw-r--r-- | src/ui/App.jsx | 71 |
5 files changed, 307 insertions, 81 deletions
diff --git a/src/index.jsx b/src/index.jsx index 8c07739..b41c7da 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,55 +1,16 @@ import * as React from "react"; import { createRoot } from "react-dom/client"; import { requestAudioContext, randrange } from "./lib/util"; -import Relabi from "./relabi"; -import { Kalimba, Drums } from "./lib/instruments"; + +import App from "./ui/App.jsx"; document.body.style.backgroundColor = "#111"; document.body.style.color = "#fff"; +document.body.style.margin = 0; +document.body.style.padding = 0; requestAudioContext(() => { document.body.innerHTML = '<div id="app"></div>'; - - const relabi = new Relabi({ - waves: [ - { type: "sine", frequency: 0.75 }, - { type: "sine", frequency: 1.0 }, - { type: "sine", frequency: 1.617 }, - { type: "sine", frequency: 3.141 }, - ], - bounds: [ - { - level: -0.5, - sounds: [ - { instrument: Drums, index: 0 }, - { instrument: Drums, index: 1 }, - ], - }, - { - level: 0.5, - sounds: [ - { instrument: Drums, index: 2 }, - { instrument: Drums, index: 3 }, - ], - }, - { - level: -0.25, - sounds: [ - { instrument: Kalimba, frequency: 440 }, - { instrument: Kalimba, frequency: (440 * 3) / 2 }, - ], - }, - { - level: 0.25, - sounds: [ - { instrument: Kalimba, frequency: (440 * 6) / 5 }, - { instrument: Kalimba, frequency: (440 * 6) / 7 }, - ], - }, - ], - }); - relabi.start(); - const root = createRoot(document.getElementById("app")); - root.render(<h1>Relabi generator</h1>); + root.render(<App />); }); diff --git a/src/lib/startAudioContext.js b/src/lib/startAudioContext.js index 5a339a2..54bbe71 100644 --- a/src/lib/startAudioContext.js +++ b/src/lib/startAudioContext.js @@ -5,6 +5,8 @@ * @copyright 2016 Yotam Mann */ +window.__audio_context_started = false; + (function (root, factory) { if (typeof define === "function" && define.amd) { define([], factory); @@ -90,8 +92,9 @@ */ StartAudioContext.isStarted = function () { return ( - StartAudioContext.context !== null && - StartAudioContext.context.state === "running" + (StartAudioContext.context !== null && + StartAudioContext.context.state === "running") || + window.__audio_context_started ); }; @@ -148,31 +151,38 @@ * event has been triggered. */ function onTap() { - //start the audio context with a silent oscillator - if (StartAudioContext.context && !StartAudioContext.isStarted()) { - var osc = StartAudioContext.context.createOscillator(); - var silent = StartAudioContext.context.createGain(); - silent.gain.value = 0; - osc.connect(silent); - silent.connect(StartAudioContext.context.destination); - var now = StartAudioContext.context.currentTime; - osc.start(now); - osc.stop(now + 0.5); - } + try { + //start the audio context with a silent oscillator + if (StartAudioContext.context && !StartAudioContext.isStarted()) { + var osc = StartAudioContext.context.createOscillator(); + var silent = StartAudioContext.context.createGain(); + silent.gain.value = 0; + osc.connect(silent); + silent.connect(StartAudioContext.context.destination); + var now = StartAudioContext.context.currentTime; + osc.start(now); + osc.stop(now + 0.5); + } - //dispose all the tap listeners - if (StartAudioContext._tapListeners) { - for (var i = 0; i < StartAudioContext._tapListeners.length; i++) { - StartAudioContext._tapListeners[i].dispose(); + //dispose all the tap listeners + if (StartAudioContext._tapListeners) { + for (var i = 0; i < StartAudioContext._tapListeners.length; i++) { + StartAudioContext._tapListeners[i].dispose(); + } + StartAudioContext._tapListeners = null; } - StartAudioContext._tapListeners = null; - } - //the onstarted callbacks - if (StartAudioContext._onStarted) { - for (var j = 0; j < StartAudioContext._onStarted.length; j++) { - StartAudioContext._onStarted[j](); + + //the onstarted callbacks + if (StartAudioContext._onStarted) { + for (var j = 0; j < StartAudioContext._onStarted.length; j++) { + StartAudioContext._onStarted[j](); + } + StartAudioContext._onStarted = null; } - StartAudioContext._onStarted = null; + + window.__audio_context_started = true; + } catch (error) { + // } } diff --git a/src/relabi/canvas.js b/src/relabi/canvas.js new file mode 100644 index 0000000..dfbffa0 --- /dev/null +++ b/src/relabi/canvas.js @@ -0,0 +1,155 @@ +/** + * Relabi waveform display + * @module src/relabi/canvas.js; + */ + +export default class RelabiCanvas { + /** + * Initialize relabi wave renderer + */ + constructor({ relabi, parent }) { + this.relabi = relabi; + this.height = 400; + this.lastFrame = 0; + this.lastAppendTime = 0; + this.lastAppendFrame = 0; + + // Speed of the wave in pixels per second + this.speed = 1 / 5; + + // Attach to the DOM + this.parent = parent = document.body; + this.canvas = document.createElement("canvas"); + this.ctx = this.canvas.getContext("2d"); + this.parent.appendChild(this.canvas); + + // Initialize array + this.values = new Array(window.innerWidth).fill(0); + this.append([]); + + // Clear the canvas + this.resize(); + + // Start drawing + this.requestAnimationFrame(0); + } + + /** + * Draw the next frame + */ + requestAnimationFrame(frame) { + requestAnimationFrame((frame) => { + this.requestAnimationFrame(frame); + }); + this.paint(frame); + this.lastFrame = frame; + } + + /** + * Handle window resize + */ + resize() { + this.canvas.width = window.innerWidth; + this.canvas.height = this.height; + } + + /** + * Clear the canvas + */ + clear() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Append values + */ + append(time, values) { + this.lastAppendTime = time; + this.lastAppendFrame = this.lastFrame; + this.values = this.values.concat(values).slice(-this.canvas.width); + } + + /** + * Paint the canvas + */ + paint(frame) { + this.clear(); + this.drawRelabiWave(frame); + this.drawBounds(); + } + + /** + * Draw the bounds + */ + drawBounds() { + const { canvas, ctx } = this; + const { width, height } = canvas; + + // Draw dashed lines for all bounds + for (const bound of this.relabi.bounds) { + const y = getWaveHeight(bound.level, height); + + ctx.beginPath(); + ctx.setLineDash([10, 5]); + ctx.strokeStyle = bound.color || "#888"; + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + } + + /** + * Draw the relabi wave + */ + drawRelabiWave(frame) { + const { canvas, ctx } = this; + const { width, height } = canvas; + + const pointCount = this.values.length; + let index = 0; + + // Start the path + ctx.beginPath(); + ctx.strokeStyle = "white"; + ctx.setLineDash([]); + + // This is the offset in seconds from the last frame we computed + const frameOffset = (frame - this.lastAppendFrame) / 1000; + + // Make a path connecting all values + for (const point of this.values) { + if (!point) { + index += 1; + continue; + } + + const [time, value] = point; + + // This is the offset of this time from current time + // If this is in the future, it will be positive. + // Subtracting the frame offset pulls it negative. + const timeOffset = this.lastAppendTime + frameOffset - time; + + const x = width - timeOffset * this.speed * width - 10; + + if (x < 0) { + continue; + } + + const y = getWaveHeight(value, height); + if (index === 0) { + ctx.moveTo(x, y); + } + ctx.lineTo(x, y); + index += 1; + } + + // Paint the line + ctx.stroke(); + } +} + +/** + * Get the height of the wave at a given value + */ +const getWaveHeight = (value, height) => ((value + 1) / 2) * height; diff --git a/src/relabi/index.js b/src/relabi/index.js index ad5b11d..e392a27 100644 --- a/src/relabi/index.js +++ b/src/relabi/index.js @@ -1,4 +1,9 @@ +/** + * Relabi event generator + */ + import * as Tone from "tone"; +import RelabiCanvas from "./canvas"; const TWO_PI = 2 * Math.PI; @@ -24,8 +29,8 @@ export default class Relabi { /** * Initialize generator */ - constructor({ waves, bounds }) { - this.updateTime = 0.5; + constructor({ waves, bounds, parent }) { + this.updateTime = 1; this.steps = 50; this.waves = waves || [ { type: "sine", frequency: randrange(0.5, 1.5) }, @@ -34,6 +39,8 @@ export default class Relabi { { type: "sine", frequency: randrange(2, 4) }, ]; this.bounds = bounds; + this.previousValue = 0; + this.canvas = new RelabiCanvas({ relabi: this, parent }); } /** @@ -42,7 +49,11 @@ export default class Relabi { start() { console.log("Start Relabi"); this.stop(); - this.clock = new Tone.Clock((time) => this.step(time), this.updateTime); + this.clock = new Tone.Clock((time) => { + const values = this.generate(time); + this.canvas.append(time, values); + this.play(values); + }, this.updateTime); this.clock.start(); } @@ -59,19 +70,17 @@ export default class Relabi { /** * Generate relabi events */ - step(time) { + generate(time) { const waveCount = this.waves.length; - const boundsCount = this.bounds.length; - let previousValue = this.previousValue; let index; let step; let value; - let noteCount = 0; + let values = []; // 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; + const timeOffset = time + (step * this.updateTime) / this.steps; // Initialize value value = 0; @@ -79,22 +88,42 @@ export default class Relabi { // 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); + value += WAVE_FUNCTIONS[wave.type](timeOffset * wave.frequency); } // Scale to [-1, 1] - value /= waveCount / Math.PI; + value /= waveCount; + + values.push([timeOffset, value]); + } + + return values; + } + + /** + * Schedule relabi events + */ + play(values) { + const boundsCount = this.bounds.length; + let previousValue = this.previousValue; + let index; + let step; + let noteCount = 0; + + 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) { // Going down - this.trigger(offset, bound.sounds[0]); + this.trigger(time, bound.sounds[0]); noteCount += 1; } else if (value > bound.level && bound.level > previousValue) { // Going up - this.trigger(offset, bound.sounds[1]); + this.trigger(time, bound.sounds[1]); noteCount += 1; } } @@ -104,7 +133,7 @@ export default class Relabi { } // Store the latest value - this.previousValue = value; + this.previousValue = previousValue; } /** diff --git a/src/ui/App.jsx b/src/ui/App.jsx new file mode 100644 index 0000000..5ee21b7 --- /dev/null +++ b/src/ui/App.jsx @@ -0,0 +1,71 @@ +/** + * Relabi generator UI + * @module src/ui/App.js; + */ + +import * as React from "react"; +import { useState, useEffect } from "react"; +import Relabi from "../relabi"; +import { Kalimba, Drums } from "../lib/instruments"; + +export default function App() { + const [relabi, setRelabi] = useState(); + + /** + * Instantiate the Relabi generator + */ + useEffect(() => { + if (!relabi) { + const relabiGenerator = new Relabi({ + waves: [ + { type: "sine", frequency: 0.75 }, + { type: "sine", frequency: 1.0 }, + { type: "sine", frequency: 1.617 }, + { type: "sine", frequency: 3.141 }, + ], + bounds: [ + { + level: -0.75, + color: "#f00", + sounds: [ + { instrument: Drums, index: 0 }, + { instrument: Drums, index: 1 }, + ], + }, + { + level: 0.75, + color: "#f00", + sounds: [ + { instrument: Drums, index: 2 }, + { instrument: Drums, index: 3 }, + ], + }, + { + level: -0.25, + color: "#00f", + sounds: [ + { instrument: Kalimba, frequency: 440 }, + { instrument: Kalimba, frequency: (440 * 3) / 2 }, + ], + }, + { + level: 0.25, + color: "#00f", + sounds: [ + { instrument: Kalimba, frequency: (440 * 6) / 5 }, + { instrument: Kalimba, frequency: (440 * 6) / 7 }, + ], + }, + ], + }); + relabiGenerator.start(); + setRelabi(relabiGenerator); + } + }, [relabi]); + + /** + * Render + */ + + return <div>Relabi generator</div>; +} |
