e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 1import { useCallback, useRef } from 'react'
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 2import { useCanvas } from '../hooks/useCanvas'
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 3import { useZoomPan, type View } from '../hooks/useZoomPan'
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 4import { AXIS, FONT, FRAME, GRID, formatTick, niceTicks } from '../plot'
6type Props = {
7 samples: Float32Array
8 fs: number
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 9 /** visible window in seconds; shared between plots so they stay synchronized */
10 view: View
11 onView: (v: View) => void
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 12 yMax: number
13 label: string
14 color: string
15}
17const M = { l: 48, r: 12, t: 6, b: 22 }
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 18const MARGINS = { left: M.l, right: M.r }
19const MIN_SPAN_SAMPLES = 16
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 20
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 21/**
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 22 * One waveform with drag-to-zoom, wheel zoom, and double-click reset. Draws a
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 23 * min/max envelope when there are many samples per pixel, a line otherwise.
24 */
25export function WaveformPlot({ samples, fs, view, onView, yMax, label, color }: Props) {
26 const duration = samples.length / fs
27 const { start: startSec, span: spanSec } = view
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 28 const draw = useCallback(
29 (ctx: CanvasRenderingContext2D, w: number, h: number) => {
30 const pw = w - M.l - M.r
31 const ph = h - M.t - M.b
32 const ymax = yMax > 0 ? yMax : 1
33 const y0 = M.t + ph / 2
34 const toY = (v: number) => y0 - (v / ymax) * (ph / 2)
35 const toX = (t: number) => M.l + ((t - startSec) / spanSec) * pw
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 36 const ms = spanSec < 0.5
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 37
38 ctx.font = FONT
39 ctx.lineWidth = 1
40 ctx.strokeStyle = GRID
41 ctx.fillStyle = AXIS
42 ctx.textAlign = 'center'
43 ctx.textBaseline = 'top'
44 for (const v of niceTicks(startSec, startSec + spanSec, 6)) {
45 const x = Math.round(toX(v)) + 0.5
46 if (x < M.l || x > M.l + pw) continue
47 ctx.beginPath()
48 ctx.moveTo(x, M.t)
49 ctx.lineTo(x, M.t + ph)
50 ctx.stroke()
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 51 ctx.fillText(formatTick(ms ? v * 1000 : v), x, M.t + ph + 4)
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 52 }
53 ctx.textAlign = 'right'
54 ctx.textBaseline = 'middle'
55 ctx.fillText(formatTick(ymax), M.l - 5, toY(ymax))
56 ctx.fillText('0', M.l - 5, y0)
57 ctx.fillText(formatTick(-ymax), M.l - 5, toY(-ymax))
58 ctx.strokeStyle = '#b8bcc2'
59 ctx.beginPath()
60 ctx.moveTo(M.l, Math.round(y0) + 0.5)
61 ctx.lineTo(M.l + pw, Math.round(y0) + 0.5)
62 ctx.stroke()
63 ctx.strokeStyle = FRAME
64 ctx.strokeRect(M.l + 0.5, M.t + 0.5, pw, ph)
65 ctx.fillStyle = AXIS
66 ctx.textAlign = 'right'
67 ctx.textBaseline = 'bottom'
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 68 ctx.fillText(ms ? 'ms' : 's', M.l + pw, h - 1)
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 69
70 const i0 = Math.max(0, Math.floor(startSec * fs))
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 71 const i1 = Math.min(samples.length, Math.ceil((startSec + spanSec) * fs) + 1)
72 const perPx = ((i1 - i0) * 1.0) / pw
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 73 ctx.save()
74 ctx.beginPath()
75 ctx.rect(M.l, M.t, pw, ph)
76 ctx.clip()
77 ctx.strokeStyle = color
78 ctx.fillStyle = color
79 ctx.lineWidth = 1.2
80 ctx.lineJoin = 'round'
81 if (perPx > 2) {
82 // min/max envelope per pixel column
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 83 const sampPerPx = (spanSec * fs) / pw
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 84 for (let px = 0; px < pw; px++) {
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 85 const a = Math.max(0, Math.floor(startSec * fs + px * sampPerPx))
86 const b = Math.min(samples.length, Math.floor(startSec * fs + (px + 1) * sampPerPx))
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 87 if (b <= a) continue
88 let lo = Infinity
89 let hi = -Infinity
90 for (let i = a; i < b; i++) {
91 const v = samples[i]
92 if (v < lo) lo = v
93 if (v > hi) hi = v
94 }
95 const yTop = toY(hi)
96 const yBot = toY(lo)
97 ctx.fillRect(M.l + px, yTop, 1, Math.max(1, yBot - yTop))
98 }
99 } else {
100 ctx.beginPath()
101 for (let i = i0; i < i1; i++) {
102 const px = toX(i / fs)
103 const py = toY(samples[i])
104 if (i === i0) ctx.moveTo(px, py)
105 else ctx.lineTo(px, py)
106 }
107 ctx.stroke()
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 108 if (perPx < 0.25) {
109 // individual samples are visible: mark them
110 for (let i = i0; i < i1; i++) {
111 ctx.beginPath()
112 ctx.arc(toX(i / fs), toY(samples[i]), 2, 0, 2 * Math.PI)
113 ctx.fill()
114 }
115 }
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 116 }
117 ctx.restore()
119 ctx.fillStyle = color
120 ctx.font = 'bold 12px system-ui, sans-serif'
121 ctx.textAlign = 'left'
122 ctx.textBaseline = 'top'
123 ctx.fillText(label, M.l + 6, M.t + 4)
124 },
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 125 [samples, fs, startSec, spanSec, yMax, label, color],
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 126 )
127 const ref = useCanvas(draw)
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 128 const overlay = useRef<HTMLDivElement>(null)
129 useZoomPan(ref, overlay, view, duration, MIN_SPAN_SAMPLES / fs, MARGINS, onView)
130 return (
131 <div className="plot-wrap">
132 <canvas ref={ref} className="plot waveform-plot" />
133 <div ref={overlay} className="select-overlay" style={{ top: M.t, bottom: M.b, display: 'none' }} />
134 </div>
135 )