Skip to content

Commit b0b0bad

Browse files
AudioRadialVisualizer
1 parent 037de5c commit b0b0bad

File tree

5 files changed

+457
-12
lines changed

5 files changed

+457
-12
lines changed

app/ui/_components.tsx

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@ import { MicrophoneIcon } from '@phosphor-icons/react/dist/ssr';
1313
import { useSession } from '@/components/app/session-provider';
1414
import { AgentControlBar } from '@/components/livekit/agent-control-bar/agent-control-bar';
1515
import { TrackControl } from '@/components/livekit/agent-control-bar/track-control';
16-
import { TrackDeviceSelect } from '@/components/livekit/agent-control-bar/track-device-select';
17-
import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle';
16+
// import { TrackDeviceSelect } from '@/components/livekit/agent-control-bar/track-device-select';
17+
// import { TrackToggle } from '@/components/livekit/agent-control-bar/track-toggle';
1818
import { Alert, AlertDescription, AlertTitle, alertVariants } from '@/components/livekit/alert';
1919
import { AlertToast } from '@/components/livekit/alert-toast';
2020
import { BarVisualizer } from '@/components/livekit/audio-visualizer/audio-bar-visualizer/_bar-visualizer';
2121
import {
2222
AudioBarVisualizer,
2323
audioBarVisualizerVariants,
2424
} from '@/components/livekit/audio-visualizer/audio-bar-visualizer/audio-bar-visualizer';
25+
import {
26+
AudioRadialVisualizer,
27+
audioRadialVisualizerVariants,
28+
} from '@/components/livekit/audio-visualizer/audio-radial-visualizer/audio-radial-visualizer';
2529
import { Button, buttonVariants } from '@/components/livekit/button';
2630
import { ChatEntry } from '@/components/livekit/chat-entry';
2731
import {
@@ -40,6 +44,9 @@ type buttonVariantsType = VariantProps<typeof buttonVariants>['variant'];
4044
type buttonVariantsSizeType = VariantProps<typeof buttonVariants>['size'];
4145
type alertVariantsType = VariantProps<typeof alertVariants>['variant'];
4246
type audioBarVisualizerVariantsSizeType = VariantProps<typeof audioBarVisualizerVariants>['size'];
47+
type audioRadialVisualizerVariantsSizeType = VariantProps<
48+
typeof audioRadialVisualizerVariants
49+
>['size'];
4350

4451
export function useMicrophone() {
4552
const { startSession } = useSession();
@@ -191,10 +198,10 @@ export const COMPONENTS = {
191198
</Container>
192199
),
193200

194-
// Audio visualizer
195-
AudioVisualizer: () => {
201+
// Audio bar visualizer
202+
AudioBarVisualizer: () => {
196203
const barCounts = ['0', '3', '5', '7', '9'];
197-
const sizes = ['icon', 'xs', 'sm', 'md', 'lg', 'xl'];
204+
const sizes = ['icon', 'sm', 'md', 'lg', 'xl'];
198205
const states = [
199206
'disconnected',
200207
'connecting',
@@ -207,7 +214,7 @@ export const COMPONENTS = {
207214
const { microphoneTrack, localParticipant } = useLocalParticipant();
208215
const [barCount, setBarCount] = useState<string>(barCounts[0]);
209216
const [size, setSize] = useState<audioBarVisualizerVariantsSizeType>(
210-
sizes[3] as audioBarVisualizerVariantsSizeType
217+
'md' as audioBarVisualizerVariantsSizeType
211218
);
212219
const [state, setState] = useState<AgentState>(states[0]);
213220

@@ -258,7 +265,7 @@ export const COMPONENTS = {
258265
<SelectContent>
259266
{sizes.map((size) => (
260267
<SelectItem key={size} value={size as string}>
261-
{size}
268+
{size.toUpperCase()}
262269
</SelectItem>
263270
))}
264271
</SelectContent>
@@ -285,7 +292,7 @@ export const COMPONENTS = {
285292
</div>
286293

287294
<div className="relative flex flex-col justify-center gap-4">
288-
<div className="grid h-40 place-items-center">
295+
<div className="grid place-items-center py-8">
289296
<AudioBarVisualizer
290297
size={size as audioBarVisualizerVariantsSizeType}
291298
state={state}
@@ -295,7 +302,7 @@ export const COMPONENTS = {
295302
/>
296303
</div>
297304
<div className="text-center">Original BarVisualizer</div>
298-
<div className="border-border grid h-40 place-items-center space-y-4 rounded-xl border p-4">
305+
<div className="border-border grid place-items-center rounded-xl border p-4 py-8">
299306
<BarVisualizer
300307
size={size as audioBarVisualizerVariantsSizeType}
301308
state={state}
@@ -309,6 +316,114 @@ export const COMPONENTS = {
309316
);
310317
},
311318

319+
// Audio bar visualizer
320+
AudioRadialVisualizer: () => {
321+
const barCounts = ['0', '4', '8', '12', '16', '24'];
322+
const sizes = ['icon', 'sm', 'md', 'lg', 'xl'];
323+
const states = [
324+
'disconnected',
325+
'connecting',
326+
'initializing',
327+
'listening',
328+
'thinking',
329+
'speaking',
330+
] as AgentState[];
331+
332+
const { microphoneTrack, localParticipant } = useLocalParticipant();
333+
const [barCount, setBarCount] = useState<string>(barCounts[0]);
334+
const [size, setSize] = useState<audioRadialVisualizerVariantsSizeType>(
335+
'md' as audioRadialVisualizerVariantsSizeType
336+
);
337+
const [state, setState] = useState<AgentState>(states[0]);
338+
339+
const micTrackRef = useMemo<TrackReferenceOrPlaceholder | undefined>(() => {
340+
return state === 'speaking'
341+
? ({
342+
participant: localParticipant,
343+
source: Track.Source.Microphone,
344+
publication: microphoneTrack,
345+
} as TrackReference)
346+
: undefined;
347+
}, [state, localParticipant, microphoneTrack]);
348+
349+
useMicrophone();
350+
351+
return (
352+
<Container componentName="AudioVisualizer">
353+
<div className="flex items-center gap-2">
354+
<div className="flex-1">
355+
<label className="font-mono text-xs uppercase" htmlFor="state">
356+
State
357+
</label>
358+
<Select value={state} onValueChange={(value) => setState(value as AgentState)}>
359+
<SelectTrigger id="state" className="w-full">
360+
<SelectValue placeholder="Select a state" />
361+
</SelectTrigger>
362+
<SelectContent>
363+
{states.map((state) => (
364+
<SelectItem key={state} value={state}>
365+
{state}
366+
</SelectItem>
367+
))}
368+
</SelectContent>
369+
</Select>
370+
</div>
371+
372+
<div className="flex-1">
373+
<label className="font-mono text-xs uppercase" htmlFor="size">
374+
Size
375+
</label>
376+
<Select
377+
value={size as string}
378+
onValueChange={(value) => setSize(value as audioRadialVisualizerVariantsSizeType)}
379+
>
380+
<SelectTrigger id="size" className="w-full">
381+
<SelectValue placeholder="Select a size" />
382+
</SelectTrigger>
383+
<SelectContent>
384+
{sizes.map((size) => (
385+
<SelectItem key={size} value={size as string}>
386+
{size.toUpperCase()}
387+
</SelectItem>
388+
))}
389+
</SelectContent>
390+
</Select>
391+
</div>
392+
393+
<div className="flex-1">
394+
<label className="font-mono text-xs uppercase" htmlFor="barCount">
395+
Bar count
396+
</label>
397+
<Select value={barCount.toString()} onValueChange={(value) => setBarCount(value)}>
398+
<SelectTrigger id="barCount" className="w-full">
399+
<SelectValue placeholder="Select a bar count" />
400+
</SelectTrigger>
401+
<SelectContent>
402+
{barCounts.map((barCount) => (
403+
<SelectItem key={barCount} value={barCount.toString()}>
404+
{parseInt(barCount) || 'Default'}
405+
</SelectItem>
406+
))}
407+
</SelectContent>
408+
</Select>
409+
</div>
410+
</div>
411+
412+
<div className="relative flex flex-col justify-center gap-4">
413+
<div className="grid place-items-center py-20">
414+
<AudioRadialVisualizer
415+
size={size as audioBarVisualizerVariantsSizeType}
416+
state={state}
417+
audioTrack={micTrackRef!}
418+
barCount={parseInt(barCount) || undefined}
419+
className="mx-auto"
420+
/>
421+
</div>
422+
</div>
423+
</Container>
424+
);
425+
},
426+
312427
// Agent control bar
313428
AgentControlBar: () => {
314429
useMicrophone();

components/livekit/audio-visualizer/audio-bar-visualizer/audio-bar-visualizer.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const audioBarVisualizerVariants = cva(['relative flex items-center justi
1313
variants: {
1414
size: {
1515
icon: 'h-[24px] gap-[2px]',
16-
xs: 'h-[32px] gap-[2px]',
1716
sm: 'h-[56px] gap-[4px]',
1817
md: 'h-[112px] gap-[8px]',
1918
lg: 'h-[224px] gap-[16px]',
@@ -33,7 +32,6 @@ export const audioBarVisualizerBarVariants = cva(
3332
variants: {
3433
size: {
3534
icon: 'w-[4px] min-h-[4px]',
36-
xs: 'w-[4px] min-h-[4px]',
3735
sm: 'w-[8px] min-h-[8px]',
3836
md: 'w-[16px] min-h-[16px]',
3937
lg: 'w-[32px] min-h-[32px]',
@@ -68,7 +66,7 @@ export function AudioBarVisualizer({
6866
}
6967
switch (size) {
7068
case 'icon':
71-
case 'xs':
69+
case 'sm':
7270
return 3;
7371
default:
7472
return 5;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useEffect, useMemo, useRef } from 'react';
2+
import { type VariantProps, cva } from 'class-variance-authority';
3+
import {
4+
type AgentState,
5+
BarVisualizer as LiveKitBarVisualizer,
6+
type TrackReferenceOrPlaceholder,
7+
} from '@livekit/components-react';
8+
import { cn } from '@/lib/utils';
9+
10+
const MIN_HEIGHT = 15; // 15%
11+
12+
export const barVisualizerVariants = cva(
13+
['relative flex aspect-square h-36 items-center justify-center'],
14+
{
15+
variants: {
16+
size: {
17+
default: 'h-32',
18+
icon: 'h-6',
19+
xs: 'h-8',
20+
sm: 'h-16',
21+
md: 'h-32',
22+
lg: 'h-64',
23+
xl: 'h-96',
24+
'2xl': 'h-128',
25+
},
26+
},
27+
defaultVariants: {
28+
size: 'default',
29+
},
30+
}
31+
);
32+
33+
interface BarVisualizerProps {
34+
state?: AgentState;
35+
barCount?: number;
36+
audioTrack?: TrackReferenceOrPlaceholder;
37+
className?: string;
38+
}
39+
40+
export function BarVisualizer({
41+
size,
42+
state,
43+
barCount,
44+
audioTrack,
45+
className,
46+
}: BarVisualizerProps & VariantProps<typeof barVisualizerVariants>) {
47+
const ref = useRef<HTMLDivElement>(null);
48+
const _barCount = useMemo(() => {
49+
if (barCount) {
50+
return barCount;
51+
}
52+
switch (size) {
53+
case 'icon':
54+
case 'xs':
55+
return 3;
56+
default:
57+
return 5;
58+
}
59+
}, [barCount, size]);
60+
61+
const x = (1 / (_barCount + (_barCount + 1) / 2)) * 100;
62+
63+
// reset bars height when audio track is disconnected
64+
useEffect(() => {
65+
if (ref.current && !audioTrack) {
66+
const bars = [...(ref.current.querySelectorAll('& > span') ?? [])] as HTMLElement[];
67+
68+
bars.forEach((bar) => {
69+
bar.style.height = `${MIN_HEIGHT}%`;
70+
});
71+
}
72+
}, [audioTrack]);
73+
74+
return (
75+
<LiveKitBarVisualizer
76+
ref={ref}
77+
barCount={_barCount}
78+
state={state}
79+
trackRef={audioTrack}
80+
options={{ minHeight: x }}
81+
className={cn(barVisualizerVariants({ size }), className)}
82+
style={{
83+
gap: `${x / 2}%`,
84+
}}
85+
>
86+
<span
87+
className={cn([
88+
'bg-muted rounded-full',
89+
'origin-center transition-colors duration-250 ease-linear',
90+
'data-[lk-highlighted=true]:bg-foreground data-[lk-muted=true]:bg-muted',
91+
])}
92+
style={{
93+
minHeight: `${x}%`,
94+
width: `${x}%`,
95+
}}
96+
/>
97+
</LiveKitBarVisualizer>
98+
);
99+
}

0 commit comments

Comments
 (0)