Skip to content

Commit ec1479e

Browse files
update AudioGridVisualizer
1 parent 210591c commit ec1479e

File tree

4 files changed

+278
-329
lines changed

4 files changed

+278
-329
lines changed

app/ui/_components.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,8 @@ export const COMPONENTS = {
323323

324324
// Audio grid visualizer
325325
AudioGridVisualizer: () => {
326-
const rowCounts = ['0', '3', '5', '7', '9'];
326+
const rowCounts = ['3', '5', '7', '9', '11', '13', '15'];
327+
const columnCounts = ['3', '5', '7', '9', '11', '13', '15'];
327328
const states = [
328329
'disconnected',
329330
'connecting',
@@ -335,6 +336,7 @@ export const COMPONENTS = {
335336

336337
const { microphoneTrack, localParticipant } = useLocalParticipant();
337338
const [rowCount, setRowCount] = useState(rowCounts[0]);
339+
const [columnCount, setColumnCount] = useState(columnCounts[0]);
338340
const [state, setState] = useState<AgentState>(states[0]);
339341
const [demoIndex, setDemoIndex] = useState(0);
340342

@@ -350,6 +352,12 @@ export const COMPONENTS = {
350352

351353
useMicrophone();
352354

355+
const demoOptions = {
356+
rowCount: parseInt(rowCount),
357+
columnCount: parseInt(columnCount),
358+
...gridVariants[demoIndex],
359+
};
360+
353361
return (
354362
<Container componentName="AudioVisualizer">
355363
<div className="flex items-center gap-2">
@@ -389,6 +397,24 @@ export const COMPONENTS = {
389397
</Select>
390398
</div>
391399

400+
<div className="flex-1">
401+
<label className="font-mono text-xs uppercase" htmlFor="columnCount">
402+
Column count
403+
</label>
404+
<Select value={columnCount.toString()} onValueChange={(value) => setColumnCount(value)}>
405+
<SelectTrigger id="columnCount" className="w-full">
406+
<SelectValue placeholder="Select a column count" />
407+
</SelectTrigger>
408+
<SelectContent>
409+
{columnCounts.map((columnCount) => (
410+
<SelectItem key={columnCount} value={columnCount.toString()}>
411+
{parseInt(columnCount) || 'Default'}
412+
</SelectItem>
413+
))}
414+
</SelectContent>
415+
</Select>
416+
</div>
417+
392418
<div className="flex-1">
393419
<label className="font-mono text-xs uppercase" htmlFor="demoIndex">
394420
Demo
@@ -413,16 +439,15 @@ export const COMPONENTS = {
413439

414440
<div className="grid place-items-center py-12">
415441
<AudioGridVisualizer
416-
key={`${demoIndex}-${rowCount}`}
442+
key={`${demoIndex}-${rowCount}-${columnCount}`}
417443
state={state}
418444
audioTrack={micTrackRef!}
419-
columnCount={parseInt(rowCount) || 5}
420-
options={gridVariants[demoIndex] as GridOptions}
445+
options={demoOptions}
421446
/>
422447
</div>
423-
<div className="border-border bg-muted rounded-xl border p-8">
448+
<div className="border-border bg-muted overflow-x-auto rounded-xl border p-8">
424449
<pre className="text-muted-foreground text-sm">
425-
<code>{JSON.stringify(gridVariants[demoIndex], null, 2)}</code>
450+
<code>{JSON.stringify(demoOptions, null, 2)}</code>
426451
</pre>
427452
</div>
428453
</Container>
Lines changed: 107 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,136 @@
1-
import { CSSProperties, ComponentType } from 'react';
1+
import { CSSProperties, ComponentType, JSX, useMemo } from 'react';
22
import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';
33
import {
44
type AgentState,
55
type TrackReferenceOrPlaceholder,
66
useMultibandTrackVolume,
77
} from '@livekit/components-react';
8-
import { type GridAnimationOptions, useGridAnimator } from './hooks/useGridAnimator';
8+
import { cn } from '@/lib/utils';
9+
import { type Coordinate, useGridAnimator } from './hooks/useGridAnimator';
10+
11+
type GridComponentType =
12+
| ComponentType<{ style?: CSSProperties; className?: string }>
13+
| keyof JSX.IntrinsicElements;
914

1015
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;
16+
radius?: number;
17+
interval?: number;
1718
rowCount?: number;
18-
animationOptions?: GridAnimationOptions;
19-
maxHeight?: number;
20-
minHeight?: number;
21-
radiusFactor?: number;
22-
radial?: boolean;
19+
columnCount?: number;
20+
className?: string;
21+
baseClassName?: string;
22+
offClassName?: string;
23+
onClassName?: string;
24+
gridComponent?: GridComponentType;
25+
transformer?: (index: number, rowCount: number, columnCount: number) => CSSProperties;
2326
}
2427

25-
export interface AudioGridVisualizerProps {
26-
style?: 'grid' | 'bar' | 'radial' | 'waveform';
27-
columnCount?: number;
28+
function useGrid(options: GridOptions) {
29+
return useMemo(() => {
30+
const { columnCount = 5, rowCount } = options;
31+
32+
const _columnCount = columnCount;
33+
const _rowCount = rowCount ?? columnCount;
34+
const items = new Array(_columnCount * _rowCount).fill(0).map((_, idx) => idx);
35+
36+
return { columnCount: _columnCount, rowCount: _rowCount, items };
37+
}, [options]);
38+
}
39+
40+
interface GridCellProps {
41+
index: number;
2842
state: AgentState;
29-
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
30-
options?: GridOptions;
43+
options: GridOptions;
44+
rowCount: number;
45+
volumeBands: number[];
46+
columnCount: number;
47+
highlightedCoordinate: Coordinate;
48+
Component: GridComponentType;
3149
}
3250

33-
export function AudioGridVisualizer({
51+
function GridCell({
52+
index,
3453
state,
35-
columnCount = 5,
36-
audioTrack,
3754
options,
38-
}: AudioGridVisualizerProps) {
55+
rowCount,
56+
volumeBands,
57+
columnCount,
58+
highlightedCoordinate,
59+
Component,
60+
}: GridCellProps) {
61+
const { interval = 100, baseClassName, onClassName, offClassName, transformer } = options;
62+
const rowMidPoint = Math.floor(rowCount / 2);
63+
64+
if (state === 'speaking') {
65+
const y = Math.floor(index / columnCount);
66+
const volumeChunks = 1 / (rowMidPoint + 1);
67+
const distanceToMid = Math.abs(rowMidPoint - y);
68+
const threshold = distanceToMid * volumeChunks;
69+
const isOn = volumeBands[index % columnCount] >= threshold;
70+
71+
return <Component className={cn(baseClassName, isOn ? onClassName : offClassName)} />;
72+
}
73+
74+
let transformerStyle: CSSProperties | undefined;
75+
if (transformer) {
76+
transformerStyle = transformer(index, rowCount, columnCount);
77+
}
78+
79+
const isOn =
80+
highlightedCoordinate.x === index % columnCount &&
81+
highlightedCoordinate.y === Math.floor(index / columnCount);
82+
83+
const transitionDurationInSeconds = interval / (isOn ? 1000 : 100);
84+
85+
return (
86+
<Component
87+
style={{
88+
transitionProperty: 'all',
89+
transitionDuration: `${transitionDurationInSeconds}s`,
90+
transitionTimingFunction: 'ease-out',
91+
...transformerStyle,
92+
}}
93+
className={cn(baseClassName, isOn ? onClassName : offClassName)}
94+
/>
95+
);
96+
}
97+
98+
export interface AudioGridVisualizerProps {
99+
state: AgentState;
100+
options: GridOptions;
101+
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
102+
}
103+
104+
export function AudioGridVisualizer({ state, options, audioTrack }: AudioGridVisualizerProps) {
105+
const { radius, interval = 100, className, gridComponent } = options;
106+
const { columnCount, rowCount, items } = useGrid(options);
107+
const highlightedCoordinate = useGridAnimator(state, rowCount, columnCount, interval, radius);
39108
const volumeBands = useMultibandTrackVolume(audioTrack, {
40109
bands: columnCount,
41110
loPass: 100,
42111
hiPass: 200,
43112
});
44113

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-
/>
108-
);
109-
})}
110-
</div>
111-
);
112-
});
114+
const GridComponent = gridComponent || 'div';
113115

114116
return (
115117
<div
116-
className="flex h-full items-center justify-center"
117-
style={{
118-
gap: options?.gridSpacing ?? '4px',
119-
}}
118+
className={cn('grid gap-1', className)}
119+
style={{ gridTemplateColumns: `repeat(${columnCount}, 1fr)` }}
120120
>
121-
{grid}
121+
{items.map((idx) => (
122+
<GridCell
123+
key={idx}
124+
index={idx}
125+
state={state}
126+
options={options}
127+
rowCount={rowCount}
128+
columnCount={columnCount}
129+
volumeBands={volumeBands}
130+
highlightedCoordinate={highlightedCoordinate}
131+
Component={GridComponent}
132+
/>
133+
))}
122134
</div>
123135
);
124136
}

0 commit comments

Comments
 (0)