diff --git a/src/Audiovis.tsx b/src/Audiovis.tsx index f53c5d7..0b1e58f 100644 --- a/src/Audiovis.tsx +++ b/src/Audiovis.tsx @@ -71,65 +71,83 @@ const Waveform: FC<{ audio: AudioBuffer }> = ({ audio }) => { const Sonogram: FC<{ audio: AudioBuffer }> = ({ audio }) => { const canvas = useRef(null); - const fttData = useMemo(() => { - const fftSize = 512; // Ensure this is a power of two - const fft = new FFT(fftSize); - const data = audio.getChannelData(0); + const imageData = useMemo(() => { + const fftSize = 512; + const data = spectrum(audio, fftSize); + const image = spectrumToImage(data, fftSize); - const chunks = Math.floor(audio.length / fftSize); - const target = new Float32Array(fftSize * chunks); - - const sample = new Array(fftSize); - const out = fft.createComplexArray(); - - let max = -Infinity; + return image; + }, [audio]); - for (let i = 0; i < chunks; i++) { - const offset = i * fftSize; - for (let i = 0; i < sample.length; i++) sample[i] = data[i + offset]; - fft.realTransform(out, sample); + useEffect(() => { + const ctx = canvas.current?.getContext("2d"); + if (ctx) { + ctx.canvas.width = imageData.width; + ctx.canvas.height = imageData.height; - for (let j = 0; j < fftSize; j++) { - target[i * fftSize + j] = Math.abs(out[j]); - max = Math.max(max, target[i * fftSize + j]); - } + ctx.putImageData(imageData, 0, 0); } + }, [imageData]); - max = max / 4; + return ; +}; - const image = new ImageData(chunks, fftSize); +// load the spectrum for an entire audio buffer +function spectrum( + audio: AudioBuffer, + fftSize: 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 +): Float32Array { + const fft = new FFT(fftSize); + const data = audio.getChannelData(0); - for (let i = 0; i < chunks; i++) { - for (let j = 0; j < fftSize; j++) { - const idx = j * chunks + i; - const value = target[i * fftSize + j]; + const chunks = Math.floor(audio.length / fftSize); + const target = new Float32Array(fftSize * chunks); - const scaledValue = Math.pow(value / max, 0.3); // Apply a power scale + const sample = new Array(fftSize); + const out = fft.createComplexArray(); - const [r, g, b, a] = valueToColor(scaledValue); + for (let i = 0; i < chunks; i++) { + const offset = i * fftSize; + for (let i = 0; i < sample.length; i++) sample[i] = data[i + offset]; + fft.realTransform(out, sample); - image.data[idx * 4] = r; - image.data[idx * 4 + 1] = g; - image.data[idx * 4 + 2] = b; - image.data[idx * 4 + 3] = a; - } + for (let j = 0; j < fftSize; j++) { + target[i * fftSize + j] = Math.abs(out[j]); } + } - return image; - }, [audio]); + return target; +} - useEffect(() => { - const ctx = canvas.current?.getContext("2d"); - if (ctx) { - ctx.canvas.width = fttData.width; - ctx.canvas.height = fttData.height; +// Render a spectrum as a visible image to be written to canvas +function spectrumToImage( + spectrum: Float32Array, + fftSize: 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 +) { + const chunks = spectrum.length / fftSize; + + const image = new ImageData(chunks, fftSize); + + const max = spectrum.reduce((a, b) => Math.max(a, b), -Infinity); - ctx.putImageData(fttData, 0, 0); + for (let i = 0; i < chunks; i++) { + for (let j = 0; j < fftSize; j++) { + const idx = j * chunks + i; + const value = spectrum[i * fftSize + j]; + + const scaledValue = Math.pow(value / max, 0.3); // Apply a power scale + + const [r, g, b, a] = valueToColor(scaledValue); + + image.data[idx * 4] = r; + image.data[idx * 4 + 1] = g; + image.data[idx * 4 + 2] = b; + image.data[idx * 4 + 3] = a; } - }, [fttData]); + } - return ; -}; + return image; +} function valueToColor(value: number) { const r = value < 0.5 ? 0 : 255 * (value - 0.5) * 2;