Skip to content

Commit 8f86129

Browse files
add AudioGridVisualizer
1 parent 9fd6637 commit 8f86129

File tree

4 files changed

+617
-4
lines changed

4 files changed

+617
-4
lines changed

app/ui/_components.tsx

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@ 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+
AudioGridVisualizer,
27+
type GridOptions,
28+
} from '@/components/livekit/audio-visualizer/audio-grid-visualizer/audio-grid-visualizer';
29+
import { gridVariants } from '@/components/livekit/audio-visualizer/audio-grid-visualizer/demos';
2530
import { Button, buttonVariants } from '@/components/livekit/button';
2631
import { ChatEntry } from '@/components/livekit/chat-entry';
2732
import {
@@ -191,8 +196,8 @@ export const COMPONENTS = {
191196
</Container>
192197
),
193198

194-
// Audio visualizer
195-
AudioVisualizer: () => {
199+
// Audio bar visualizer
200+
AudioBarVisualizer: () => {
196201
const barCounts = ['0', '3', '5', '7', '9'];
197202
const sizes = ['icon', 'xs', 'sm', 'md', 'lg', 'xl'];
198203
const states = [
@@ -309,6 +314,90 @@ export const COMPONENTS = {
309314
);
310315
},
311316

317+
// Audio grid visualizer
318+
AudioGridVisualizer: () => {
319+
const barCounts = ['0', '3', '5', '7', '9'];
320+
const states = [
321+
'disconnected',
322+
'connecting',
323+
'initializing',
324+
'listening',
325+
'thinking',
326+
'speaking',
327+
] as AgentState[];
328+
329+
const { microphoneTrack, localParticipant } = useLocalParticipant();
330+
const [barCount, setBarCount] = useState<string>(barCounts[0]);
331+
const [state, setState] = useState<AgentState>(states[0]);
332+
333+
const micTrackRef = useMemo<TrackReferenceOrPlaceholder | undefined>(() => {
334+
return state === 'speaking'
335+
? ({
336+
participant: localParticipant,
337+
source: Track.Source.Microphone,
338+
publication: microphoneTrack,
339+
} as TrackReference)
340+
: undefined;
341+
}, [state, localParticipant, microphoneTrack]);
342+
343+
useMicrophone();
344+
345+
return (
346+
<Container componentName="AudioVisualizer">
347+
<div className="flex items-center gap-2">
348+
<div className="flex-1">
349+
<label className="font-mono text-xs uppercase" htmlFor="state">
350+
State
351+
</label>
352+
<Select value={state} onValueChange={(value) => setState(value as AgentState)}>
353+
<SelectTrigger id="state" className="w-full">
354+
<SelectValue placeholder="Select a state" />
355+
</SelectTrigger>
356+
<SelectContent>
357+
{states.map((state) => (
358+
<SelectItem key={state} value={state}>
359+
{state}
360+
</SelectItem>
361+
))}
362+
</SelectContent>
363+
</Select>
364+
</div>
365+
366+
<div className="flex-1">
367+
<label className="font-mono text-xs uppercase" htmlFor="barCount">
368+
Bar count
369+
</label>
370+
<Select value={barCount.toString()} onValueChange={(value) => setBarCount(value)}>
371+
<SelectTrigger id="barCount" className="w-full">
372+
<SelectValue placeholder="Select a bar count" />
373+
</SelectTrigger>
374+
<SelectContent>
375+
{barCounts.map((barCount) => (
376+
<SelectItem key={barCount} value={barCount.toString()}>
377+
{parseInt(barCount) || 'Default'}
378+
</SelectItem>
379+
))}
380+
</SelectContent>
381+
</Select>
382+
</div>
383+
</div>
384+
385+
<div className="relative flex flex-col justify-center gap-4">
386+
{gridVariants.map((variant: GridOptions, idx) => (
387+
<div key={idx} className="border-border grid place-items-center rounded-xl border p-8">
388+
<AudioGridVisualizer
389+
state={state}
390+
audioTrack={micTrackRef!}
391+
columnCount={parseInt(barCount) || 5}
392+
options={variant}
393+
/>
394+
</div>
395+
))}
396+
</div>
397+
</Container>
398+
);
399+
},
400+
312401
// Agent control bar
313402
AgentControlBar: () => {
314403
useMicrophone();
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { CSSProperties, ComponentType } from 'react';
2+
import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';
3+
import {
4+
type AgentState,
5+
type TrackReferenceOrPlaceholder,
6+
useMultibandTrackVolume,
7+
} from '@livekit/components-react';
8+
import { type GridAnimationOptions, useGridAnimator } from './hooks/useGridAnimator';
9+
10+
export interface GridOptions {
11+
baseStyle: CSSProperties;
12+
gridComponent?: ComponentType<{ style: CSSProperties }>;
13+
gridSpacing?: string;
14+
onStyle?: CSSProperties;
15+
offStyle?: CSSProperties;
16+
transformer?: (distanceFromCenter: number, volumeBands: number[]) => CSSProperties;
17+
rowCount?: number;
18+
animationOptions?: GridAnimationOptions;
19+
maxHeight?: number;
20+
minHeight?: number;
21+
radiusFactor?: number;
22+
radial?: boolean;
23+
}
24+
25+
export interface AudioGridVisualizerProps {
26+
style?: 'grid' | 'bar' | 'radial' | 'waveform';
27+
columnCount?: number;
28+
state: AgentState;
29+
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
30+
options?: GridOptions;
31+
}
32+
33+
export function AudioGridVisualizer({
34+
state,
35+
columnCount = 5,
36+
audioTrack,
37+
options,
38+
}: AudioGridVisualizerProps) {
39+
const volumeBands = useMultibandTrackVolume(audioTrack, {
40+
bands: columnCount,
41+
loPass: 100,
42+
hiPass: 200,
43+
});
44+
45+
const gridColumns = volumeBands.length;
46+
const gridRows = options?.rowCount ?? gridColumns;
47+
const gridArray = Array.from({ length: gridColumns }).map((_, i) => i);
48+
const gridRowsArray = Array.from({ length: gridRows }).map((_, i) => i);
49+
const highlightedIndex = useGridAnimator(
50+
state,
51+
gridRows,
52+
gridColumns,
53+
options?.animationOptions?.interval ?? 100,
54+
state !== 'speaking' ? 'active' : 'paused',
55+
options?.animationOptions
56+
);
57+
58+
const rowMidPoint = Math.floor(gridRows / 2.0);
59+
const volumeChunks = 1 / (rowMidPoint + 1);
60+
61+
const baseStyle = options?.baseStyle ?? {};
62+
const onStyle = { ...baseStyle, ...(options?.onStyle ?? {}) };
63+
const offStyle = { ...baseStyle, ...(options?.offStyle ?? {}) };
64+
const GridComponent = options?.gridComponent || 'div';
65+
66+
const grid = gridArray.map((x) => {
67+
return (
68+
<div
69+
key={x}
70+
className="flex flex-col"
71+
style={{
72+
gap: options?.gridSpacing ?? '4px',
73+
}}
74+
>
75+
{gridRowsArray.map((y) => {
76+
const distanceToMid = Math.abs(rowMidPoint - y);
77+
const threshold = distanceToMid * volumeChunks;
78+
let targetStyle: CSSProperties;
79+
if (state !== 'speaking') {
80+
if (highlightedIndex.x === x && highlightedIndex.y === y) {
81+
targetStyle = {
82+
transition: `all ${(options?.animationOptions?.interval ?? 100) / 1000}s ease-out`,
83+
...onStyle,
84+
};
85+
} else {
86+
targetStyle = {
87+
transition: `all ${(options?.animationOptions?.interval ?? 100) / 100}s ease-out`,
88+
...offStyle,
89+
};
90+
}
91+
} else {
92+
if (volumeBands[x] >= threshold) {
93+
targetStyle = onStyle;
94+
} else {
95+
targetStyle = offStyle;
96+
}
97+
}
98+
99+
const distanceFromCenter = Math.sqrt(
100+
Math.pow(rowMidPoint - x, 2) + Math.pow(rowMidPoint - y, 2)
101+
);
102+
103+
return (
104+
<GridComponent
105+
style={{ ...targetStyle, ...options?.transformer?.(distanceFromCenter, volumeBands) }}
106+
key={x + '-' + y}
107+
></GridComponent>
108+
);
109+
})}
110+
</div>
111+
);
112+
});
113+
114+
return (
115+
<div
116+
className="flex h-full items-center justify-center"
117+
style={{
118+
gap: options?.gridSpacing ?? '4px',
119+
}}
120+
>
121+
{grid}
122+
</div>
123+
);
124+
}

0 commit comments

Comments
 (0)