Skip to content

Commit fb42cc6

Browse files
committed
feat: enhance spectrogram with selectable colormap
1 parent 1d9e7bb commit fb42cc6

5 files changed

Lines changed: 232 additions & 105 deletions

File tree

internal/service/frp_client/config.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,25 @@ func (s *frpClientConfigServerAddrImpl) GetNamespace() string { return ID
6666
func (s *frpClientConfigServerAddrImpl) GetKey() string { return "server_addr" }
6767
func (s *frpClientConfigServerAddrImpl) GetType() action.SettingType { return action.String }
6868
func (s *frpClientConfigServerAddrImpl) IsRequired() bool { return true }
69-
func (s *frpClientConfigServerAddrImpl) GetVersion() int { return 0 }
69+
func (s *frpClientConfigServerAddrImpl) GetVersion() int { return 1 }
7070
func (s *frpClientConfigServerAddrImpl) GetOptions() map[string]any { return nil }
71-
func (s *frpClientConfigServerAddrImpl) GetDefaultValue() any { return "anyshake.ip-ddns.com" }
71+
func (s *frpClientConfigServerAddrImpl) GetDefaultValue() any { return "anyshake.observer" }
7272
func (s *frpClientConfigServerAddrImpl) GetDescription() string {
7373
return "The address of the FRP server to connect to."
7474
}
7575
func (s *frpClientConfigServerAddrImpl) Init(handler *action.Handler) error {
7676
if _, err := handler.SettingsInit(s.GetNamespace(), s.GetKey(), s.GetType(), s.GetVersion(), s.GetDefaultValue()); err != nil {
7777
return fmt.Errorf("failed to set default FRP client server address: %w", err)
7878
}
79+
80+
if currentVal, _, ver, err := handler.SettingsGet(s.GetNamespace(), s.GetKey());
81+
// migrate to new domain
82+
err == nil && ver == 0 && currentVal == "anyshake.ip-ddns.com" {
83+
if err = s.Set(handler, s.GetDefaultValue()); err != nil {
84+
return fmt.Errorf("failed to migrate server address: %w", err)
85+
}
86+
}
87+
7988
return nil
8089
}
8190
func (s *frpClientConfigServerAddrImpl) Set(handler *action.Handler, newVal any) error {

web/src/src/components/chart/DequeSpectrogram.tsx

Lines changed: 147 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
useState
1111
} from 'react';
1212
import { 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

1516
import TimeSeriesBuffer from '../../helpers/storage/TimeSeriesBuffer';
17+
import { DEFAULT_SPECTROGRAM_COLOR_MAP, SPECTROGRAM_COLOR_MAPS } from './spectrogramColorMaps';
1618

1719
export 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

3538
export 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

Comments
 (0)