@@ -10,9 +10,11 @@ import {
1010 useState
1111} from 'react' ;
1212import { useTranslation } from 'react-i18next' ;
13- import { FFTExecutor , Spectrogram as SpectrogramCore } from 'spectrogram-js' ;
13+ import type { ColorMapName , FFTExecutor } from 'spectrogram-js' ;
14+ import { Spectrogram as SpectrogramCore } from 'spectrogram-js' ;
1415
1516import TimeSeriesBuffer from '../../helpers/storage/TimeSeriesBuffer' ;
17+ import { DEFAULT_SPECTROGRAM_COLOR_MAP , SPECTROGRAM_COLOR_MAPS } from './spectrogramColorMaps' ;
1618
1719export interface DequeSpectrogramHandle {
1820 addData ( values : number [ ] , recordTime : number , currentTime : number , sampleRate : number ) : void ;
@@ -27,9 +29,10 @@ interface ISpectrogramDeque {
2729 readonly maxDB : number ;
2830 readonly windowSize : number ;
2931 readonly overlap : number ;
32+ readonly colorMap ?: ColorMapName ;
3033 readonly fftExecutor ?: FFTExecutor ;
3134 readonly renderFPS ?: number ;
32- readonly onSpectrogramUpdate ?: ( minDB : number , maxDB : number ) => void ;
35+ readonly onSpectrogramUpdate ?: ( minDB : number , maxDB : number , colorMap : ColorMapName ) => void ;
3336}
3437
3538export const DequeSpectrogram = memo (
@@ -44,6 +47,7 @@ export const DequeSpectrogram = memo(
4447 freqRange,
4548 windowSize,
4649 overlap,
50+ colorMap = DEFAULT_SPECTROGRAM_COLOR_MAP ,
4751 fftExecutor,
4852 renderFPS = 2 ,
4953 onSpectrogramUpdate
@@ -55,10 +59,15 @@ export const DequeSpectrogram = memo(
5559 const [ showSettings , setShowSettings ] = useState ( false ) ;
5660 const [ minDBState , setMinDBState ] = useState ( minDB ) ;
5761 const [ maxDBState , setMaxDBState ] = useState ( maxDB ) ;
62+ const [ colormap , setColormap ] = useState < ColorMapName > ( colorMap ) ;
5863
59- const [ initialized , setInitialized ] = useState ( false ) ;
64+ const bufferRef = useRef < TimeSeriesBuffer > ( new TimeSeriesBuffer ( duration ) ) ;
65+ const needsUpdateRef = useRef ( true ) ;
66+ const [ initializedSpectrogram , setInitializedSpectrogram ] =
67+ useState < SpectrogramCore | null > ( null ) ;
6068 const spectrogramRef = useRef < SpectrogramCore | null > ( null ) ;
6169 useEffect ( ( ) => {
70+ setInitializedSpectrogram ( null ) ;
6271 const sp = new SpectrogramCore ( {
6372 overlap,
6473 sampleRate,
@@ -70,23 +79,31 @@ export const DequeSpectrogram = memo(
7079 } ) ;
7180 spectrogramRef . current = sp ;
7281 sp . init ( ) . then ( ( ) => {
73- sp . setColormap ( 'jet' ) ;
74- setInitialized ( true ) ;
82+ if ( spectrogramRef . current !== sp ) {
83+ return ;
84+ }
85+ needsUpdateRef . current = true ;
86+ setInitializedSpectrogram ( sp ) ;
7587 } ) ;
7688 return ( ) => {
7789 sp . destroy ( ) ;
78- spectrogramRef . current = null ;
90+ if ( spectrogramRef . current === sp ) {
91+ spectrogramRef . current = null ;
92+ }
7993 } ;
8094 } , [ fftExecutor , maxDB , minDB , overlap , sampleRate , windowSize ] ) ;
8195
82- const bufferRef = useRef < TimeSeriesBuffer > ( new TimeSeriesBuffer ( duration ) ) ;
96+ useEffect ( ( ) => {
97+ setColormap ( colorMap ) ;
98+ } , [ colorMap ] ) ;
99+
83100 useEffect ( ( ) => {
84101 bufferRef . current = new TimeSeriesBuffer ( duration ) ;
102+ needsUpdateRef . current = true ;
85103 } , [ duration ] ) ;
86104
87105 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
88106 const sizeRef = useRef ( { width : 0 , height : 0 } ) ;
89- const needsUpdateRef = useRef ( false ) ;
90107 useEffect ( ( ) => {
91108 const canvas = canvasRef . current ;
92109 if ( ! canvas || ! canvas . parentElement ) {
@@ -99,9 +116,8 @@ export const DequeSpectrogram = memo(
99116 }
100117 const { width, height } = entry . contentRect ;
101118 if ( width !== sizeRef . current . width || height !== sizeRef . current . height ) {
102- sizeRef . current . width = Math . max ( 1 , Math . floor ( width ) ) ;
103- sizeRef . current . height = Math . max ( 1 , Math . floor ( height ) ) ;
104- needsUpdateRef . current = true ;
119+ sizeRef . current . width = Math . max ( 0 , Math . floor ( width ) ) ;
120+ sizeRef . current . height = Math . max ( 0 , Math . floor ( height ) ) ;
105121 }
106122 } ) ;
107123
@@ -120,50 +136,113 @@ export const DequeSpectrogram = memo(
120136 useImperativeHandle ( ref , ( ) => ( { addData } ) , [ addData ] ) ;
121137
122138 const timeRangeRef = useRef < [ number , number ] > ( [ 0 , 0.001 ] ) ;
123- useEffect ( ( ) => {
124- let rafId : number ;
125- let lastRenderTime = 0 ;
126- const frameInterval = 1000 / renderFPS ;
127- const renderLoop = ( now : number ) => {
128- rafId = requestAnimationFrame ( renderLoop ) ;
139+ const needsBitmapFollowupRef = useRef ( false ) ;
140+ const bitmapRenderRef = useRef < number | null > ( null ) ;
141+ const drawSpectrogram = useCallback ( ( ) => {
142+ const canvas = canvasRef . current ;
143+ const sp = spectrogramRef . current ;
144+ if ( ! canvas || ! sp || initializedSpectrogram !== sp ) {
145+ return ;
146+ }
129147
130- if ( now - lastRenderTime < frameInterval ) {
131- return ;
132- }
133- lastRenderTime = now ;
148+ const { width , height } = sizeRef . current ;
149+ if ( ! width || ! height ) {
150+ return ;
151+ }
134152
135- const canvas = canvasRef . current ;
136- const sp = spectrogramRef . current ;
137- if ( ! canvas || ! sp || ! initialized ) {
138- return ;
139- }
140- if ( needsUpdateRef . current ) {
141- needsUpdateRef . current = false ;
142- const bufData = bufferRef . current
143- . getData ( )
144- . filter ( ( v ) : v is [ number , number ] => v [ 1 ] !== null ) ;
145- sp . setData ( bufData ) ;
146- if ( bufData . length > 0 ) {
147- const end = sp . getDuration ( ) ;
148- timeRangeRef . current = [ end - duration , end ] ;
149- }
153+ sp . render ( {
154+ timeRange : timeRangeRef . current ,
155+ canvas,
156+ width,
157+ height,
158+ freqRange
159+ } ) ;
160+ } , [ freqRange , initializedSpectrogram ] ) ;
161+
162+ const renderSpectrogram = useCallback ( ( ) => {
163+ const canvas = canvasRef . current ;
164+ const sp = spectrogramRef . current ;
165+ const { width, height } = sizeRef . current ;
166+ if ( ! canvas || ! sp || initializedSpectrogram !== sp || ! width || ! height ) {
167+ return ;
168+ }
169+
170+ let needsBitmapFollowup = needsBitmapFollowupRef . current ;
171+ needsBitmapFollowupRef . current = false ;
172+
173+ if ( needsUpdateRef . current ) {
174+ needsUpdateRef . current = false ;
175+ const bufData = bufferRef . current
176+ . getData ( )
177+ . filter ( ( v ) : v is [ number , number ] => v [ 1 ] !== null ) ;
178+ sp . setData ( bufData ) ;
179+ if ( bufData . length > 0 ) {
180+ const end = sp . getDuration ( ) ;
181+ timeRangeRef . current = [ end - duration , end ] ;
150182 }
183+ needsBitmapFollowup = true ;
184+ }
151185
152- const { width, height } = sizeRef . current ;
153- if ( ! width || ! height ) {
154- return ;
186+ drawSpectrogram ( ) ;
187+ if ( needsBitmapFollowup ) {
188+ if ( bitmapRenderRef . current !== null ) {
189+ cancelAnimationFrame ( bitmapRenderRef . current ) ;
155190 }
156- sp . render ( {
157- timeRange : timeRangeRef . current ,
158- canvas ,
159- width ,
160- height ,
161- freqRange
191+ bitmapRenderRef . current = requestAnimationFrame ( ( ) => {
192+ drawSpectrogram ( ) ;
193+ bitmapRenderRef . current = requestAnimationFrame ( ( ) => {
194+ bitmapRenderRef . current = null ;
195+ drawSpectrogram ( ) ;
196+ } ) ;
162197 } ) ;
163- } ;
164- rafId = requestAnimationFrame ( renderLoop ) ;
165- return ( ) => cancelAnimationFrame ( rafId ) ;
166- } , [ duration , freqRange , initialized , renderFPS ] ) ;
198+ }
199+ } , [ drawSpectrogram , duration , initializedSpectrogram ] ) ;
200+
201+ const pendingRenderRef = useRef < number | null > ( null ) ;
202+ const requestSpectrogramRender = useCallback ( ( ) => {
203+ if ( pendingRenderRef . current !== null ) {
204+ return ;
205+ }
206+ pendingRenderRef . current = requestAnimationFrame ( ( ) => {
207+ pendingRenderRef . current = null ;
208+ renderSpectrogram ( ) ;
209+ } ) ;
210+ } , [ renderSpectrogram ] ) ;
211+
212+ useEffect (
213+ ( ) => ( ) => {
214+ if ( pendingRenderRef . current !== null ) {
215+ cancelAnimationFrame ( pendingRenderRef . current ) ;
216+ pendingRenderRef . current = null ;
217+ }
218+ if ( bitmapRenderRef . current !== null ) {
219+ cancelAnimationFrame ( bitmapRenderRef . current ) ;
220+ bitmapRenderRef . current = null ;
221+ }
222+ } ,
223+ [ requestSpectrogramRender ]
224+ ) ;
225+
226+ useEffect ( ( ) => {
227+ if ( ! initializedSpectrogram ) {
228+ return ;
229+ }
230+
231+ const frameInterval = 1000 / renderFPS ;
232+ const intervalId = window . setInterval ( requestSpectrogramRender , frameInterval ) ;
233+ requestSpectrogramRender ( ) ;
234+ return ( ) => window . clearInterval ( intervalId ) ;
235+ } , [ initializedSpectrogram , renderFPS , requestSpectrogramRender ] ) ;
236+
237+ useEffect ( ( ) => {
238+ if ( ! initializedSpectrogram || spectrogramRef . current !== initializedSpectrogram ) {
239+ return ;
240+ }
241+
242+ initializedSpectrogram . setColormap ( colormap ) ;
243+ needsBitmapFollowupRef . current = true ;
244+ requestSpectrogramRender ( ) ;
245+ } , [ colormap , initializedSpectrogram , requestSpectrogramRender ] ) ;
167246
168247 const handlePreviewMinDB = useCallback ( ( value : number ) => {
169248 setMinDBState ( value ) ;
@@ -176,9 +255,9 @@ export const DequeSpectrogram = memo(
176255 minDb : value ,
177256 maxDb : Math . max ( value , maxDBState )
178257 } ) ;
179- onSpectrogramUpdate ?.( value , Math . max ( value , maxDBState ) ) ;
258+ onSpectrogramUpdate ?.( value , Math . max ( value , maxDBState ) , colormap ) ;
180259 } ,
181- [ maxDBState , onSpectrogramUpdate , spectrogramRef ]
260+ [ colormap , maxDBState , onSpectrogramUpdate , spectrogramRef ]
182261 ) ;
183262
184263 const handlePreviewMaxDB = useCallback ( ( value : number ) => {
@@ -192,11 +271,19 @@ export const DequeSpectrogram = memo(
192271 minDb : Math . min ( value , minDBState ) ,
193272 maxDb : value
194273 } ) ;
195- onSpectrogramUpdate ?.( Math . min ( value , minDBState ) , value ) ;
274+ onSpectrogramUpdate ?.( Math . min ( value , minDBState ) , value , colormap ) ;
196275 } ,
197- [ minDBState , onSpectrogramUpdate , spectrogramRef ]
276+ [ colormap , minDBState , onSpectrogramUpdate , spectrogramRef ]
198277 ) ;
199278
279+ const handleToggleColormap = useCallback ( ( ) => {
280+ const currentIndex = SPECTROGRAM_COLOR_MAPS . indexOf ( colormap ) ;
281+ const nextIndex = ( currentIndex + 1 ) % SPECTROGRAM_COLOR_MAPS . length ;
282+ const next = SPECTROGRAM_COLOR_MAPS [ nextIndex ] ;
283+ setColormap ( next ) ;
284+ onSpectrogramUpdate ?.( minDBState , maxDBState , next ) ;
285+ } , [ colormap , maxDBState , minDBState , onSpectrogramUpdate ] ) ;
286+
200287 return (
201288 < div className = "relative h-full w-full" >
202289 < canvas ref = { canvasRef } className = "block h-full w-full" />
@@ -214,6 +301,13 @@ export const DequeSpectrogram = memo(
214301 >
215302 < Icon path = { mdiCog } size = { 0.7 } />
216303 </ button >
304+
305+ < button
306+ className = "flex h-6 min-w-12 flex-shrink-0 cursor-pointer items-center justify-center rounded bg-black/50 px-2 text-xs font-semibold text-white opacity-50 transition-all hover:opacity-100"
307+ onClick = { handleToggleColormap }
308+ >
309+ { colormap }
310+ </ button >
217311 </ div >
218312
219313 { showSettings && (
0 commit comments