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