Skip to content

Commit 81a32fc

Browse files
committed
Improve demo clarity
1 parent b9329a2 commit 81a32fc

4 files changed

Lines changed: 87 additions & 31 deletions

File tree

‎demo/app/_components/clips.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import type { DemoTrack } from './tracks';
99
export const CLIP_SECONDS = 5;
1010

1111
export const CLIPS: DemoTrack[] = [
12-
{ url: '/clips/part-1.wav', title: 'part-1.wav' },
13-
{ url: '/clips/part-2.wav', title: 'part-2.wav' },
12+
{ url: '/clips/part-1.wav', title: 'Scarlet Begonias' },
13+
{ url: '/clips/part-2.wav', title: 'Fire on the Mountain' },
1414
];
1515

1616
/** Normalized peak amplitude, 64 buckets per clip. */

‎demo/app/_components/gapless-demo.tsx‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { TrackInfo } from 'gapless';
44
import { useGapless } from './use-gapless';
55
import { CLIPS, CLIP_SECONDS } from './clips';
6-
import { Waveform } from './waveform';
6+
import { TrackBrackets, Waveform } from './waveform';
77
import { formatTime } from './tracks';
88
import { PauseIcon, PlayIcon } from './icons';
99
import { StageBadge, type Tone } from './ui';
@@ -83,25 +83,25 @@ export function GaplessDemo() {
8383
? [[i * CLIP_SECONDS, (i + 1) * CLIP_SECONDS] as const]
8484
: []
8585
)}
86-
marker={{
87-
label: !lastHandoff ? 'gap' : lastHandoff.gapless ? 'no gap' : 'gap',
88-
tone: lastHandoff && !lastHandoff.gapless ? 'bad' : 'good',
89-
}}
86+
seamless={!lastHandoff || lastHandoff.gapless}
87+
marker={
88+
!lastHandoff || lastHandoff.gapless
89+
? { label: 'gapless', tone: 'good' }
90+
: { label: 'gap', tone: 'bad' }
91+
}
9092
/>
9193

