Media

AudioPlayer

An audio clip with play, a seek bar that says the time, and a button cycling through playback speeds.

#media#audio#interactive

Demo

Preview
T. rex roar · MDN sample sounds

Ended 0 times.

No speed button

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/audio_player.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/audio_player.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/audio_player.qwe
// The clip is one of MDN's shared sample sounds.let audio_player_demo_src = "https://mdn.github.io/shared-assets/audio/t-rex-roar.mp3"component AudioPlayerDemoLive() {    state ended = 0    <div.demo-stack>        <AudioPlayer src={audio_player_demo_src} title="T. rex roar" artist="MDN sample sounds" on_end={() => { ended += 1 }}/>        <p.demo-note id="audio-player-demo-note">Ended {ended} times.</p>    </div>}view AudioPlayerDemo() =    <div.demo-stack.demo-narrow style="max-width: 520px">        <AudioPlayerDemoLive/>        <AudioPlayer src={audio_player_demo_src} title="Compact, two speeds" compact={true} speeds={[1, 2]}/>        <AudioPlayer src={audio_player_demo_src} title="No speed button" speeds={[]}/>    </div>

Source

ui/audio_player.qwe
// AudioPlayer: an <audio> with the kit's own controls, in one row: play and// pause, the title, a seek bar with the time, and a speed button that cycles// through the speeds. The seek bar is a slider for the keyboard and screen// readers, and says the time.////   <AudioPlayer src="/audio/ep12.mp3" title="Episode 12: Views first" artist="The Quillo Podcast"/>//   <AudioPlayer src={clip} title="Voice note" speeds={[1, 1.5, 2]} on_end={() => { heard = true }}/>//// src: the audio. title, artist: what is playing (title is also the player's// name for screen readers). speeds: the playback speeds the button cycles// through ([1, 1.25, 1.5, 2]); none hides the button. loop: as on <audio>.// preload: "metadata" (default), "auto" or "none". compact: no title line.// on_play(), on_pause(), on_end(): inside one of your components.import ui/theme.qwe use ui_clslet audio_player_css = css`.ui-audio-player {  display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); font: 13px/1.3 var(--ui-font);}.ui-audio-player-play {  display: grid; place-items: center; flex: none; width: 40px; height: 40px; padding: 0; border: 0; border-radius: 50%;  background: var(--ui-accent); color: var(--ui-accent-fg); cursor: pointer; transition: transform .12s;}.ui-audio-player-play:hover { transform: scale(1.05); }.ui-audio-player-play:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-audio-player-play svg { width: 18px; height: 18px; }.ui-audio-player-main { flex: 1; min-width: 0; display: grid; gap: 4px; }.ui-audio-player-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }.ui-audio-player-artist { color: var(--ui-muted); font-weight: 400; }.ui-audio-player-row { display: flex; align-items: center; gap: 8px; }.ui-audio-player-time { flex: none; color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-audio-player-seek { flex: 1; min-width: 60px; appearance: none; height: 18px; margin: 0; background: transparent; cursor: pointer; }.ui-audio-player-seek::-webkit-slider-runnable-track { height: 5px; border-radius: 999px; background: linear-gradient(to right, var(--ui-accent) var(--f, 0%), var(--ui-subtle) var(--f, 0%)); box-shadow: inset 0 0 0 1px var(--ui-border); }.ui-audio-player-seek::-moz-range-track { height: 5px; border-radius: 999px; background: var(--ui-subtle); }.ui-audio-player-seek::-moz-range-progress { height: 5px; border-radius: 999px; background: var(--ui-accent); }.ui-audio-player-seek::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; margin-top: -4.5px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); }.ui-audio-player-seek::-moz-range-thumb { width: 10px; height: 10px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); }.ui-audio-player-seek:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 999px; }.ui-audio-player-speed {  flex: none; height: 28px; min-width: 44px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface);  color: var(--ui-fg); font: 600 12px/1 var(--ui-font); font-variant-numeric: tabular-nums; cursor: pointer;}.ui-audio-player-speed:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-audio-player-speed:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-audio-player-play { transition: none; } }`// Seconds as "m:ss", or "h:mm:ss" from an hour.fn audio_player_time(s: num) -> str {    let t = floor(s)    let h = floor(t / 3600)    let m = floor(t % 3600 / 60)    let sec = t % 60    let two = (n: int) => n < 10 ? `0${n}` : str(n)    h > 0 ? `${h}:${two(m)}:${two(sec)}` : `${m}:${two(sec)}`}// A speed as its button says it: 1 is "1×", 1.25 is "1.25×".fn audio_player_speed(x: num) -> str = `${x}×`component AudioPlayer(src: str, title: str = "Audio", artist: str? = null, speeds: list = [1, 1.25, 1.5, 2], loop: bool = false,                      preload: str = "metadata", compact: bool = false, on_play = null, on_pause = null, on_end = null,                      class: str? = null) {    style global audio_player_css    state el = null    state playing = false    state time = 0    state dur = 0    state at = 0    let total = dur > 0 ? dur : 0    let fill = total > 0 ? round(time * 100 / total, 2) : 0    let toggle = () => {        if el != null {            // The browser may refuse to play (no click on the page yet): the button stays Play.            if playing { el.pause() } else { el.play().catch((_ = null) => { playing = false }) }        }    }    <div class={ui_cls(["ui-audio-player", class])} role="region" aria-label={title}>        <audio src={src} loop={loop} preload={preload}               @mount={(a) => { el = a; if math.isfinite(a.duration) && a.duration > 0 { dur = a.duration } }}               @loadedmetadata={(e) => { dur = math.isfinite(e.duration) ? e.duration : 0 }}               @durationchange={(e) => { dur = math.isfinite(e.duration) ? e.duration : 0 }}               @timeupdate={(e) => { time = e.currentTime }}               @play={() => { playing = true; on_play && on_play() }}               @pause={() => { playing = false; on_pause && on_pause() }}               @ended={() => { playing = false; on_end && on_end() }}></audio>        <button.ui-audio-player-play type="button" aria-label={playing ? "Pause" : "Play"} @click={toggle}>            {playing                ? <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>                : <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4v16l14-8z"/></svg>}        </button>        <div.ui-audio-player-main>            {!compact && <div.ui-audio-player-title>{title}{artist != null && <span.ui-audio-player-artist> · {artist}</span>}</div>}            <div.ui-audio-player-row>                <span.ui-audio-player-time aria-hidden="true">{audio_player_time(time)}</span>                <input.ui-audio-player-seek type="range" min="0" max={total} step="0.1" value={time} aria-label="Seek" disabled={total == 0}                       aria-valuetext={`${audio_player_time(time)} of ${audio_player_time(total)}`} style={`--f: ${fill}%`}                       @input={(e) => { if el != null { el.currentTime = float(e.value) } }}/>                <span.ui-audio-player-time aria-hidden="true">{audio_player_time(total)}</span>            </div>        </div>        {len(speeds) > 0 && <button.ui-audio-player-speed type="button" aria-label={`Speed: ${audio_player_speed(speeds[at])}`}                title="Playback speed"                @click={() => {                    at = (at + 1) % len(speeds)                    if el != null { el.playbackRate = speeds[at] }                }}>{audio_player_speed(speeds[at])}</button>}    </div>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On AudioPlayer: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.