import { UNIFORM_STD, type PhaseMapSpec } from '../phasemap' import { phaseColor } from '../plot' import type { RenderResult } from '../types' import { ImagePlot } from './ImagePlot' import { LinePlot } from './LinePlot' type Props = { sigmaMs: number setSigmaMs: (v: number) => void map: PhaseMapSpec setMap: (m: PhaseMapSpec) => void fmax: number result: RenderResult | null } const SIGMA_LOG_MIN = Math.log10(0.3) const SIGMA_LOG_MAX = Math.log10(50) const TIME_LOG_MIN = 0 // 1 ms const TIME_LOG_MAX = Math.log10(3000) const sig3 = (x: number) => Number(x.toPrecision(3)) const THRESHOLD_MS = 1 type Preset = { name: string; prediction: string; sigmaMs: number; map: PhaseMapSpec } const randomMap = (timeScaleMs: number | null): PhaseMapSpec => ({ kind: 'random', smoothBands: 8, timeScaleMs, seed: 1, strength: 1 }) const PRESETS: Preset[] = [ { 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) }, { 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) }, { 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) }, { 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) }, { name: 'Time scale 20 ms', prediction: 'Frequency shifts of about 14 Hz. Predicted audible as frequency modulation.', sigmaMs: 5, map: randomMap(20) }, { 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 } }, ] const sameSpec = (a: PhaseMapSpec, b: PhaseMapSpec) => JSON.stringify(a) === JSON.stringify(b) /** Window width, phase map choice and parameters, and the plots that describe the resulting filter. */ export function AnalysisPanel({ sigmaMs, setSigmaMs, map, setMap, fmax, result }: Props) { const sigmaF = 1000 / (2 * Math.PI * sigmaMs) const setKind = (kind: PhaseMapSpec['kind']) => { if (kind === map.kind) return if (kind === 'random') setMap(randomMap(null)) else if (kind === 'delay') setMap({ kind, delayMs: sig3(sigmaMs / 4) }) else setMap({ kind, delayAtNyquistMs: sig3(sigmaMs / 4) }) } const timeVarying = map.kind === 'random' && map.timeScaleMs !== null // a Gaussian-smoothed field with std s and time scale T has a derivative of std s / T const fmHz = map.kind === 'random' && map.timeScaleMs !== null ? (map.strength * UNIFORM_STD) / (2 * Math.PI * (map.timeScaleMs / 1000)) : 0 const active = PRESETS.find((q) => q.sigmaMs === sigmaMs && sameSpec(q.map, map)) return (

2. Window and phase map

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 one. A map fixed in time is an all-pass filter whose group delay, minus the slope of the phase across frequency, smears transients; peaks beyond about {THRESHOLD_MS} ms are reported audible on impulsive sounds (Blauert and Laws, 1978). A map varying in time shifts each band's frequency by (d phi / dt) / 2 pi. The map must change by less than about a radian between neighbouring bands, otherwise the bands partly cancel and the magnitude response deviates from 0 dB.

Presets: {PRESETS.map((q) => ( ))}
{active ? active.prediction : ''}
{map.kind === 'random' && ( <> seed {map.seed} )} {map.kind === 'delay' && ( )} {map.kind === 'chirp' && ( )}
{map.kind === 'random' && (
{map.timeScaleMs !== null && ( )}
)} {result && result.maxMagDeviationDb > 1 && (
The magnitude response deviates by up to {result.maxMagDeviationDb.toFixed(1)} dB, so B is not a pure phase change of A. The map changes too quickly between neighbouring bands for this window: increase sigma, increase the smoothing, or reduce the delay.
)} {result && (
phi(f, t){result.timeVarying ? '' : ', fixed in time'} -180° to 180°
Magnitude response |G(f)|{timeVarying ? ' at mid-clip' : ''}
Group delay{timeVarying ? ' at mid-clip' : ''}, largest {result.maxGroupDelayMs.toFixed(2)} ms
)}
) }