92-
<div className="mt-3 grid grid-cols-2 gap-1">
93-
{CLIPS.map((clip, i) => (
94-
<div
95-
key={clip.url}
96-
className={`flex items-center gap-2 ${i === 1 ? 'justify-end' : ''}`}
97-
>
94+
<TrackBrackets
95+
seamless={!lastHandoff || lastHandoff.gapless}
96+
labels={CLIPS.map((clip, i) => (
97+
<div key={clip.url} className="flex items-center gap-2">
9898
<p className="font-mono text-xs/5 whitespace-nowrap text-zinc-600 max-sm:hidden dark:text-zinc-400">
9999
{clip.title}
100100
</p>
101101
<StageBadge stage={stage(tracks[i], i === idx)} />
102102
</div>
103103
))}
104-
</div>
104+
/>
105105
</div>
106106

107107
<div className="mt-4 flex flex-wrap items-center justify-between gap-x-6 gap-y-2">
@@ -110,13 +110,13 @@ export function GaplessDemo() {
110110
{lastHandoff ? (
111111
lastHandoff.gapless ? (
112112
<p className="text-emerald-700 dark:text-emerald-400">
113-
<span className="font-semibold">Gapless.</span> part-2.wav started on the exact
114-
sample part-1.wav ended.
113+
<span className="font-semibold">Gapless.</span> “Fire on the Mountain” started on
114+
the exact sample “Scarlet Begonias” ended.
115115
</p>
116116
) : (
117117
<p className="text-amber-700 dark:text-amber-400">
118-
<span className="font-semibold">Not gapless.</span> part-2.wav wasn’t decoded in
119-
time, so it fell back to HTML5. Replay the transition.
118+
<span className="font-semibold">Not gapless.</span> “Fire on the Mountain” wasn’t
119+
decoded in time, so it fell back to HTML5. Replay the transition.
120120
</p>
121121
)
122122
) : (

‎demo/app/_components/native-demo.tsx‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect, useState, useSyncExternalStore } from 'react';
44
import { CLIPS, CLIP_SECONDS } from './clips';
55
import { formatTime } from './tracks';
66
import { PauseIcon, PlayIcon } from './icons';
7-
import { Waveform } from './waveform';
7+
import { TrackBrackets, Waveform } from './waveform';
88
import { claimPlayback, registerPlayer } from './exclusive-playback';
99

1010
interface NativeSnapshot {
@@ -231,14 +231,27 @@ export function NativeDemo() {
231231
</p>
232232
</div>
233233
<div className="flex min-w-0 basis-full items-center gap-4 pt-4 sm:basis-0 sm:flex-1 sm:pt-0">
234-
<Waveform
235-
className="h-10 min-w-0 flex-1"
236-
position={position}
237-
showPlayhead={started}
238-
onSeek={store.seek}
239-
buffered={buffered}
240-
marker={{ label: gapMs === null ? 'gap' : `${gapMs} ms gap`, tone: 'bad' }}
241-
/>
234+
<div className="min-w-0 flex-1">
235+
<Waveform
236+
className="h-10"
237+
position={position}
238+
showPlayhead={started}
239+
onSeek={store.seek}
240+
buffered={buffered}
241+
marker={{ label: gapMs === null ? 'gap' : `${gapMs} ms gap`, tone: 'bad' }}
242+
/>
243+
<TrackBrackets
244+
small
245+
labels={CLIPS.map((clip) => (
246+
<p
247+
key={clip.url}
248+
className="max-w-full truncate font-mono text-[0.6875rem]/4 text-zinc-500 dark:text-zinc-400"
249+
>
250+
{clip.title}
251+
</p>
252+
))}
253+
/>
254+
</div>
242255
<button
243256
type="button"
244257
onClick={() => store.seek(CLIP_SECONDS - 3)}

‎demo/app/_components/waveform.tsx‎

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import type { CSSProperties, MouseEvent } from 'react';
3+
import type { CSSProperties, MouseEvent, ReactNode } from 'react';
44
import { CLIP_SECONDS, PEAKS } from './clips';
55

66
const TOTAL = CLIP_SECONDS * PEAKS.length;
@@ -15,6 +15,7 @@ export function Waveform({
1515
showPlayhead,
1616
onSeek,
1717
marker,
18+
seamless = false,
1819
buffered = [],
1920
className = 'h-20',
2021
}: {
@@ -24,6 +25,8 @@ export function Waveform({
2425
onSeek: (seconds: number) => void;
2526
/** The line between the two files, and what to say about it. */
2627
marker: { label: string; tone: 'good' | 'bad' };
28+
/** Join the two panels with no space between them, telling them apart by tint. */
29+
seamless?: boolean;
2730
/** Buffered ranges in combined seconds, drawn brighter than unloaded audio. */
2831
buffered?: readonly (readonly [number, number])[];
2932
className?: string;
@@ -46,7 +49,7 @@ export function Waveform({
4649
if (e.key === 'ArrowRight') onSeek(Math.min(TOTAL - 0.5, position + 2));
4750
if (e.key === 'ArrowLeft') onSeek(Math.max(0, position - 2));
4851
}}
49-
className={`relative grid cursor-pointer grid-cols-2 gap-2 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-zinc-950 dark:focus-visible:outline-white ${className}`}
52+
className={`relative grid cursor-pointer grid-cols-2 ${seamless ? '' : 'gap-2'} focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-zinc-950 dark:focus-visible:outline-white ${className}`}
5053
>
5154
{PEAKS.map((peaks, clip) => {
5255
const start = clip * CLIP_SECONDS;
@@ -58,7 +61,16 @@ export function Waveform({
5861
);
5962
};
6063
return (
61-
<div key={clip} className="flex rounded-md bg-zinc-950/4 px-1.5 dark:bg-white/4">
64+
<div
65+
key={clip}
66+
className={`flex ${
67+
!seamless
68+
? 'rounded-md bg-zinc-950/4 px-1.5 dark:bg-white/4'
69+
: clip === 0
70+
? 'rounded-l-md bg-zinc-950/4 pr-px pl-1.5 dark:bg-white/4'
71+
: 'rounded-r-md bg-zinc-950/8 pr-1.5 dark:bg-white/8'
72+
}`}
73+
>
6274
<div
6375
onClick={onClick}
6476
className="relative flex flex-1 items-center gap-px"
@@ -93,10 +105,41 @@ export function Waveform({
93105
className={`pointer-events-none absolute -inset-y-2 left-1/2 w-0.5 -translate-x-1/2 rounded-full ${markerColor[0]}`}
94106
/>
95107
<div
96-
className={`pointer-events-none absolute -top-5 left-1/2 -translate-x-1/2 font-mono text-xs/5 font-medium whitespace-nowrap ${markerColor[1]}`}
108+
className={`pointer-events-none absolute -top-6 left-1/2 -translate-x-1/2 font-mono text-xs/5 font-medium whitespace-nowrap ${markerColor[1]}`}
97109
>
98110
{marker.label}
99111
</div>
100112
</div>
101113
);
102114
}
115+
116+
/**
117+
* A bracket under each clip's half of the waveform, with a stem down to its
118+
* label, so the strip reads as two tracks. Spacing matches `Waveform`'s
119+
* panels: pass `seamless` when the waveform does.
120+
*/
121+
export function TrackBrackets({
122+
labels,
123+
seamless = false,
124+
small = false,
125+
}: {
126+
labels: readonly ReactNode[];
127+
seamless?: boolean;
128+
small?: boolean;
129+
}) {
130+
const line = 'border-zinc-950/20 dark:border-white/20';
131+
return (
132+
<div className={`grid grid-cols-2 ${seamless ? '' : 'gap-2'} ${small ? 'mt-1.5' : 'mt-2'}`}>
133+
{labels.map((label, i) => (
134+
<div key={i} className="flex min-w-0 flex-col items-center">
135+
<div
136+
aria-hidden
137+
className={`mx-0.5 self-stretch rounded-b-xs border-x border-b ${line} ${small ? 'h-1' : 'h-1.5'}`}
138+
/>
139+
<div aria-hidden className={`w-px border-l ${line} ${small ? 'h-1' : 'h-2'}`} />
140+
{label}
141+
</div>
142+
))}
143+
</div>
144+
);
145+
}

0 commit comments

Comments
 (0)