/ simulated_instruments / bass
Sign in
simulated_instruments / bass
bass / webapp / src / components / ParamPanel.tsx
123 lines · 4.2 KBBlameHistoryRaw
1import { PARAM_GROUPS, type ParamSpec, type BassParams } from '../params';
3interface Props {
4 params: BassParams;
5 onChange: (key: keyof BassParams, value: number | boolean) => void;
6}
8const SLIDER_STEPS = 1000;
10function sliderFromValue(spec: ParamSpec, v: number): number {
11 if (spec.log) {
12 const t = (Math.log(v) - Math.log(spec.min)) / (Math.log(spec.max) - Math.log(spec.min));
13 return Math.round(t * SLIDER_STEPS);
14 }
15 return Math.round(((v - spec.min) / (spec.max - spec.min)) * SLIDER_STEPS);
18function valueFromSlider(spec: ParamSpec, s: number): number {
19 const t = s / SLIDER_STEPS;
20 let v: number;
21 if (spec.log) {
22 v = Math.exp(Math.log(spec.min) + t * (Math.log(spec.max) - Math.log(spec.min)));
23 // keep three significant digits so the displayed number stays readable
24 const mag = 10 ** Math.floor(Math.log10(Math.abs(v)));
25 v = Math.round((v / mag) * 100) / 100 * mag;
26 } else {
27 v = spec.min + t * (spec.max - spec.min);
28 if (spec.step) v = Math.round(v / spec.step) * spec.step;
29 }
30 if (spec.integer) v = Math.round(v);
31 return Math.min(spec.max, Math.max(spec.min, v));
34function formatValue(v: number): string {
35 if (v === 0) return '0';
36 const a = Math.abs(v);
37 if (a >= 1e5 || a < 1e-3) return v.toExponential(2);
38 return String(Math.round(v * 1e6) / 1e6);
41function ParamRow({ spec, params, onChange }: Props & { spec: ParamSpec }) {
42 const v = params[spec.key] as number;
43 return (
44 <div className="param-row" title={spec.help ?? ''}>
45 <div className="param-label">
46 <span className="name">
47 {spec.label}
48 {spec.unit ? <span style={{ color: 'var(--muted)' }}> ({spec.unit})</span> : null}
49 </span>
50 </div>
51 <div className="param-controls">
52 <input
53 type="range"
54 min={0}
55 max={SLIDER_STEPS}
56 value={sliderFromValue(spec, v)}
57 onChange={(e) => onChange(spec.key, valueFromSlider(spec, Number(e.target.value)))}
58 />
59 <input
60 type="number"
61 value={formatValue(v)}
62 step={spec.step ?? 'any'}
63 onChange={(e) => {
64 const nv = Number(e.target.value);
65 if (Number.isFinite(nv)) onChange(spec.key, spec.integer ? Math.round(nv) : nv);
66 }}
67 />
68 </div>
69 </div>
70 );
73export default function ParamPanel({ params, onChange }: Props) {
74 return (
75 <div>
76 {PARAM_GROUPS.map((g, gi) => (
77 <details key={g.name} className="group" open={gi < 2}>
78 <summary>{g.name}</summary>
79 <div className="group-body">
80 {g.specs.map((spec) => (
81 <ParamRow key={spec.key} spec={spec} params={params} onChange={onChange} />
82 ))}
83 {g.name === 'Output' && (
84 <>
85 <div className="param-row">
86 <div className="param-label">
87 <span className="name">Normalize peak</span>
88 </div>
89 <div className="param-controls">
90 <input
91 type="checkbox"
92 checked={params.normalize}
93 onChange={(e) => onChange('normalize', e.target.checked)}
94 />
95 <span className="param-help">
96 {params.normalize
97 ? `scaled to ${params.peak_dbfs} dBFS`
98 : `absolute: ${params.full_scale_pa} Pa = 0 dBFS`}
99 </span>
100 </div>
101 </div>
102 <div className="param-row">
103 <div className="param-label">
104 <span className="name">WAV bit depth</span>
105 </div>
106 <div className="param-controls">
107 <select
108 value={params.bit_depth}
109 onChange={(e) => onChange('bit_depth', Number(e.target.value))}
110 >
111 <option value={16}>16-bit</option>
112 <option value={24}>24-bit</option>
113 </select>
114 </div>
115 </div>
116 </>
117 )}
118 </div>
119 </details>
120 ))}
121 </div>
122 );
↑↓ move↵openescclose