/ concept-collection / timeseries-compressibility
Sign in
concept-collection / timeseries-compressibility
timeseries-compressibility / src / components / Controls.tsx
143 lines · 4.0 KBBlameHistoryRaw
1import { FAMILY_LABELS, type FilterFamily, type FilterSpec } from '../model/filters'
3export interface ControlsProps {
4 sigma: number
5 setSigma: (v: number) => void
6 sampleRateHz: number
7 setSampleRateHz: (v: number) => void
8 spec: FilterSpec
9 setSpec: (v: FilterSpec) => void
10 dither: boolean
11 setDither: (v: boolean) => void
14function formatHz(hz: number): string {
15 return hz >= 1000 ? `${(hz / 1000).toFixed(hz >= 10000 ? 0 : 1)} kHz` : `${Math.round(hz)} Hz`
18/** A slider that is linear in log10 of its value. */
19function LogSlider(props: {
20 label: string
21 value: number
22 min: number
23 max: number
24 display: string
25 onChange: (v: number) => void
26}) {
27 const lo = Math.log10(props.min)
28 const hi = Math.log10(props.max)
29 return (
30 <div className="control">
31 <label>
32 {props.label} <span className="value">{props.display}</span>
33 </label>
34 <input
35 type="range"
36 min={lo}
37 max={hi}
38 step={(hi - lo) / 400}
39 value={Math.log10(props.value)}
40 onChange={e => props.onChange(10 ** Number(e.target.value))}
41 />
42 </div>
43 )
46export default function Controls(p: ControlsProps) {
47 const { spec } = p
48 const nyquist = p.sampleRateHz / 2
49 const sinc = spec.family === 'lowpass' || spec.family === 'bandpass'
50 return (
51 <div className="controls">
52 <LogSlider
53 label="σ (quantization steps)"
54 value={p.sigma}
55 min={0.1}
56 max={100}
57 display={p.sigma.toPrecision(3)}
58 onChange={v => p.setSigma(Number(v.toPrecision(3)))}
59 />
60 <div className="control">
61 <label>filter</label>
62 <select
63 value={spec.family}
64 onChange={e => p.setSpec({ ...spec, family: e.target.value as FilterFamily })}
65 >
66 {(Object.keys(FAMILY_LABELS) as FilterFamily[]).map(f => (
67 <option key={f} value={f}>
68 {FAMILY_LABELS[f]}
69 </option>
70 ))}
71 </select>
72 </div>
73 {spec.family === 'bandpass' && (
74 <LogSlider
75 label="low cutoff"
76 value={spec.lowHz}
77 min={10}
78 max={nyquist * 0.9}
79 display={formatHz(spec.lowHz)}
80 onChange={v => p.setSpec({ ...spec, lowHz: Math.round(v) })}
81 />
82 )}
83 {sinc && (
84 <LogSlider
85 label={spec.family === 'bandpass' ? 'high cutoff' : 'cutoff'}
86 value={spec.highHz}
87 min={20}
88 max={nyquist * 0.98}
89 display={formatHz(spec.highHz)}
90 onChange={v => p.setSpec({ ...spec, highHz: Math.round(v) })}
91 />
92 )}
93 {sinc && (
94 <div className="control">
95 <label>
96 kernel length <span className="value">{spec.taps} taps</span>
97 </label>
98 <input
99 type="range"
100 min={15}
101 max={201}
102 step={2}
103 value={spec.taps}
104 onChange={e => p.setSpec({ ...spec, taps: Number(e.target.value) })}
105 />
106 </div>
107 )}
108 {spec.family === 'movingAverage' && (
109 <div className="control">
110 <label>
111 width <span className="value">{spec.width} samples</span>
112 </label>
113 <input
114 type="range"
115 min={2}
116 max={64}
117 value={spec.width}
118 onChange={e => p.setSpec({ ...spec, width: Number(e.target.value) })}
119 />
120 </div>
121 )}
122 <div className="control">
123 <label>sample rate</label>
124 <select value={p.sampleRateHz} onChange={e => p.setSampleRateHz(Number(e.target.value))}>
125 {[1000, 8000, 16000, 30000, 44100, 96000].map(r => (
126 <option key={r} value={r}>
127 {formatHz(r)}
128 </option>
129 ))}
130 </select>
131 </div>
132 <div className="control control-toggle">
133 <input
134 id="dither"
135 type="checkbox"
136 checked={p.dither}
137 onChange={e => p.setDither(e.target.checked)}
138 />
139 <label htmlFor="dither">dither (uniform ±½ before rounding)</label>
140 </div>
141 </div>
142 )
moveopenescclose