/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / components / AnalysisPanel.tsx
226 lines · 11.6 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
23type QuickStart = { name: string; expect: string; sigmaMs: number; map: PhaseMapSpec }
25const randomMap = (timeScaleMs: number | null): PhaseMapSpec => ({ kind: 'random', smoothBands: 8, timeScaleMs, seed: 1, strength: 1 })
27const QUICK: QuickStart[] = [
28 { name: 'Fixed scramble, short window', expect: 'Expected inaudible: transients can only smear by a fraction of a millisecond.', sigmaMs: 0.5, map: randomMap(null) },
29 { name: 'Fixed scramble, medium window', expect: 'Borderline: attacks smear by a few milliseconds, near the reported threshold. Steady parts should sound the same.', sigmaMs: 5, map: randomMap(null) },
30 { name: 'Fixed scramble, long window', expect: 'Expected audible on attacks: they smear over tens of milliseconds. Steady parts should sound the same.', sigmaMs: 20, map: randomMap(null) },
31 { name: 'Slowly changing scramble', expect: 'Expected inaudible or nearly so: each frequency drifts by about 1 Hz.', sigmaMs: 5, map: randomMap(500) },
32 { name: 'Quickly changing scramble', expect: 'Expected audible: each frequency wobbles by tens of Hz, heard as warble or roughness.', sigmaMs: 5, map: randomMap(20) },
33 { name: 'Pure delay (control)', expect: 'B is A delayed by a little over a millisecond. Must be inaudible; a check on the pipeline and the listener.', sigmaMs: 5, map: { kind: 'delay', delayMs: 1.25 } },
36const sameSpec = (a: PhaseMapSpec, b: PhaseMapSpec) => JSON.stringify(a) === JSON.stringify(b)
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
43 if (kind === 'random') setMap(randomMap(null))
44 else if (kind === 'delay') setMap({ kind, delayMs: sig3(sigmaMs / 4) })
45 else setMap({ kind, delayAtNyquistMs: sig3(sigmaMs / 4) })
46 }
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
50 const active = QUICK.find((q) => q.sigmaMs === sigmaMs && sameSpec(q.map, map))
52 return (
53 <section className="card">
54 <h2>2. Choose how the phases are scrambled</h2>
55 <p className="hint">
56 The sound is split into overlapping frequency bands. Each band keeps its loudness over time but its phase is shifted by an amount
57 that depends on the band and, optionally, on time. The bands are then added back together to make B.
58 </p>
59 <div className="quick-start">
60 <span className="quick-label">Quick start:</span>
61 {QUICK.map((q) => (
62 <button key={q.name} className={active?.name === q.name ? 'selected' : ''} title={q.expect} onClick={() => { setSigmaMs(q.sigmaMs); setMap(q.map) }}>
63 {q.name}
64 </button>
65 ))}
66 </div>
67 <div className="expect">{active ? active.expect : 'Custom settings.'}</div>
69 <div className="global-controls">
70 <label title="Width of the time window, and so of each frequency band. Longer windows mean narrower bands and allow attacks to smear more.">
71 <span>Time window</span>
72 <input
73 type="range"
74 className="wide-range"
75 min={SIGMA_LOG_MIN}
76 max={SIGMA_LOG_MAX}
77 step={0.01}
78 value={Math.min(SIGMA_LOG_MAX, Math.max(SIGMA_LOG_MIN, Math.log10(sigmaMs)))}
79 onChange={(e) => setSigmaMs(sig3(10 ** Number(e.target.value)))}
80 />
81 <input
82 type="number"
83 className="slider-value"
84 min={0.1}
85 max={200}
86 step="any"
87 value={sigmaMs}
88 onChange={(e) => {
89 const v = Number(e.target.value)
90 if (Number.isFinite(v) && v > 0) setSigmaMs(Math.min(200, Math.max(0.1, v)))
91 }}
92 />
93 <span className="slider-unit">ms</span>
94 <span className="info">
95 bands {sig3(sigmaF)} Hz wide{result ? `, ${result.bands} of them` : ''}
96 </span>
97 </label>
98 </div>
99 <div className="global-controls">
100 <label>
101 <span>Scramble pattern</span>
102 <select value={map.kind} onChange={(e) => setKind(e.target.value as PhaseMapSpec['kind'])}>
103 <option value="random">Random, smoothed across bands</option>
104 <option value="delay">Pure delay (control: must be inaudible)</option>
105 <option value="chirp">Chirp: delay growing with frequency</option>
106 </select>
107 </label>
108 {map.kind === 'random' && (
109 <>
110 <label title="How many neighbouring bands share a similar phase. Too little smoothing and neighbouring bands cancel each other (see the warning).">
111 <span>Smoothness across bands</span>
112 <input type="range" min={1} max={40} step={1} value={map.smoothBands} onChange={(e) => setMap({ ...map, smoothBands: Number(e.target.value) })} />
113 <span className="slider-unit">
114 {map.smoothBands} bands ({sig3(map.smoothBands * sigmaF)} Hz)
115 </span>
116 </label>
117 <label title="Size of the phase shifts. 0 makes B identical to A.">
118 <span>Amount</span>
119 <input type="range" min={0} max={2} step={0.05} value={map.strength} onChange={(e) => setMap({ ...map, strength: Number(e.target.value) })} />
120 <span className="slider-unit">{map.strength.toFixed(2)}</span>
121 </label>
122 <button onClick={() => setMap({ ...map, seed: (map.seed % 100000) + 1 })}>New random pattern</button>
123 </>
124 )}
125 {map.kind === 'delay' && (
126 <label>
127 <span>Delay</span>
128 <input type="range" min={-20} max={20} step={0.05} value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) })} />
129 <input type="number" className="slider-value" step="any" value={map.delayMs} onChange={(e) => setMap({ ...map, delayMs: Number(e.target.value) || 0 })} />
130 <span className="slider-unit">ms</span>
131 </label>
132 )}
133 {map.kind === 'chirp' && (
134 <label>
135 <span>Delay at the highest frequency</span>
136 <input type="range" min={-20} max={20} step={0.05} value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) })} />
137 <input type="number" className="slider-value" step="any" value={map.delayAtNyquistMs} onChange={(e) => setMap({ ...map, delayAtNyquistMs: Number(e.target.value) || 0 })} />
138 <span className="slider-unit">ms</span>
139 </label>
140 )}
141 </div>
142 {map.kind === 'random' && (
143 <div className="global-controls">
144 <label title="A fixed pattern is an all-pass filter. A changing pattern shifts each band's frequency by the rate of change of its phase.">
145 <input type="checkbox" checked={map.timeScaleMs === null} onChange={(e) => setMap({ ...map, timeScaleMs: e.target.checked ? null : 200 })} />
146 <span>Pattern fixed in time</span>
147 </label>
148 {map.timeScaleMs !== null && (
149 <label title="How slowly the pattern changes. Faster change means larger frequency wobble.">
150 <span>Changes over</span>
151 <input
152 type="range"
153 className="wide-range"
154 min={TIME_LOG_MIN}
155 max={TIME_LOG_MAX}
156 step={0.01}
157 value={Math.min(TIME_LOG_MAX, Math.max(TIME_LOG_MIN, Math.log10(map.timeScaleMs)))}
158 onChange={(e) => setMap({ ...map, timeScaleMs: sig3(10 ** Number(e.target.value)) })}
159 />
160 <input
161 type="number"
162 className="slider-value"
163 min={0.1}
164 step="any"
165 value={map.timeScaleMs}
166 onChange={(e) => {
167 const v = Number(e.target.value)
168 if (Number.isFinite(v) && v > 0) setMap({ ...map, timeScaleMs: v })
169 }}
170 />
171 <span className="slider-unit">ms</span>
172 <span className="info">frequencies wobble by about {sig3(fmHz)} Hz</span>
173 </label>
174 )}
175 </div>
176 )}
178 {result && result.maxMagDeviationDb > 1 && (
179 <div className="warning">
180 The loudness of some frequencies changes by up to {result.maxMagDeviationDb.toFixed(1)} dB, so B is not just a phase change of A.
181 The pattern changes too quickly from one band to the next for this window: lengthen the time window, increase the smoothness, or
182 reduce the delay.
183 </div>
184 )}
186 {result && (
187 <details className="details">
188 <summary>Show the phase pattern and the equivalent filter</summary>
189 <p className="hint">
190 A pattern that is fixed in time is an all-pass filter: it leaves every frequency's loudness alone and only delays each
191 frequency by its <em>group delay</em>, the slope of the phase across frequency. Group delay peaks beyond about {THRESHOLD_MS} ms
192 have been reported to be audible on impulsive sounds, and the achievable group delay grows with the time window. A pattern
193 that changes in time shifts each band's frequency by the rate of change of its phase, which becomes audible on steady sounds
194 once the shift approaches the pitch threshold, roughly 1 Hz for low tones.
195 </p>
196 <div className="response-plots">
197 <div>
198 <div className="plot-label">
199 Phase shift by frequency and time{result.timeVarying ? '' : ' (fixed in time)'} <span className="phase-legend" /> -180° to 180°
200 </div>
201 <ImagePlot image={result.phaseField} color={phaseColor} label="" labelColor="#000" />
202 </div>
203 <div>
204 <div className="plot-label">Loudness change by frequency{timeVarying ? ', at mid-clip' : ''} (should stay near 0 dB)</div>
205 <LinePlot series={[{ x: result.magDb.f, y: result.magDb.y, color: '#1f5fa8' }]} xRange={[0, fmax]} yRange={[-30, 3]} xLabel="frequency (Hz)" yLabel="dB" zeroLine />
206 </div>
207 <div>
208 <div className="plot-label">
209 Group delay by frequency{timeVarying ? ', at mid-clip' : ''} (largest {result.maxGroupDelayMs.toFixed(2)} ms)
210 </div>
211 <LinePlot
212 series={[{ x: result.groupDelayMs.f, y: result.groupDelayMs.y, color: '#1f5fa8' }]}
213 xRange={[0, fmax]}
214 symmetric
215 xLabel="frequency (Hz)"
216 yLabel="ms"
217 band={{ y0: -THRESHOLD_MS, y1: THRESHOLD_MS, color: 'rgba(46, 160, 67, 0.15)', label: 'approximate audibility threshold' }}
218 zeroLine
219 />
220 </div>
221 </div>
222 </details>
223 )}
224 </section>
225 )
↑↓ move↵openescclose