11import { FC , useEffect , useMemo , useRef , useState } from "react" ;
22import styles from "./Audiovis.module.css" ;
33import { useAudioCtx } from "./AudioCtxCtx" ;
4+ import FFT from "fft.js" ;
45
56export 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+ }
0 commit comments