/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / components / AnalysisPanel.tsx
158 lines · 7.1 KBBlameHistoryRaw
1import type { PhaseMapSpec } from '../phasemap'
2import type { RenderResult } from '../types'
3import { LinePlot } from './LinePlot'
5type Props = {
6 sigmaMs: number
7 setSigmaMs: (v: number) => void
8 map: PhaseMapSpec
9 setMap: (m: PhaseMapSpec) => void
10 fmax: number
11 result: RenderResult | null
14const SIGMA_LOG_MIN = Math.log10(0.3)
15const SIGMA_LOG_MAX = Math.log10(50)
16const sig3 = (x: number) => Number(x.toPrecision(3))
18const MAP_COLOR = '#d9641e'
19const THRESHOLD_MS = 1
21/** Window width, phase map choice and parameters, and the plots that describe the resulting filter. */
22export function AnalysisPanel({ sigmaMs, setSigmaMs, map, setMap, fmax, result }: Props) {
23 const sigmaF = 1000 / (2 * Math.PI * sigmaMs)
24 const setKind = (kind: PhaseMapSpec['kind']) => {
25 if (kind === map.kind) return
26 if (kind === 'random') setMap({ kind, smoothBands: 8, seed: 1, strength: 1 })
27 else if (kind === 'delay') setMap({ kind, delayMs: sig3(sigmaMs / 4) })
28 else setMap({ kind, delayAtNyquistMs: sig3(sigmaMs / 4) })
29 }
31 return (
32 <section className="card">
33 <h2>Filter bank and phase map</h2>
34 <p className="hint">
35 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
36 rotating the phase of each band by phi(f) and adding the bands back together, which leaves every band's envelope unchanged. For a
37 fixed map this is an all-pass filter; its group delay, minus the slope of the phase, is what smears transients. Peaks of group delay
38 beyond about {THRESHOLD_MS} ms are reported to be audible on impulsive sounds. Because the map is sampled at the band centers, it
39 must change by less than about a radian from one band to the next; the magnitude plot shows when it does not. For a pure delay this
40 means delays shorter than sigma, so the achievable group delay grows with the window width.
41 </p>
42 <div className="global-controls">
43 <label>
44 <span>Window sigma</span>
45 <input
46 type="range"
47 className="wide-range"
48 min={SIGMA_LOG_MIN}
49 max={SIGMA_LOG_MAX}
50 step={0.01}
51 value={Math.min(SIGMA_LOG_MAX, Math.max(SIGMA_LOG_MIN, Math.log10(sigmaMs)))}
52 onChange={(e) => setSigmaMs(sig3(10 ** Number(e.target.value)))}
53 />
54 <input
55 type="number"
56 className="slider-value"
57 min={0.1}
58 max={200}
59 step="any"
60 value={sigmaMs}
61 onChange={(e) => {
62 const v = Number(e.target.value)
63 if (Number.isFinite(v) && v > 0) setSigmaMs(Math.min(200, Math.max(0.1, v)))
64 }}
65 />
66 <span className="slider-unit">ms</span>
67 <span className="info">
68 band width {sig3(sigmaF)} Hz{result ? `, ${result.bands} bands` : ''}
69 </span>
70 </label>
71 </div>
72 <div className="global-controls">
73 <label>
74 <span>Phase map</span>
75 <select value={map.kind} onChange={(e) => setKind(e.target.value as PhaseMapSpec['kind'])}>
76 <option value="random">Smoothed random</option>
77 <option value="delay">Pure delay (control)</option>
78 <option value="chirp">Chirp (group delay linear in frequency)</option>
79 </select>
80 </label>
81 {map.kind === 'random' && (
82 <>
83 <label>
84 <span>Smoothing</span>
85 <input type="range" min={1} max={40} step={1} value={map.smoothBands} onChange={(e) => setMap({ ...map, smoothBands: Number(e.target.value) })} />
86 <span className="slider-unit">
87 {map.smoothBands} bands ({sig3(map.smoothBands * sigmaF)} Hz)
88 </span>
89 </label>
90 <label>
91 <span>Strength</span>
92 <input type="range" min={0} max={2} step={0.05} value={map.strength} onChange={(e) => setMap({ ...map, strength: Number(e.target.value) })} />
93 <span className="slider-unit">{map.strength.toFixed(2)}</span>
94 </label>
95 <button onClick={() => setMap({ ...map, seed: (map.seed % 100000) + 1 })}>New random map</button>
96 <span className="info">seed {map.seed}</span>
97 </>
98 )}
99 {map.kind === 'delay' && (
100 <label>
101 <span>Delay</span>
102 <input type="range" min={-20} max={20} step={0.05} value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) })} />
103 <input type="number" className="slider-value" step="any" value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) || 0 })} />
104 <span className="slider-unit">ms</span>
105 </label>
106 )}
107 {map.kind === 'chirp' && (
108 <label>
109 <span>Group delay at Nyquist</span>
110 <input type="range" min={-20} max={20} step={0.05} value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) })} />
111 <input type="number" className="slider-value" step="any" value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) || 0 })} />
112 <span className="slider-unit">ms</span>
113 </label>
114 )}
115 </div>
117 {result && result.maxMagDeviationDb > 1 && (
118 <div className="warning">
119 The magnitude response deviates by up to {result.maxMagDeviationDb.toFixed(1)} dB, so B is not just a phase change of A. The map
120 changes too quickly across neighbouring bands for this window: increase sigma, increase the smoothing, or reduce the delay.
121 </div>
122 )}
123 {result && (
124 <div className="response-plots">
125 <div>
126 <div className="plot-label">Phase map phi(f), wrapped to (-180°, 180°]</div>
127 <LinePlot
128 series={[{ x: result.map.f, y: Array.from(result.map.y, (v) => (v * 180) / Math.PI), color: MAP_COLOR, dots: result.map.f.length <= 400, width: 1 }]}
129 xRange={[0, fmax]}
130 yRange={[-190, 190]}
131 xLabel="frequency (Hz)"
132 yLabel="degrees"
133 zeroLine
134 />
135 </div>
136 <div>
137 <div className="plot-label">Magnitude response of the equivalent filter (should stay near 0 dB)</div>
138 <LinePlot series={[{ x: result.magDb.f, y: result.magDb.y, color: '#1f5fa8' }]} xRange={[0, fmax]} yRange={[-30, 3]} xLabel="frequency (Hz)" yLabel="dB" zeroLine />
139 </div>
140 <div>
141 <div className="plot-label">
142 Group delay of the equivalent filter (largest {result.maxGroupDelayMs.toFixed(2)} ms)
143 </div>
144 <LinePlot
145 series={[{ x: result.groupDelayMs.f, y: result.groupDelayMs.y, color: '#1f5fa8' }]}
146 xRange={[0, fmax]}
147 symmetric
148 xLabel="frequency (Hz)"
149 yLabel="ms"
150 band={{ y0: -THRESHOLD_MS, y1: THRESHOLD_MS, color: 'rgba(46, 160, 67, 0.15)', label: 'approximate audibility threshold' }}
151 zeroLine
152 />
153 </div>
154 </div>
155 )}
156 </section>
157 )
↑↓ move↵openescclose