summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjulian laplace <julescarbon@gmail.com>2023-05-09 16:38:32 +0200
committerjulian laplace <julescarbon@gmail.com>2023-05-09 16:38:32 +0200
commitd4f904da669b003c91394799bc5521ebd745122b (patch)
tree979cac792bd8402e96aa4737ec261bbd0fa98386
parent65b92872357db12d8c485ebd514bfc05881250b8 (diff)
render relabi wave to canvas
-rw-r--r--src/index.jsx49
-rw-r--r--src/lib/startAudioContext.js58
-rw-r--r--src/relabi/canvas.js155
-rw-r--r--src/relabi/index.js55
-rw-r--r--src/ui/App.jsx71
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>;
+}