1import { NOTE_NAMES, PARAM_GROUPS, type ParamSpec, type RecorderParams } from '../params';
3interface Props {
4 params: RecorderParams;
5 onChange: (key: keyof RecorderParams, 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);
16}
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));
32}
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);
39}
41function ParamRow({ spec, params, onChange }: Props & { spec: ParamSpec }) {
42 const v = params[spec.key] as number;
43 if (spec.note) {
44 return (
45 <div className="param-row" title={spec.help ?? ''}>
46 <div className="param-label">
47 <span className="name">{spec.label}</span>
48 </div>
49 <div className="param-controls">
50 <select
51 value={Math.min(NOTE_NAMES.length, Math.max(1, Math.round(v)))}
52 onChange={(e) => onChange(spec.key, Number(e.target.value))}
53 >
54 {NOTE_NAMES.map((name, i) => (
55 <option key={name} value={i + 1}>
56 {name}
57 {i >= 10 ? ' (pinched)' : ''}
58 </option>
59 ))}
60 </select>
61 </div>
62 </div>
63 );
64 }
65 return (
66 <div className="param-row" title={spec.help ?? ''}>
67 <div className="param-label">
68 <span className="name">
69 {spec.label}
70 {spec.unit ? <span style={{ color: 'var(--muted)' }}> ({spec.unit})</span> : null}
71 </span>
72 </div>
73 <div className="param-controls">
74 <input
75 type="range"
76 min={0}
77 max={SLIDER_STEPS}
78 value={sliderFromValue(spec, v)}
79 onChange={(e) => onChange(spec.key, valueFromSlider(spec, Number(e.target.value)))}
80 />
81 <input
82 type="number"
83 value={formatValue(v)}
84 step={spec.step ?? 'any'}
85 onChange={(e) => {
86 const nv = Number(e.target.value);
87 if (Number.isFinite(nv)) onChange(spec.key, spec.integer ? Math.round(nv) : nv);
88 }}
89 />
90 </div>
91 </div>
92 );
93}
95export default function ParamPanel({ params, onChange }: Props) {
96 return (
97 <div>
98 {PARAM_GROUPS.map((g, gi) => (
99 <details key={g.name} className="group" open={gi < 2}>
100 <summary>{g.name}</summary>
101 <div className="group-body">
102 {g.specs
103 .filter(
104 (spec) =>
105 !(
106 (spec.key === 'note2' && params.note_count < 2) ||
107 (spec.key === 'note3' && params.note_count < 3) ||
108 (spec.key === 'gap_ms' && params.note_count < 2)
109 ),
110 )
111 .map((spec) => (
112 <ParamRow key={spec.key} spec={spec} params={params} onChange={onChange} />
113 ))}
114 {g.name === 'Output' && (
115 <>
116 <div className="param-row">
117 <div className="param-label">
118 <span className="name">Normalize peak</span>
119 </div>
120 <div className="param-controls">
121 <input
122 type="checkbox"
123 checked={params.normalize}
124 onChange={(e) => onChange('normalize', e.target.checked)}
125 />
126 <span className="param-help">
127 {params.normalize
128 ? `scaled to ${params.peak_dbfs} dBFS`
129 : `absolute: ${params.full_scale_pa} Pa = 0 dBFS`}
130 </span>
131 </div>
132 </div>
133 <div className="param-row">
134 <div className="param-label">
135 <span className="name">WAV bit depth</span>
136 </div>
137 <div className="param-controls">
138 <select
139 value={params.bit_depth}
140 onChange={(e) => onChange('bit_depth', Number(e.target.value))}
141 >
142 <option value={16}>16-bit</option>
143 <option value={24}>24-bit</option>
144 </select>
145 </div>
146 </div>
147 </>
148 )}
149 </div>
150 </details>
151 ))}
152 </div>
153 );
154}