import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { getSampleRate, playSequence, stopAll } from './audio' import { AnalysisPanel } from './components/AnalysisPanel' import { PlaybackPanel } from './components/PlaybackPanel' import { SourcePanel } from './components/SourcePanel' import { defaultPhaseMap, type PhaseMapSpec } from './phasemap' import { Renderer } from './renderer' import { makePreset, presets } from './sources' import type { RenderResult, Source } from './types' const RENDER_DEBOUNCE_MS = 120 const MAX_COLS = 600 const MAX_ROWS = 256 const CURVE_POINTS = 1024 export default function App() { const fs = useMemo(() => getSampleRate(), []) const fmax = Math.min(10000, fs / 2) const [source, setSource] = useState(() => makePreset(presets[4], fs)) const [sigmaMs, setSigmaMs] = useState(5) const [map, setMap] = useState(defaultPhaseMap) const [result, setResult] = useState(null) const [rendering, setRendering] = useState(false) const [error, setError] = useState(null) const [playing, setPlaying] = useState(null) const renderer = useMemo(() => new Renderer(), []) const resultSource = useRef(null) useEffect(() => () => renderer.terminate(), [renderer]) // Re-render B whenever the source, the window, or the map changes (debounced for slider drags). useEffect(() => { if (!source) return setRendering(true) const timer = setTimeout(() => { renderer .request({ samples: source.samples, fs: source.fs, sigmaMs, map, fmax, maxCols: MAX_COLS, maxRows: MAX_ROWS, curvePoints: CURVE_POINTS }) .then((r) => { if (!r) return // superseded resultSource.current = source setResult(r) setError(null) setRendering(false) }) .catch((err: unknown) => { setError(err instanceof Error ? err.message : String(err)) setRendering(false) }) }, RENDER_DEBOUNCE_MS) return () => clearTimeout(timer) }, [source, sigmaMs, map, fmax, renderer]) const play = useCallback( async (label: string, which: ('A' | 'B')[]) => { const src = resultSource.current if (!src || !result) return const g = result.gain const scaled = (x: Float32Array) => x.map((v) => v * g) const buffers = which.map((w) => scaled(w === 'A' ? src.samples : result.y)) setPlaying(label) try { await playSequence(buffers) } finally { setPlaying(null) } }, [result], ) // Show plots for the source the current result belongs to, even while a new render is pending. const shownSource = resultSource.current return (

Phase Perception 2

The first app asked whether a steady tone still sounds the same when its harmonic phases change. Here the same question is asked of any sound. The sound is split into overlapping frequency bands, each band's phase is rotated by an amount that depends on frequency, and the bands are added back together. Every band keeps its envelope, so the spectrogram at that resolution is unchanged, but the waveform is not. The width of the bands is a free parameter: narrow bands (a long window) allow large group delays and smear transients, wide bands (a short window) do not.

{error &&
Rendering failed: {error}
} {result && shownSource && ( void play(l, w)} stop={stopAll} /> )} {!result && !error &&
Rendering…
}
) }