Commit: b2e5f96
Parent: b620bfb

Render the 1s mark of an audio file

Mårten Åsberg committed on 2025-12-18 at 16:56
index.html +4 -0
diff --git a/index.html b/index.html
index 4f87b66..2ce6700 100644
@@ -6,6 +6,10 @@
<title>lines</title>
</head>
<body>
<label id="input">
<span>Audio file</span>
<input type="file" accept="audio/*" />
</label>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
src/audioSelector.ts +37 -0
diff --git a/src/audioSelector.ts b/src/audioSelector.ts
new file mode 100644
index 0000000..70b07bb
@@ -0,0 +1,37 @@
const input = document.querySelector("#input input") as HTMLInputElement;
input?.addEventListener("input", async () => {
if (input.files?.length !== 1) {
return;
}
const file = input.files.item(0);
if (file == null) {
return;
}
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();
source.buffer = audioBuffer;
source.start();
const analyserNode = offlineAudioContext.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();
});
analyserNode.connect(offlineAudioContext.destination);
await offlineAudioContext.startRendering();
});
src/main.ts +38 -17
diff --git a/src/main.ts b/src/main.ts
index 35daa31..c4ba7c8 100644
@@ -1,4 +1,5 @@
import "./style.css";
import "./audioSelector";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { NURBSCurve } from "three/examples/jsm/curves/NURBSCurve.js";
@@ -37,23 +38,43 @@ scene.add(
return lights;
})(),
);
scene.add(
(() => {
const curve = new NURBSCurve(
3,
[0, 0, 0, 0, 1, 1, 1, 1],
[
new THREE.Vector4(-1, -1, 0, 1),
new THREE.Vector4(-1, 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);
const material = new THREE.MeshPhongMaterial({ color: 0xbada55, side: THREE.DoubleSide });
return new THREE.Mesh(geometry, material);
})(),
);
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);
};
window.requestAnimationFrame(animate);
function animate() {
src/style.css +24 -3
diff --git a/src/style.css b/src/style.css
index be4af90..fe71377 100644
@@ -17,10 +17,8 @@
height: 100vh;
transition: background-color 0.4s ease-in;
}
@media (prefers-color-scheme: light) {
:root {
@media (prefers-color-scheme: light) {
color: #213547;
background-color: #ffffff;
}
@@ -34,4 +32,27 @@ body {
width: 100%;
height: 100%;
margin: 0;
display: grid;
grid-template-areas:
"config"
"display";
grid-template-rows: auto 1fr;
grid-template-columns: 1fr;
#input {
grid-area: config;
border: 1px solid #bada55;
text-align: center;
z-index: 1;
input {
display: none;
}
}
canvas {
grid-area: display;
grid-row-start: config;
z-index: 0;
}
}