/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / components / AnalysisPanel.tsx
194 lines · 9.0 KBBlameHistoryRaw
1import { UNIFORM_STD, type PhaseMapSpec } from '../phasemap'
2import { phaseColor } from '../plot'
3import type { RenderResult } from '../types'
4import { ImagePlot } from './ImagePlot'
5import { LinePlot } from './LinePlot'
7type Props = {
8 sigmaMs: number
9 setSigmaMs: (v: number) => void
10 map: PhaseMapSpec
11 setMap: (m: PhaseMapSpec) => void
12 fmax: number
13 result: RenderResult | null
16const SIGMA_LOG_MIN = Math.log10(0.3)
17const SIGMA_LOG_MAX = Math.log10(50)
18const TIME_LOG_MIN = 0 // 1 ms
19const TIME_LOG_MAX = Math.log10(3000)
20const sig3 = (x: number) => Number(x.toPrecision(3))
21const THRESHOLD_MS = 1
23/** Window width, phase map choice and parameters, and the plots that describe the resulting filter. */
24export function AnalysisPanel({ sigmaMs, setSigmaMs, map, setMap, fmax, result }: Props) {
25 const sigmaF = 1000 / (2 * Math.PI * sigmaMs)
26 const setKind = (kind: PhaseMapSpec['kind']) => {
27 if (kind === map.kind) return
28 if (kind === 'random') setMap({ kind, smoothBands: 8, timeScaleMs: null, seed: 1, strength: 1 })
29 else if (kind === 'delay') setMap({ kind, delayMs: sig3(sigmaMs / 4) })
30 else setMap({ kind, delayAtNyquistMs: sig3(sigmaMs / 4) })
31 }
32 const timeVarying = map.kind === 'random' && map.timeScaleMs !== null
33 // a Gaussian-smoothed field with std s and time scale T has a derivative of std s / T
34 const fmHz = map.kind === 'random' && map.timeScaleMs !== null ? (map.strength * UNIFORM_STD) / (2 * Math.PI * (map.timeScaleMs / 1000)) : 0
36 return (
37 <section className="card">
38 <h2>Filter bank and phase map</h2>
39 <p className="hint">
40 The sound is split into overlapping Gaussian bands of width sigma in time (and 1 / (2 pi sigma) in frequency). Sound B is made by
41 rotating the phase of each band by phi(f, t) and adding the bands back together, which leaves every band's envelope unchanged. A map
42 that is fixed in time is an all-pass filter; its group delay, minus the slope of the phase in frequency, is what smears transients,
43 and peaks beyond about {THRESHOLD_MS} ms are reported to be audible on impulsive sounds. A map that changes in time shifts the
44 frequency of each band by the rate of change of its phase, which is audible on steady sounds once the shift approaches the pitch
45 threshold. Because the map is sampled at the band centers, it must change by less than about a radian from one band to the next; the
46 magnitude plot shows when it does not.
47 </p>
48 <div className="global-controls">
49 <label>
50 <span>Window sigma</span>
51 <input
52 type="range"
53 className="wide-range"
54 min={SIGMA_LOG_MIN}
55 max={SIGMA_LOG_MAX}
56 step={0.01}
57 value={Math.min(SIGMA_LOG_MAX, Math.max(SIGMA_LOG_MIN, Math.log10(sigmaMs)))}
58 onChange={(e) => setSigmaMs(sig3(10 ** Number(e.target.value)))}
59 />
60 <input
61 type="number"
62 className="slider-value"
63 min={0.1}
64 max={200}
65 step="any"
66 value={sigmaMs}
67 onChange={(e) => {
68 const v = Number(e.target.value)
69 if (Number.isFinite(v) && v > 0) setSigmaMs(Math.min(200, Math.max(0.1, v)))
70 }}
71 />
72 <span className="slider-unit">ms</span>
73 <span className="info">
74 band width {sig3(sigmaF)} Hz{result ? `, ${result.bands} bands` : ''}
75 </span>
76 </label>
77 </div>
78 <div className="global-controls">
79 <label>
80 <span>Phase map</span>
81 <select value={map.kind} onChange={(e) => setKind(e.target.value as PhaseMapSpec['kind'])}>
82 <option value="random">Smoothed random</option>
83 <option value="delay">Pure delay (control)</option>
84 <option value="chirp">Chirp (group delay linear in frequency)</option>
85 </select>
86 </label>
87 {map.kind === 'random' && (
88 <>
89 <label>
90 <span>Smoothing in frequency</span>
91 <input type="range" min={1} max={40} step={1} value={map.smoothBands} onChange={(e) => setMap({ ...map, smoothBands: Number(e.target.value) })} />
92 <span className="slider-unit">
93 {map.smoothBands} bands ({sig3(map.smoothBands * sigmaF)} Hz)
94 </span>
95 </label>
96 <label>
97 <span>Strength</span>
98 <input type="range" min={0} max={2} step={0.05} value={map.strength} onChange={(e) => setMap({ ...map, strength: Number(e.target.value) })} />
99 <span className="slider-unit">{map.strength.toFixed(2)}</span>
100 </label>
101 <button onClick={() => setMap({ ...map, seed: (map.seed % 100000) + 1 })}>New random map</button>
102 <span className="info">seed {map.seed}</span>
103 </>
104 )}
105 {map.kind === 'delay' && (
106 <label>
107 <span>Delay</span>
108 <input type="range" min={-20} max={20} step={0.05} value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) })} />
109 <input type="number" className="slider-value" step="any" value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) || 0 })} />
110 <span className="slider-unit">ms</span>
111 </label>
112 )}
113 {map.kind === 'chirp' && (
114 <label>
115 <span>Group delay at Nyquist</span>
116 <input type="range" min={-20} max={20} step={0.05} value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) })} />
117 <input type="number" className="slider-value" step="any" value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) || 0 })} />
118 <span className="slider-unit">ms</span>
119 </label>
120 )}
121 </div>
122 {map.kind === 'random' && (
123 <div className="global-controls">
124 <label>
125 <input type="checkbox" checked={map.timeScaleMs === null} onChange={(e) => setMap({ ...map, timeScaleMs: e.target.checked ? null : 200 })} />
126 <span>Fixed in time</span>
127 </label>
128 {map.timeScaleMs !== null && (
129 <label>
130 <span>Smoothing in time</span>
131 <input
132 type="range"
133 className="wide-range"
134 min={TIME_LOG_MIN}
135 max={TIME_LOG_MAX}
136 step={0.01}
137 value={Math.min(TIME_LOG_MAX, Math.max(TIME_LOG_MIN, Math.log10(map.timeScaleMs)))}
138 onChange={(e) => setMap({ ...map, timeScaleMs: sig3(10 ** Number(e.target.value)) })}
139 />
140 <input
141 type="number"
142 className="slider-value"
143 min={0.1}
144 step="any"
145 value={map.timeScaleMs}
146 onChange={(e) => {
147 const v = Number(e.target.value)
148 if (Number.isFinite(v) && v > 0) setMap({ ...map, timeScaleMs: v })
149 }}
150 />
151 <span className="slider-unit">ms</span>
152 <span className="info">typical frequency shift {sig3(fmHz)} Hz; the window is {sigmaMs} ms</span>
153 </label>
154 )}
155 </div>
156 )}
158 {result && result.maxMagDeviationDb > 1 && (
159 <div className="warning">
160 The magnitude response deviates by up to {result.maxMagDeviationDb.toFixed(1)} dB, so B is not just a phase change of A. The map
161 changes too quickly across neighbouring bands for this window: increase sigma, increase the smoothing, or reduce the delay.
162 </div>
163 )}
164 {result && (
165 <div className="response-plots">
166 <div>
167 <div className="plot-label">
168 Phase map phi(f, t){result.timeVarying ? '' : ', fixed in time'} <span className="phase-legend" /> -180° to 180°
169 </div>
170 <ImagePlot image={result.phaseField} color={phaseColor} label="" labelColor="#000" />
171 </div>
172 <div>
173 <div className="plot-label">Magnitude response of the equivalent filter{timeVarying ? ' at mid-clip' : ''} (should stay near 0 dB)</div>
174 <LinePlot series={[{ x: result.magDb.f, y: result.magDb.y, color: '#1f5fa8' }]} xRange={[0, fmax]} yRange={[-30, 3]} xLabel="frequency (Hz)" yLabel="dB" zeroLine />
175 </div>
176 <div>
177 <div className="plot-label">
178 Group delay{timeVarying ? ' at mid-clip' : ''} (largest {result.maxGroupDelayMs.toFixed(2)} ms)
179 </div>
180 <LinePlot
181 series={[{ x: result.groupDelayMs.f, y: result.groupDelayMs.y, color: '#1f5fa8' }]}
182 xRange={[0, fmax]}
183 symmetric
184 xLabel="frequency (Hz)"
185 yLabel="ms"
186 band={{ y0: -THRESHOLD_MS, y1: THRESHOLD_MS, color: 'rgba(46, 160, 67, 0.15)', label: 'approximate audibility threshold' }}
187 zeroLine
188 />
189 </div>
190 </div>
191 )}
192 </section>
193 )
↑↓ move↵openescclose