Skip to content

Commit 7b327a8

Browse files
committed
Sonogram
1 parent 1e6a870 commit 7b327a8

5 files changed

Lines changed: 104 additions & 1 deletion

File tree

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"fft.js": "^4.0.4",
1314
"react": "^18.3.1",
1415
"react-dom": "^18.3.1"
1516
},

src/Audiovis.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
& audio,
1212
& canvas {
1313
width: 100%;
14+
max-height: 20em;
1415
}
1516
}

src/Audiovis.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FC, useEffect, useMemo, useRef, useState } from "react";
22
import styles from "./Audiovis.module.css";
33
import { useAudioCtx } from "./AudioCtxCtx";
4+
import FFT from "fft.js";
45

56
export const Audiovis: FC<{ srcObject: Blob }> = ({ srcObject }) => {
67
const url = useMemo(() => URL.createObjectURL(srcObject), [srcObject]);
@@ -11,6 +12,7 @@ export const Audiovis: FC<{ srcObject: Blob }> = ({ srcObject }) => {
1112
<audio src={url} controls />
1213

1314
{buffer && <Waveform audio={buffer} />}
15+
{buffer && <Sonogram audio={buffer} />}
1416
</section>
1517
);
1618
};
@@ -65,3 +67,95 @@ const Waveform: FC<{ audio: AudioBuffer }> = ({ audio }) => {
6567
/>
6668
);
6769
};
70+
71+
const Sonogram: FC<{ audio: AudioBuffer }> = ({ audio }) => {
72+
const canvas = useRef<HTMLCanvasElement>(null);
73+
74+
const fttData = useMemo(() => {
75+
const fftSize = 1024; // Ensure this is a power of two
76+
const fft = new FFT(fftSize);
77+
const data = audio.getChannelData(0);
78+
79+
const chunks = Math.floor(audio.length / fftSize);
80+
const target = new Float32Array(fftSize * chunks);
81+
82+
const truncatedData = new Array(fftSize);
83+
const out = fft.createComplexArray();
84+
85+
let min = Infinity;
86+
let max = -Infinity;
87+
88+
for (let i = 0; i < chunks; i++) {
89+
const offset = i * fftSize;
90+
for (let i = 0; i < truncatedData.length; i++)
91+
truncatedData[i] = data[i + offset];
92+
fft.realTransform(out, truncatedData);
93+
94+
for (let j = 0; j < fftSize; j++) {
95+
target[i * fftSize + j] = out[j];
96+
max = Math.max(max, target[i * fftSize + j]);
97+
min = Math.min(min, target[i * fftSize + j]);
98+
}
99+
}
100+
101+
const image = new ImageData(chunks, fftSize);
102+
103+
for (let i = 0; i < chunks; i++) {
104+
for (let j = 0; j < fftSize; j++) {
105+
const idx = j * chunks + i;
106+
const value = Math.abs(target[i * fftSize + j]);
107+
108+
const scaledValue = value * 0.1;
109+
110+
const clamped = Math.max(Math.min(scaledValue, 1), 0);
111+
112+
const col = hslToRgb(clamped / 2, 0.9, 0.5);
113+
114+
image.data[idx * 4] = col[0];
115+
image.data[idx * 4 + 1] = col[1];
116+
image.data[idx * 4 + 2] = col[2];
117+
image.data[idx * 4 + 3] = clamped * 255;
118+
}
119+
}
120+
121+
return image;
122+
}, [audio]);
123+
124+
useEffect(() => {
125+
const ctx = canvas.current?.getContext("2d");
126+
if (ctx) {
127+
ctx.canvas.width = fttData.width;
128+
ctx.canvas.height = fttData.height;
129+
130+
ctx.putImageData(fttData, 0, 0);
131+
}
132+
}, [fttData]);
133+
134+
return <canvas ref={canvas} className={styles.waveform} />;
135+
};
136+
137+
function hslToRgb(h: number, s: number, l: number) {
138+
let r: number, g: number, b: number;
139+
140+
if (s == 0) {
141+
r = g = b = l; // achromatic
142+
} else {
143+
function hue2rgb(p: number, q: number, t: number) {
144+
if (t < 0) t += 1;
145+
if (t > 1) t -= 1;
146+
if (t < 1 / 6) return p + (q - p) * 6 * t;
147+
if (t < 1 / 2) return q;
148+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
149+
return p;
150+
}
151+
152+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
153+
const p = 2 * l - q;
154+
155+
r = hue2rgb(p, q, h + 1 / 3);
156+
g = hue2rgb(p, q, h);
157+
b = hue2rgb(p, q, h - 1 / 3);
158+
}
159+
160+
return [r * 255, g * 255, b * 255];
161+
}

src/RecordAudio.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const RecordAudio: FC<{ onCreated: (blob: Blob) => void }> = ({
2727
for (const track of stream.getTracks()) {
2828
track.stop();
2929
}
30-
}, 5000); // Stop recording after 5 seconds
30+
}, 8000); // Stop recording after 5 seconds
3131
};
3232

3333
return (

0 commit comments

Comments
 (0)