All examples

Audio player and waveform

Play/pause and keyboard seek on a waveform built from peak samples. Untrusted sources go through the media URL policy; unsafe URLs are refused.

Audio player and waveform

media-audio · media/AudioPlayer

media audio
Props
  • allowedOrigins
  • barCount
  • label
  • peaks
  • src
  • trust

Live example loads in an isolated same-origin preview frame.

Source (media-audio/MediaAudioDemo.svelte)
<script lang="ts">
import { AudioPlayer, buildWaveformPeaks, Waveform } from '@texuli/media/audio';
import { onDestroy, onMount } from 'svelte';

/**
 * Audio example: synthetic peaks, a blob WAV (untrusted-safe), a standalone
 * waveform for keyboard seek, and a blocked javascript: source. Fictional data.
 */

function buildTonePeaks(bars = 64): number[] {
  const samples = Float32Array.from({ length: bars * 8 }, (_, i) => {
    const t = i / (bars * 8);
    const envelope = Math.sin(Math.PI * t);
    return envelope * (0.35 + 0.65 * Math.abs(Math.sin(i * 0.37)));
  });
  return buildWaveformPeaks(samples, bars);
}

/** Minimal mono 16-bit PCM WAV (silence + soft click envelope) for blob playback. */
function makeDemoWav(seconds = 2, sampleRate = 8_000): Blob {
  const numSamples = sampleRate * seconds;
  const dataSize = numSamples * 2;
  const buffer = new ArrayBuffer(44 + dataSize);
  const view = new DataView(buffer);
  const writeStr = (offset: number, s: string) => {
    for (let i = 0; i < s.length; i += 1) view.setUint8(offset + i, s.charCodeAt(i));
  };
  writeStr(0, 'RIFF');
  view.setUint32(4, 36 + dataSize, true);
  writeStr(8, 'WAVE');
  writeStr(12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, 1, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, sampleRate * 2, true);
  view.setUint16(32, 2, true);
  view.setUint16(34, 16, true);
  writeStr(36, 'data');
  view.setUint32(40, dataSize, true);
  for (let i = 0; i < numSamples; i += 1) {
    const t = i / sampleRate;
    const env = Math.sin(Math.PI * (i / numSamples));
    const sample = Math.sin(2 * Math.PI * 220 * t) * env * 0.25;
    const int = Math.max(-1, Math.min(1, sample));
    view.setInt16(44 + i * 2, (int * 0x7fff) | 0, true);
  }
  return new Blob([buffer], { type: 'audio/wav' });
}

const peaks = buildTonePeaks(64);
let blobUrl = $state('');
let waveProgress = $state(0.35);

onMount(() => {
  blobUrl = URL.createObjectURL(makeDemoWav(2));
});

onDestroy(() => {
  if (blobUrl) URL.revokeObjectURL(blobUrl);
});
</script>

<article class="demo" data-example="media-audio" aria-labelledby="media-audio-title">
  <h2 id="media-audio-title">Audio player and waveform</h2>
  <p>
    Peak bars are precomputed from a fictional tone so the demo stays deterministic. The clip is a
    short in-memory WAV served as a <code>blob:</code> URL, which the untrusted media policy
    accepts.
  </p>

  <section aria-labelledby="player-title">
    <h3 id="player-title">Player with waveform</h3>
    {#if blobUrl}
      <AudioPlayer
        src={blobUrl}
        {peaks}
        label="Harbor Notes demo clip"
      />
    {:else}
      <p role="status">Preparing demo clip…</p>
    {/if}
  </section>

  <section aria-labelledby="wave-title">
    <h3 id="wave-title">Standalone waveform (keyboard seek)</h3>
    <p>Arrow keys move ±2% (Shift ±10%). Home and End jump to the ends.</p>
    <Waveform
      {peaks}
      progress={waveProgress}
      label="Standalone waveform"
      onSeek={(ratio) => (waveProgress = ratio)}
    />
    <p role="status">
      Progress: <span data-wave-progress>{Math.round(waveProgress * 100)}</span>%
    </p>
  </section>

  <section aria-labelledby="blocked-title">
    <h3 id="blocked-title">Blocked unsafe source</h3>
    <AudioPlayer src="javascript:alert(1)" label="Blocked clip" />
  </section>
</article>

<style>
  .demo {
    display: grid;
    gap: 1.5rem;
    max-inline-size: 36rem;
    padding: 1.5rem;
  }

  .demo h2,
  .demo h3,
  .demo p {
    margin: 0;
  }

  .demo code {
    font-family: var(--tx-font-mono, ui-monospace, monospace);
    font-size: 0.9em;
  }
</style>