/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / components / AnalysisPanel.tsx
219 lines · 11.2 KBCodeBlameHistory
2import { phaseColor } from '../plot'
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)
19const TIME_LOG_MAX = Math.log10(3000)
21const THRESHOLD_MS = 1
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 23type Preset = { name: string; prediction: string; sigmaMs: number; map: PhaseMapSpec }
25const randomMap = (timeScaleMs: number | null): PhaseMapSpec => ({ kind: 'random', smoothBands: 8, timeScaleMs, seed: 1, strength: 1 })
28 { name: 'Fixed map, sigma 0.5 ms', prediction: 'Group delay below 0.3 ms, under the reported threshold. Predicted inaudible.', sigmaMs: 0.5, map: randomMap(null) },
29 { name: 'Fixed map, sigma 5 ms', prediction: 'Group delay of a few ms, near the reported threshold. Predicted borderline on transients, inaudible on steady sounds.', sigmaMs: 5, map: randomMap(null) },
30 { name: 'Fixed map, sigma 20 ms', prediction: 'Group delay around 10 ms. Predicted audible on transients as smearing, inaudible on steady sounds.', sigmaMs: 20, map: randomMap(null) },
31 { name: 'Time scale 500 ms', prediction: 'Frequency shifts of about 0.6 Hz, below the pitch threshold. Predicted inaudible or nearly so.', sigmaMs: 5, map: randomMap(500) },
32 { name: 'Time scale 20 ms', prediction: 'Frequency shifts of about 14 Hz. Predicted audible as frequency modulation.', sigmaMs: 5, map: randomMap(20) },
33 { name: 'Delay 1.25 ms (control)', prediction: 'B is A delayed by 1.25 ms with 0.3 dB attenuation. Must be inaudible.', sigmaMs: 5, map: { kind: 'delay', delayMs: 1.25 } },
36const sameSpec = (a: PhaseMapSpec, b: PhaseMapSpec) => JSON.stringify(a) === JSON.stringify(b)
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 38/** Window width, phase map choice and parameters, and the plots that describe the resulting filter. */
39export function AnalysisPanel({ sigmaMs, setSigmaMs, map, setMap, fmax, result }: Props) {
40 const sigmaF = 1000 / (2 * Math.PI * sigmaMs)
41 const setKind = (kind: PhaseMapSpec['kind']) => {
42 if (kind === map.kind) return
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 44 else if (kind === 'delay') setMap({ kind, delayMs: sig3(sigmaMs / 4) })
45 else setMap({ kind, delayAtNyquistMs: sig3(sigmaMs / 4) })
46 }
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 47 const timeVarying = map.kind === 'random' && map.timeScaleMs !== null
48 // a Gaussian-smoothed field with std s and time scale T has a derivative of std s / T
49 const fmHz = map.kind === 'random' && map.timeScaleMs !== null ? (map.strength * UNIFORM_STD) / (2 * Math.PI * (map.timeScaleMs / 1000)) : 0
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 50 const active = PRESETS.find((q) => q.sigmaMs === sigmaMs && sameSpec(q.map, map))
52 return (
53 <section className="card">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 56 Bands are Gaussian with width sigma in time and sigma_f = 1 / (2 pi sigma) in frequency, spaced by sigma_f, normalized to sum to
57 one. A map fixed in time is an all-pass filter whose group delay, minus the slope of the phase across frequency, smears transients;
58 peaks beyond about {THRESHOLD_MS} ms are reported audible on impulsive sounds (Blauert and Laws, 1978). A map varying in time
59 shifts each band's frequency by (d phi / dt) / 2 pi. The map must change by less than about a radian between neighbouring bands,
60 otherwise the bands partly cancel and the magnitude response deviates from 0 dB.
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 63 <span className="quick-label">Presets:</span>
64 {PRESETS.map((q) => (
65 <button key={q.name} className={active?.name === q.name ? 'selected' : ''} title={q.prediction} onClick={() => { setSigmaMs(q.sigmaMs); setMap(q.map) }}>
67 </button>
68 ))}
69 </div>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 70 <div className="expect">{active ? active.prediction : ''}</div>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 73 <label title="Gaussian window width sigma. The band width is 1 / (2 pi sigma) and the achievable group delay scales with sigma.">
74 <span>Window sigma</span>
76 type="range"
77 className="wide-range"
78 min={SIGMA_LOG_MIN}
79 max={SIGMA_LOG_MAX}
80 step={0.01}
81 value={Math.min(SIGMA_LOG_MAX, Math.max(SIGMA_LOG_MIN, Math.log10(sigmaMs)))}
82 onChange={(e) => setSigmaMs(sig3(10 ** Number(e.target.value)))}
83 />
84 <input
85 type="number"
86 className="slider-value"
87 min={0.1}
88 max={200}
89 step="any"
90 value={sigmaMs}
91 onChange={(e) => {
92 const v = Number(e.target.value)
93 if (Number.isFinite(v) && v > 0) setSigmaMs(Math.min(200, Math.max(0.1, v)))
94 }}
95 />
96 <span className="slider-unit">ms</span>
97 <span className="info">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 98 sigma_f = {sig3(sigmaF)} Hz{result ? `, ${result.bands} bands` : ''}
100 </label>
101 </div>
102 <div className="global-controls">
103 <label>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 105 <select value={map.kind} onChange={(e) => setKind(e.target.value as PhaseMapSpec['kind'])}>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 106 <option value="random">Smoothed random</option>
107 <option value="delay">Pure delay (control)</option>
108 <option value="chirp">Chirp (group delay linear in frequency)</option>
110 </label>
111 {map.kind === 'random' && (
112 <>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 113 <label title="Gaussian smoothing width of the map across bands. Below about 6 bands neighbouring bands cancel and the magnitude response develops notches.">
114 <span>Smoothing in frequency</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 115 <input type="range" min={1} max={40} step={1} value={map.smoothBands} onChange={(e) => setMap({ ...map, smoothBands: Number(e.target.value) })} />
116 <span className="slider-unit">
117 {map.smoothBands} bands ({sig3(map.smoothBands * sigmaF)} Hz)
118 </span>
119 </label>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 120 <label title="Scale factor of the map. 1 gives the standard deviation of uniform random phases, pi / sqrt(3).">
121 <span>Strength</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 122 <input type="range" min={0} max={2} step={0.05} value={map.strength} onChange={(e) => setMap({ ...map, strength: Number(e.target.value) })} />
123 <span className="slider-unit">{map.strength.toFixed(2)}</span>
124 </label>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 125 <button onClick={() => setMap({ ...map, seed: (map.seed % 100000) + 1 })}>New random map</button>
126 <span className="info">seed {map.seed}</span>
128 )}
129 {map.kind === 'delay' && (
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 130 <label title="phi(f) = -2 pi f tau. The magnitude is exp(-tau^2 / 2 sigma^2), so tau must stay well below sigma.">
131 <span>Delay tau</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 132 <input type="range" min={-20} max={20} step={0.05} value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) })} />
133 <input type="number" className="slider-value" step="any" value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) || 0 })} />
134 <span className="slider-unit">ms</span>
135 </label>
136 )}
137 {map.kind === 'chirp' && (
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 138 <label title="Group delay tau(f) = tau_N f / f_Nyquist, so phi(f) = -pi tau_N f^2 / f_Nyquist.">
139 <span>Group delay at Nyquist</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 140 <input type="range" min={-20} max={20} step={0.05} value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) })} />
141 <input type="number" className="slider-value" step="any" value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) || 0 })} />
142 <span className="slider-unit">ms</span>
143 </label>
144 )}
145 </div>
147 <div className="global-controls">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 148 <label title="A fixed map is a linear time-invariant all-pass filter. A time-varying map is applied by subband synthesis.">
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 149 <input type="checkbox" checked={map.timeScaleMs === null} onChange={(e) => setMap({ ...map, timeScaleMs: e.target.checked ? null : 200 })} />
152 {map.timeScaleMs !== null && (
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 153 <label title="Gaussian smoothing width of the map in time. The typical frequency shift is strength * pi / sqrt(3) / (2 pi T).">
154 <span>Time scale T</span>
156 type="range"
157 className="wide-range"
158 min={TIME_LOG_MIN}
159 max={TIME_LOG_MAX}
160 step={0.01}
161 value={Math.min(TIME_LOG_MAX, Math.max(TIME_LOG_MIN, Math.log10(map.timeScaleMs)))}
162 onChange={(e) => setMap({ ...map, timeScaleMs: sig3(10 ** Number(e.target.value)) })}
163 />
164 <input
165 type="number"
166 className="slider-value"
167 min={0.1}
168 step="any"
169 value={map.timeScaleMs}
170 onChange={(e) => {
171 const v = Number(e.target.value)
172 if (Number.isFinite(v) && v > 0) setMap({ ...map, timeScaleMs: v })
173 }}
174 />
175 <span className="slider-unit">ms</span>
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 176 <span className="info">typical frequency shift {sig3(fmHz)} Hz</span>
178 )}
179 </div>
180 )}
182 {result && result.maxMagDeviationDb > 1 && (
183 <div className="warning">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 184 The magnitude response deviates by up to {result.maxMagDeviationDb.toFixed(1)} dB, so B is not a pure phase change of A. The map
185 changes too quickly between neighbouring bands for this window: increase sigma, increase the smoothing, or reduce the delay.
187 )}
191 <div>
192 <div className="plot-label">
193 phi(f, t){result.timeVarying ? '' : ', fixed in time'} <span className="phase-legend" /> -180° to 180°
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 195 <ImagePlot image={result.phaseField} color={phaseColor} label="" labelColor="#000" />
196 </div>
197 <div>
198 <div className="plot-label">Magnitude response |G(f)|{timeVarying ? ' at mid-clip' : ''}</div>
199 <LinePlot series={[{ x: result.magDb.f, y: result.magDb.y, color: '#1f5fa8' }]} xRange={[0, fmax]} yRange={[-30, 3]} xLabel="frequency (Hz)" yLabel="dB" zeroLine />
200 </div>
201 <div>
202 <div className="plot-label">
203 Group delay{timeVarying ? ' at mid-clip' : ''}, largest {result.maxGroupDelayMs.toFixed(2)} ms
206 series={[{ x: result.groupDelayMs.f, y: result.groupDelayMs.y, color: '#1f5fa8' }]}
207 xRange={[0, fmax]}
208 symmetric
209 xLabel="frequency (Hz)"
210 yLabel="ms"
211 band={{ y0: -THRESHOLD_MS, y1: THRESHOLD_MS, color: 'rgba(46, 160, 67, 0.15)', label: 'approximate audibility threshold' }}
212 zeroLine
213 />
217 </section>
218 )
↑↓ move↵openescclose