Styling
wavesurfer.js draws its bars on a canvas, so colors are options passed in JavaScript, and changing them, for a dark theme for example, means calling setOptions. sillon draws the bars as a stencil over two elements, Waveform.Track and Waveform.Progress; what shows through is their background, set with classes or CSS.
const ws = WaveSurfer.create({ container, url, barWidth: 3, barGap: 2, waveColor: '#d4d0c9', progressColor: '#0f0e0c',})// Colors are JavaScript options, so a theme change sets them again.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { ws.setOptions({ waveColor: e.matches ? '#45433f' : '#d4d0c9', progressColor: e.matches ? '#f5f4f2' : '#0f0e0c', })})<Waveform.Track className="bg-track" /><Waveform.Progress className="bg-ink" />// bg-track and bg-ink are theme colors: they change with the theme.Markers and regions
With @wavesurfer/react, markers and regions come from wavesurfer.js plugins, which create their own elements; their content is a string or a DOM element. In sillon, Waveform.Marker and Waveform.Region are React components, and what goes inside them is your own JSX.
import WavesurferPlayer from '@wavesurfer/react'import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'function Player({ url, comments }) { const plugins = useMemo(() => [RegionsPlugin.create()], []) const onReady = () => { const [regions] = plugins for (const c of comments) { const avatar = document.createElement('img') avatar.src = c.user.photo regions.addRegion({ start: c.time, content: avatar }) } } return ( <WavesurferPlayer height={100} waveColor="#d4d0c9" progressColor="#0f0e0c" url={url} plugins={plugins} onReady={onReady} /> )}import { Waveform, useAudio } from '@sillon/react'function Player({ url, comments }) { const { peaks, player } = useAudio(url) return ( <Waveform.Root peaks={peaks} player={player}> <Waveform.Shape> <Waveform.Track className="bg-track" /> <Waveform.Progress className="bg-ink" /> </Waveform.Shape> {comments.map((c) => ( <Waveform.Marker key={c.id} time={c.time}> <Avatar user={c.user} /> </Waveform.Marker> ))} </Waveform.Root> )}Effects that follow the music
wavesurfer.js's renderFunction draws the bars on a canvas when the waveform renders: on load, zoom and resize. sillon's Waveform.GL runs a few lines of GLSL for every bar on every frame while playing. The shader knows each bar's height, how long ago the playhead passed it, and how loud the song is right now. The effects on the home page are made this way.
const ws = WaveSurfer.create({ container, url, renderFunction: (channels, ctx) => { const { width, height } = ctx.canvas const data = channels[0] for (let x = 0; x < width; x += 3) { const h = Math.abs(data[Math.floor((x / width) * data.length)]) * height ctx.fillRect(x, (height - h) / 2, 2, h) } },})// Runs for every bar on every frame while playing.const afterglow = `vec4 shade(Bar b) { if (!b.played) return waveColor; float heat = exp(-b.age / 14.0); vec3 ember = mix(vec3(1.0, 0.3, 0.12), vec3(1.0, 0.85, 0.55), b.uv.y * heat); return vec4(mix(progressColor.rgb, ember, heat), 1.0);}`<Waveform.Root peaks={peaks} player={player}> <Waveform.GL fragment={afterglow} /></Waveform.Root>Comments that group as you zoom
On a long track, comments pile up at the same few pixels. With wavesurfer.js, merging the markers that would overlap, and splitting them again on its zoom event, is code you write. useGroups does both, and re-renders only when the grouping changes. The comments on the home page use it.
function Comments({ comments }) { // Comments closer than 24 px at the current zoom share a marker; zoom in and they split. const groups = useGroups(comments, 24) return groups.map((g) => ( <Waveform.Marker key={g.key} time={g.time}> <Avatar user={g.items[0].user} /> {g.items.length > 1 && <Count n={g.items.length} />} </Waveform.Marker> ))}When to use wavesurfer.js
Use wavesurfer.js if you need a spectrogram, volume envelopes or separate left and right channels, which sillon doesn't have; if your app doesn't use React; or if you want a library with years of production use.