// The simplest system with the cymbal's kind of nonlinearity: one oscillator // with a hardening (Duffing) spring, u'' = -w0^2 (1 + b u^2) u - 2 s u'. // Struck softly it rings at its linear pitch; struck hard it starts sharp // and glides down as the amplitude decays. The figure integrates it, plots // the waveform and the instantaneous frequency from zero crossings, and can // play the result. import { useMemo, useState } from 'react'; import { useCanvas, drawFrame, PLOT_COLORS } from '../../components/plots/canvasUtils'; import { Figure, FigControls, FigSlider, usePlayer } from '../figparts'; const HEIGHT = 240; const SR = 24000; const DUR = 1.4; const F0 = 380; const BETA = 5.0; const SIGMA = 3.5; function simulate(amp: number) { const n = Math.round(SR * DUR); const y = new Float32Array(n); const w0 = 2 * Math.PI * F0; const dt = 1 / SR; let u = amp; let v = 0; const acc = (uu: number, vv: number) => -w0 * w0 * (1 + BETA * uu * uu) * uu - 2 * SIGMA * vv; for (let k = 0; k < n; k++) { y[k] = u; // RK4 const k1u = v, k1v = acc(u, v); const k2u = v + 0.5 * dt * k1v, k2v = acc(u + 0.5 * dt * k1u, v + 0.5 * dt * k1v); const k3u = v + 0.5 * dt * k2v, k3v = acc(u + 0.5 * dt * k2u, v + 0.5 * dt * k2v); const k4u = v + dt * k3v, k4v = acc(u + dt * k3u, v + dt * k3v); u += (dt / 6) * (k1u + 2 * k2u + 2 * k3u + k4u); v += (dt / 6) * (k1v + 2 * k2v + 2 * k3v + k4v); } // instantaneous frequency from upward zero crossings const tf: number[] = []; const ff: number[] = []; let prevT = -1; for (let k = 1; k < n; k++) { if (y[k - 1] < 0 && y[k] >= 0) { const t = (k - y[k] / (y[k] - y[k - 1])) / SR; if (prevT >= 0) { tf.push((t + prevT) / 2); ff.push(1 / (t - prevT)); } prevT = t; } } return { y, tf, ff }; } export default function PitchGlideFig() { const [amp, setAmp] = useState(0.85); const play = usePlayer(); const sim = useMemo(() => simulate(amp), [amp]); const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 44; const padR = 52; const padT = 10; const padB = 26; const pw = w - padL - padR; const ph = h - padT - padB; const { y, tf, ff } = sim; // waveform, min-max per pixel column ctx.strokeStyle = PLOT_COLORS.trace; ctx.globalAlpha = 0.85; ctx.beginPath(); const n = y.length; for (let px = 0; px < pw; px++) { const i0 = Math.floor((px / pw) * n); const i1 = Math.floor(((px + 1) / pw) * n); let mn = Infinity, mx = -Infinity; for (let i = i0; i < Math.max(i1, i0 + 1); i++) { if (y[i] < mn) mn = y[i]; if (y[i] > mx) mx = y[i]; } const ymid = padT + ph / 2; ctx.moveTo(padL + px, ymid - (mx / 1.05) * (ph / 2)); ctx.lineTo(padL + px, ymid - (mn / 1.05) * (ph / 2)); } ctx.stroke(); ctx.globalAlpha = 1; // frequency track on the right axis, 300..900 Hz const fMin = 300, fMax = 900; const fy = (f: number) => padT + ph - ((f - fMin) / (fMax - fMin)) * ph; ctx.strokeStyle = PLOT_COLORS.grid; ctx.fillStyle = PLOT_COLORS.axis; ctx.font = '10px system-ui'; ctx.textAlign = 'left'; for (let f = fMin; f <= fMax; f += 150) { ctx.beginPath(); ctx.moveTo(padL, fy(f)); ctx.lineTo(w - padR, fy(f)); ctx.stroke(); ctx.fillText(`${f} Hz`, w - padR + 5, fy(f) + 3); } ctx.strokeStyle = PLOT_COLORS.accent; ctx.lineWidth = 2; ctx.beginPath(); for (let i = 0; i < tf.length; i++) { const x = padL + (tf[i] / DUR) * pw; const yy = fy(Math.min(fMax, Math.max(fMin, ff[i]))); if (i === 0) ctx.moveTo(x, yy); else ctx.lineTo(x, yy); } ctx.stroke(); ctx.lineWidth = 1; // linear pitch reference ctx.strokeStyle = 'rgba(232, 163, 61, 0.4)'; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(padL, fy(F0)); ctx.lineTo(w - padR, fy(F0)); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = PLOT_COLORS.accent; ctx.fillText(`linear pitch ${F0} Hz`, padL + 4, fy(F0) - 5); // time axis ctx.fillStyle = PLOT_COLORS.axis; ctx.textAlign = 'center'; for (let t = 0.25; t <= DUR; t += 0.25) { ctx.fillText(`${t.toFixed(2)}`, padL + (t / DUR) * pw, h - 8); } ctx.textAlign = 'left'; ctx.fillText('time (s)', 4, h - 8); ctx.fillStyle = PLOT_COLORS.trace; ctx.fillText('waveform', padL + 4, padT + 12); const fpk = ff.length > 0 ? Math.max(...ff.slice(0, 12)) : F0; ctx.fillStyle = PLOT_COLORS.accent; ctx.fillText(`starts near ${fpk.toFixed(0)} Hz`, padL + 80, padT + 12); }, [sim], HEIGHT, ); return (
A single oscillator with a hardening spring (a Duffing oscillator): its restoring force stiffens with displacement, so its frequency rises with amplitude. Struck hard, it starts sharp and glides down toward its linear pitch as the ring decays, which is the pitch glide heard in a struck cymbal or gong. Each mode of the shell sees a hardening of exactly this kind through the membrane stretching, with the strength growing as the squared displacement measured in plate thicknesses. } > v.toFixed(2)} onChange={setAmp} />
); }