Skip to content

Commit 2117571

Browse files
add AudioVisualizerAura
1 parent 61e0e31 commit 2117571

File tree

4 files changed

+572
-0
lines changed

4 files changed

+572
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react';
2+
import { StoryObj } from '@storybook/react-vite';
3+
import {
4+
AgentSessionProvider,
5+
useMicrophone,
6+
} from '../../.storybook/lk-decorators/AgentSessionProvider';
7+
import { AudioVisualizerAura, AudioVisualizerAuraProps } from '@agents-ui';
8+
import { useTheme } from 'next-themes';
9+
10+
export default {
11+
component: AudioVisualizerAura,
12+
decorators: [AgentSessionProvider],
13+
render: (args: AudioVisualizerAuraProps) => {
14+
const audioTrack = useMicrophone();
15+
const { theme } = useTheme();
16+
const themeMode = theme === 'dark' ? 'dark' : 'light';
17+
18+
return <AudioVisualizerAura {...args} themeMode={themeMode} audioTrack={audioTrack} />;
19+
},
20+
args: {
21+
size: 'xl',
22+
themeMode: 'dark',
23+
color: '#1FD5F9',
24+
colorShift: 0.3,
25+
state: 'connecting',
26+
},
27+
argTypes: {
28+
size: {
29+
options: ['icon', 'sm', 'md', 'lg', 'xl'],
30+
control: { type: 'radio' },
31+
},
32+
state: {
33+
options: [
34+
'idle',
35+
'disconnected',
36+
'pre-connect-buffering',
37+
'connecting',
38+
'initializing',
39+
'listening',
40+
'thinking',
41+
'speaking',
42+
'failed',
43+
],
44+
control: { type: 'radio' },
45+
},
46+
color: {
47+
control: { type: 'color' },
48+
},
49+
colorShift: {
50+
control: { type: 'range', min: 0, max: 1, step: 0.01 },
51+
},
52+
className: { control: { type: 'text' } },
53+
},
54+
parameters: {
55+
layout: 'centered',
56+
actions: {
57+
handles: [],
58+
},
59+
},
60+
};
61+
62+
export const Default: StoryObj<AudioVisualizerAuraProps> = {
63+
args: {},
64+
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
'use client';
2+
3+
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import { type VariantProps, cva } from 'class-variance-authority';
5+
import {
6+
type AnimationPlaybackControlsWithThen,
7+
type ValueAnimationTransition,
8+
animate,
9+
useMotionValue,
10+
useMotionValueEvent,
11+
} from 'motion/react';
12+
import {
13+
type AgentState,
14+
type TrackReference,
15+
type TrackReferenceOrPlaceholder,
16+
// useMultibandTrackVolume,
17+
useTrackVolume,
18+
} from '@livekit/components-react';
19+
import { cn } from '@/lib/utils';
20+
import { AuraShader, type AuraShaderProps } from './shader';
21+
22+
const DEFAULT_SPEED = 10;
23+
const DEFAULT_AMPLITUDE = 2;
24+
const DEFAULT_FREQUENCY = 0.5;
25+
const DEFAULT_SCALE = 0.2;
26+
const DEFAULT_BRIGHTNESS = 1.5;
27+
const DEFAULT_TRANSITION: ValueAnimationTransition = { duration: 0.5, ease: 'easeOut' };
28+
const DEFAULT_PULSE_TRANSITION: ValueAnimationTransition = {
29+
duration: 0.35,
30+
ease: 'easeOut',
31+
repeat: Infinity,
32+
repeatType: 'mirror',
33+
};
34+
35+
function useAnimatedValue<T>(initialValue: T) {
36+
const [value, setValue] = useState(initialValue);
37+
const motionValue = useMotionValue(initialValue);
38+
const controlsRef = useRef<AnimationPlaybackControlsWithThen | null>(null);
39+
useMotionValueEvent(motionValue, 'change', (value) => setValue(value as T));
40+
41+
const animateFn = useCallback(
42+
(targetValue: T | T[], transition: ValueAnimationTransition) => {
43+
controlsRef.current = animate(motionValue, targetValue, transition);
44+
},
45+
[motionValue],
46+
);
47+
48+
return { value, motionValue, controls: controlsRef, animate: animateFn };
49+
}
50+
51+
export const AudioVisualizerAuraVariants = cva(['aspect-square'], {
52+
variants: {
53+
size: {
54+
icon: 'h-[24px] gap-[2px]',
55+
sm: 'h-[56px] gap-[4px]',
56+
md: 'h-[112px] gap-[8px]',
57+
lg: 'h-[224px] gap-[16px]',
58+
xl: 'h-[448px] gap-[32px]',
59+
},
60+
},
61+
defaultVariants: {
62+
size: 'md',
63+
},
64+
});
65+
66+
export interface AudioVisualizerAuraProps {
67+
state?: AgentState;
68+
audioTrack: TrackReferenceOrPlaceholder;
69+
}
70+
71+
export function AudioVisualizerAura({
72+
size = 'md',
73+
color,
74+
state = 'speaking',
75+
themeMode,
76+
shape = 1,
77+
colorShift = 0.05,
78+
audioTrack,
79+
className,
80+
}: AudioVisualizerAuraProps & AuraShaderProps & VariantProps<typeof AudioVisualizerAuraVariants>) {
81+
const [speed, setSpeed] = useState(DEFAULT_SPEED);
82+
const {
83+
value: scale,
84+
animate: animateScale,
85+
motionValue: scaleMotionValue,
86+
} = useAnimatedValue(DEFAULT_SCALE);
87+
const { value: amplitude, animate: animateAmplitude } = useAnimatedValue(DEFAULT_AMPLITUDE);
88+
const { value: frequency, animate: animateFrequency } = useAnimatedValue(DEFAULT_FREQUENCY);
89+
const { value: brightness, animate: animateBrightness } = useAnimatedValue(DEFAULT_BRIGHTNESS);
90+
91+
const volume = useTrackVolume(audioTrack as TrackReference, {
92+
fftSize: 512,
93+
smoothingTimeConstant: 0.55,
94+
});
95+
96+
useEffect(() => {
97+
switch (state) {
98+
case 'idle':
99+
case 'failed':
100+
case 'disconnected':
101+
setSpeed(10);
102+
animateScale(0.2, DEFAULT_TRANSITION);
103+
animateAmplitude(1.2, DEFAULT_TRANSITION);
104+
animateFrequency(0.4, DEFAULT_TRANSITION);
105+
animateBrightness(1.0, DEFAULT_TRANSITION);
106+
return;
107+
setSpeed(5);
108+
animateScale(0.2, DEFAULT_TRANSITION);
109+
animateAmplitude(1.2, DEFAULT_TRANSITION);
110+
animateFrequency(0.4, DEFAULT_TRANSITION);
111+
animateBrightness(1.0, DEFAULT_TRANSITION);
112+
return;
113+
case 'listening':
114+
case 'pre-connect-buffering':
115+
setSpeed(20);
116+
animateScale(0.3, { type: 'spring', duration: 1.0, bounce: 0.35 });
117+
animateAmplitude(1.0, DEFAULT_TRANSITION);
118+
animateFrequency(0.7, DEFAULT_TRANSITION);
119+
animateBrightness([1.5, 2.0], DEFAULT_PULSE_TRANSITION);
120+
return;
121+
case 'thinking':
122+
case 'connecting':
123+
case 'initializing':
124+
setSpeed(30);
125+
animateScale(0.3, DEFAULT_TRANSITION);
126+
animateAmplitude(0.5, DEFAULT_TRANSITION);
127+
animateFrequency(1, DEFAULT_TRANSITION);
128+
animateBrightness([0.5, 2.5], DEFAULT_PULSE_TRANSITION);
129+
return;
130+
case 'speaking':
131+
setSpeed(70);
132+
animateScale(0.3, DEFAULT_TRANSITION);
133+
animateAmplitude(0.75, DEFAULT_TRANSITION);
134+
animateFrequency(1.25, DEFAULT_TRANSITION);
135+
animateBrightness(1.5, DEFAULT_TRANSITION);
136+
return;
137+
}
138+
}, [state, shape, animateScale, animateAmplitude, animateFrequency, animateBrightness]);
139+
140+
useEffect(() => {
141+
if (state === 'speaking' && volume > 0 && !scaleMotionValue.isAnimating()) {
142+
animateScale(0.2 + 0.2 * volume, { duration: 0 });
143+
}
144+
}, [
145+
state,
146+
volume,
147+
scaleMotionValue,
148+
animateScale,
149+
animateAmplitude,
150+
animateFrequency,
151+
animateBrightness,
152+
]);
153+
154+
return (
155+
<AuraShader
156+
blur={0.2}
157+
shape={shape}
158+
color={color}
159+
colorShift={colorShift}
160+
speed={speed}
161+
scale={scale}
162+
themeMode={themeMode}
163+
amplitude={amplitude}
164+
frequency={frequency}
165+
brightness={brightness}
166+
className={cn(
167+
AudioVisualizerAuraVariants({ size }),
168+
'overflow-hidden rounded-full',
169+
className,
170+
)}
171+
/>
172+
);
173+
}

0 commit comments

Comments
 (0)