src/CurveRenderer.ts
+115
-0
diff --git a/src/CurveRenderer.ts b/src/CurveRenderer.ts
new file mode 100644
index 0000000..38ced93
@@ -0,0 +1,115 @@
import {
Curve,
DoubleSide,
Group,
LineCurve3,
MathUtils,
Mesh,
MeshPhongMaterial,
TubeGeometry,
Vector3,
Vector4,
} from "three";
import { NURBSCurve } from "three/examples/jsm/curves/NURBSCurve.js";
export class CurveRenderer {
public static audioAnalyser: AnalyserNode | null;
private static sampleRate = 50;
private static curveCount = 10;
private static nurbsDegree = 3;
private static width = 10;
private static thickness = 0.05;
private static segments = 8;
private static spacing = 1;
private static material = new MeshPhongMaterial({ color: 0xbada55, side: DoubleSide });
private readonly group: Group;
private readonly curves: Array<Mesh>;
private previousSampleTime: number | null = null;
public constructor(group: Group) {
this.group = group;
this.curves = CurveRenderer.createStraightLines();
for (const curve of this.curves) {
this.group.add(curve);
}
}
private static createStraightLines(): Array<Mesh> {
const lines = new Array<Mesh>();
const geometry = new TubeGeometry(
new LineCurve3(new Vector3(-this.width / 2, 0, 0), new Vector3(this.width / 2, 0, 0)),
2,
this.thickness,
this.segments,
);
for (let i = 0; i < this.curveCount; i++) {
const mesh = new Mesh(geometry, this.material);
mesh.position.set(0, 0, (this.curveCount / 2) * this.spacing - i * this.spacing);
lines.push(mesh);
}
return lines;
}
public update(): void {
const heights = this.calculateHeights();
if (heights == null) {
return;
}
this.group.remove(this.curves.pop()!);
for (let i = 0; i < this.curves.length; i++) {
this.curves[i].position.setComponent(2, this.curves[i].position.z - CurveRenderer.spacing);
}
const geometry = new TubeGeometry(
this.convertToCurve(heights),
heights.length * 2,
CurveRenderer.thickness,
CurveRenderer.segments,
);
const curve = new Mesh(geometry, CurveRenderer.material);
curve.position.set(0, 0, (CurveRenderer.curveCount / 2) * CurveRenderer.spacing);
this.group.add(curve);
this.curves.unshift(curve);
}
private calculateHeights(): Float32Array | null {
if (CurveRenderer.audioAnalyser == null) {
return null;
}
if (
this.previousSampleTime != null &&
this.previousSampleTime + 1 / CurveRenderer.sampleRate > CurveRenderer.audioAnalyser.context.currentTime
) {
return null;
}
this.previousSampleTime = CurveRenderer.audioAnalyser.context.currentTime;
const frequencyBuffer = new Uint8Array(CurveRenderer.audioAnalyser.frequencyBinCount);
CurveRenderer.audioAnalyser.getByteFrequencyData(frequencyBuffer);
const heights = new Float32Array(frequencyBuffer.length);
for (let i = 0; i < frequencyBuffer.length; i++) {
heights[i] =
(frequencyBuffer[i] - CurveRenderer.audioAnalyser.minDecibels) /
(CurveRenderer.audioAnalyser.maxDecibels - CurveRenderer.audioAnalyser.minDecibels);
}
return heights;
}
private convertToCurve(heights: Float32Array): Curve<Vector3> {
const knots = new Array<number>();
const controlPoints = new Array<Vector4>();
for (let i = 0; i < CurveRenderer.nurbsDegree; i++) {
knots.push(0);
}
for (let i = 0; i < heights.length; i++) {
controlPoints.push(new Vector4((i / heights.length - 0.5) * CurveRenderer.width, heights[i] - 1, 0, 1));
knots.push(MathUtils.clamp((i + 1) / (heights.length - CurveRenderer.nurbsDegree), 0, 1));
}
return new NURBSCurve(CurveRenderer.nurbsDegree, knots, controlPoints);
}
}
src/audioSelector.ts
+10
-18
diff --git a/src/audioSelector.ts b/src/audioSelector.ts
index 70b07bb..ac39135 100644
@@ -1,3 +1,5 @@
import { CurveRenderer } from "./CurveRenderer";
const input = document.querySelector("#input input") as HTMLInputElement;
input?.addEventListener("input", async () => {
if (input.files?.length !== 1) {
@@ -10,28 +12,18 @@ input?.addEventListener("input", async () => {
}
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(await file?.arrayBuffer());
const offlineAudioContext = new OfflineAudioContext({
length: audioBuffer.length,
sampleRate: audioBuffer.sampleRate,
numberOfChannels: audioBuffer.numberOfChannels,
});
const source = offlineAudioContext.createBufferSource();
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.start();
source.loop = true;
source.connect(audioContext.destination);
const analyserNode = offlineAudioContext.createAnalyser();
const analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 128;
source.connect(analyserNode);
offlineAudioContext.suspend(1).then(() => {
const frequencyBuffer = new Uint8Array(analyserNode.frequencyBinCount);
analyserNode.getByteFrequencyData(frequencyBuffer);
console.log(frequencyBuffer);
(window as any).updateCurve(frequencyBuffer);
return offlineAudioContext.resume();
});
source.connect(analyserNode);
analyserNode.connect(audioContext.destination);
analyserNode.connect(offlineAudioContext.destination);
await offlineAudioContext.startRendering();
CurveRenderer.audioAnalyser = analyserNode;
source.start();
});
src/main.ts
+13
-46
diff --git a/src/main.ts b/src/main.ts
index c4ba7c8..7d572d7 100644
@@ -1,16 +1,16 @@
import "./style.css";
import "./audioSelector";
import * as THREE from "three";
import { WebGLRenderer, PerspectiveCamera, Scene, AmbientLight, DirectionalLight, Group } from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { NURBSCurve } from "three/examples/jsm/curves/NURBSCurve.js";
import { CurveRenderer } from "./CurveRenderer";
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
const renderer = new WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 1, 10);
const camera = new PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 5, 15);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
@@ -22,63 +22,30 @@ window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
});
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff));
const scene = new Scene();
scene.add(new AmbientLight(0xffffff));
scene.add(
(() => {
const lights = new THREE.DirectionalLight(0xffffff, 1);
const lights = new DirectionalLight(0xffffff, 1);
lights.position.set(-1, -3, -2);
return lights;
})(),
);
scene.add(
(() => {
const lights = new THREE.DirectionalLight(0xffffff, 3);
const lights = new DirectionalLight(0xffffff, 3);
lights.position.set(1, 3, 2);
return lights;
})(),
);
const material = new THREE.MeshPhongMaterial({ color: 0xbada55, side: THREE.DoubleSide });
let curve = (() => {
const curve = new NURBSCurve(
3,
[0, 0, 0, 0, 0.5, 1, 1, 1, 1],
[
new THREE.Vector4(-1, -1, 0, 1),
new THREE.Vector4(-1, 1, 0, 1),
new THREE.Vector4(0, -1, 0, 1),
new THREE.Vector4(1, 1, 0, 1),
new THREE.Vector4(1, -1, 0, 1),
],
);
const geometry = new THREE.TubeGeometry(curve, 16, 0.1, 8, false);
return new THREE.Mesh(geometry, material);
})();
scene.add(curve);
(window as any).updateCurve = (heights: Uint8Array) => {
scene.remove(curve);
const degree = 3;
const knots = new Array<number>();
const controlPoints = new Array<THREE.Vector4>();
for (let i = 0; i <= degree; i++) {
knots.push(0);
}
for (let i = 0; i < heights.length; i++) {
controlPoints.push(new THREE.Vector4((i - heights.length / 2) * 0.1, heights[i] / 256, 0, 1));
knots.push(THREE.MathUtils.clamp((i + 1) / (heights.length - degree), 0, 1));
}
const nurbs = new NURBSCurve(degree, knots, controlPoints);
const geometry = new THREE.TubeGeometry(nurbs, heights.length, 0.05, 8, false);
curve = new THREE.Mesh(geometry, material);
scene.add(curve);
};
const curveGroup = new Group();
const curveRenderer = new CurveRenderer(curveGroup);
scene.add(curveGroup);
window.requestAnimationFrame(animate);
function animate() {
controls.update();
curveRenderer.update();
renderer.render(scene, camera);
window.requestAnimationFrame(animate);
}