Media
Audio visualizer
Draws sound as it plays: bars for loudness from low notes to high, or the wave itself. Point it at
an <audio> or <video> element, or a microphone.
Installation
npx sinaui add visualizerCopy src/lib/ui/Visualizer.svelte, with motion.ts and tokens.css. No dependencies.
For a file on another site, give the element crossorigin="anonymous", and the host
must allow cross-origin requests. Otherwise the visualizer only hears silence.
Examples
Bars
Press play.
<script lang="ts">
import Visualizer from '#lib/ui/Visualizer.svelte';
let audio = $state<HTMLAudioElement>();
</script>
<!-- crossorigin="anonymous": the file's host must allow it, or the visualizer hears silence. -->
<div class="demo">
<Visualizer source={audio} label="Sound levels of Taxim Hicaz" />
<audio
bind:this={audio}
controls
crossorigin="anonymous"
preload="metadata"
src="https://upload.wikimedia.org/wikipedia/commons/transcoded/0/0c/Taxim_Hicaz_%28c._1928%29.ogg/Taxim_Hicaz_%28c._1928%29.ogg.mp3"
></audio>
</div>
<style>
.demo {
display: grid;
gap: 0.75rem;
inline-size: 100%;
max-inline-size: 28rem;
}
audio {
inline-size: 100%;
}
</style>Wave
<script lang="ts">
import Visualizer from '#lib/ui/Visualizer.svelte';
let audio = $state<HTMLAudioElement>();
</script>
<div class="demo">
<Visualizer source={audio} variant="wave" />
<audio
bind:this={audio}
controls
crossorigin="anonymous"
preload="metadata"
src="https://upload.wikimedia.org/wikipedia/commons/transcoded/0/0c/Taxim_Hicaz_%28c._1928%29.ogg/Taxim_Hicaz_%28c._1928%29.ogg.mp3"
></audio>
</div>
<style>
.demo {
display: grid;
gap: 0.75rem;
inline-size: 100%;
max-inline-size: 28rem;
}
audio {
inline-size: 100%;
}
</style>Microphone
Your browser asks first. Nothing is recorded or sent anywhere.
<script lang="ts">
import { Mic01Icon, MicOff01Icon } from '@hugeicons/core-free-icons';
import Button from '#lib/ui/Button.svelte';
import Icon from '#lib/ui/Icon.svelte';
import Visualizer from '#lib/ui/Visualizer.svelte';
let stream = $state<MediaStream | null>(null);
let problem = $state('');
let asking = $state(false);
async function start() {
problem = '';
asking = true;
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (e) {
problem =
(e as DOMException).name === 'NotAllowedError'
? 'Microphone access was turned down. Allow it in your browser’s site settings, then try again.'
: 'No microphone could be found. Plug one in, then try again.';
} finally {
asking = false;
}
}
function stop() {
for (const t of stream?.getTracks() ?? []) t.stop();
stream = null;
}
// Leaving the page turns the microphone off.
$effect(() => stop);
</script>
<div class="demo">
<Visualizer
source={stream}
mirror
label={stream ? 'Your microphone’s sound levels' : undefined}
/>
{#if stream}
<Button variant="secondary" onclick={stop}
><Icon icon={MicOff01Icon} size={18} />Stop listening</Button
>
{:else}
<Button loading={asking} onclick={start}
><Icon icon={Mic01Icon} size={18} />Listen to the microphone</Button
>
{/if}
{#if problem}<p role="alert">{problem}</p>{/if}
</div>
<style>
.demo {
display: grid;
justify-items: center;
gap: 0.75rem;
inline-size: 100%;
max-inline-size: 28rem;
}
p {
margin: 0;
color: var(--ui-danger);
font-size: 0.875rem;
text-align: center;
}
</style>Props
| Prop | Type | Default |
|---|---|---|
source | HTMLMediaElement | MediaStream | null | |
variant | 'bars' | 'wave' | 'bars' |
bars | number | 32 |
mirror | boolean: grow from the middle | false |
label | string |
It takes the accent color, and its height from CSS (4rem by default): pass a class to change either.
Accessibility
- Decorative by default and hidden from screen readers; give it a
labelto name it. - It only draws while sound is playing and it’s on screen.
- With reduced motion, it updates a few times a second instead of every frame.