e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 1import { UNIFORM_STD, type PhaseMapSpec } from '../phasemap'
2import { phaseColor } from '../plot'
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 3import type { RenderResult } from '../types'
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 4import { ImagePlot } from './ImagePlot'
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 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
14}
16const SIGMA_LOG_MIN = Math.log10(0.3)
17const SIGMA_LOG_MAX = Math.log10(50)
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 18const TIME_LOG_MIN = 0 // 1 ms
19const TIME_LOG_MAX = Math.log10(3000)
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 20const sig3 = (x: number) => Number(x.toPrecision(3))
21const THRESHOLD_MS = 1
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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 } },
34]
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
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 43 if (kind === 'random') setMap(randomMap(null))
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
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 50 const active = QUICK.find((q) => q.sigmaMs === sigmaMs && sameSpec(q.map, map))
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 51
52 return (
53 <section className="card">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 54 <h2>2. Choose how the phases are scrambled</h2>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 55 <p className="hint">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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.
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 58 </p>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 69 <div className="global-controls">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 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">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 95 bands {sig3(sigmaF)} Hz wide{result ? `, ${result.bands} of them` : ''}
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 96 </span>
97 </label>
98 </div>
99 <div className="global-controls">
100 <label>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 101 <span>Scramble pattern</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 102 <select value={map.kind} onChange={(e) => setKind(e.target.value as PhaseMapSpec['kind'])}>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 106 </select>
107 </label>
108 {map.kind === 'random' && (
109 <>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 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>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 117 <label title="Size of the phase shifts. 0 makes B identical to A.">
118 <span>Amount</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 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>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 122 <button onClick={() => setMap({ ...map, seed: (map.seed % 100000) + 1 })}>New random pattern</button>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 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>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 135 <span>Delay at the highest frequency</span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 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>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 142 {map.kind === 'random' && (
143 <div className="global-controls">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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.">
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 145 <input type="checkbox" checked={map.timeScaleMs === null} onChange={(e) => setMap({ ...map, timeScaleMs: e.target.checked ? null : 200 })} />
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 146 <span>Pattern fixed in time</span>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 147 </label>
148 {map.timeScaleMs !== null && (
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 149 <label title="How slowly the pattern changes. Faster change means larger frequency wobble.">
150 <span>Changes over</span>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 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>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 172 <span className="info">frequencies wobble by about {sig3(fmHz)} Hz</span>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 173 </label>
174 )}
175 </div>
176 )}
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 177
178 {result && result.maxMagDeviationDb > 1 && (
179 <div className="warning">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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.
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 183 </div>
184 )}
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 185
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 186 {result && (
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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" />
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 202 </div>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 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 />
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 220 </div>
221 </div>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 222 </details>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 223 )}
224 </section>
225 )
226}