Skip to content

Commit fc05ce0

Browse files
AudioOscilloscopeVisualizer
1 parent 9c6662b commit fc05ce0

File tree

3 files changed

+412
-2
lines changed

3 files changed

+412
-2
lines changed

app/ui/_components.tsx

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import { useEffect, useMemo, useState } from 'react';
44
import { type VariantProps } from 'class-variance-authority';
55
import { Track } from 'livekit-client';
6-
import { RoomAudioRenderer, StartAudio } from '@livekit/components-react';
6+
// import { RoomAudioRenderer, StartAudio } from '@livekit/components-react';
77
import {
88
type AgentState,
99
type TrackReference,
1010
type TrackReferenceOrPlaceholder,
1111
useLocalParticipant,
12-
useVoiceAssistant,
12+
// useVoiceAssistant,
1313
} from '@livekit/components-react';
1414
import { MicrophoneIcon } from '@phosphor-icons/react/dist/ssr';
1515
import { useSession } from '@/components/app/session-provider';
@@ -26,6 +26,7 @@ import {
2626
} from '@/components/livekit/audio-visualizer/audio-bar-visualizer/audio-bar-visualizer';
2727
import { AudioGridVisualizer } from '@/components/livekit/audio-visualizer/audio-grid-visualizer/audio-grid-visualizer';
2828
import { gridVariants } from '@/components/livekit/audio-visualizer/audio-grid-visualizer/demos';
29+
import { AudioOscilloscopeVisualizer } from '@/components/livekit/audio-visualizer/audio-oscilloscope-visualizer/audio-oscilloscope-visualizer';
2930
import {
3031
AudioRadialVisualizer,
3132
audioRadialVisualizerVariants,
@@ -694,6 +695,102 @@ export const COMPONENTS = {
694695
);
695696
},
696697

698+
AudioOscilloscopeVisualizer: () => {
699+
// shape
700+
const [shape, setShape] = useState(1.0);
701+
702+
const sizes = ['icon', 'sm', 'md', 'lg', 'xl'];
703+
const states = [
704+
'disconnected',
705+
'connecting',
706+
'initializing',
707+
'listening',
708+
'thinking',
709+
'speaking',
710+
] as AgentState[];
711+
712+
const [size, setSize] = useState<audioShaderVisualizerVariantsSizeType>('lg');
713+
const [state, setState] = useState<AgentState>(states[0]);
714+
715+
const { microphoneTrack, localParticipant } = useLocalParticipant();
716+
const micTrackRef = useMemo<TrackReferenceOrPlaceholder | undefined>(() => {
717+
return state === 'speaking'
718+
? ({
719+
participant: localParticipant,
720+
source: Track.Source.Microphone,
721+
publication: microphoneTrack,
722+
} as TrackReference)
723+
: undefined;
724+
}, [state, localParticipant, microphoneTrack]);
725+
726+
useMicrophone();
727+
728+
return (
729+
<Container componentName="AudioShaderVisualizer">
730+
<div className="flex gap-4">
731+
<div className="flex-1">
732+
<label className="font-mono text-xs uppercase" htmlFor="size">
733+
Size
734+
</label>
735+
<Select
736+
value={size as string}
737+
onValueChange={(value) => setSize(value as audioShaderVisualizerVariantsSizeType)}
738+
>
739+
<SelectTrigger id="size" className="w-full">
740+
<SelectValue placeholder="Select a size" />
741+
</SelectTrigger>
742+
<SelectContent>
743+
{sizes.map((size) => (
744+
<SelectItem key={size} value={size as string}>
745+
{size.toUpperCase()}
746+
</SelectItem>
747+
))}
748+
</SelectContent>
749+
</Select>
750+
</div>
751+
752+
<div className="flex-1">
753+
<label className="font-mono text-xs uppercase" htmlFor="shape">
754+
Shape
755+
</label>
756+
<Select value={shape.toString()} onValueChange={(value) => setShape(parseInt(value))}>
757+
<SelectTrigger id="shape" className="w-full">
758+
<SelectValue placeholder="Select a shape" />
759+
</SelectTrigger>
760+
<SelectContent>
761+
<SelectItem value="1">Circle</SelectItem>
762+
<SelectItem value="2">Line</SelectItem>
763+
</SelectContent>
764+
</Select>
765+
</div>
766+
</div>
767+
768+
<div className="py-12">
769+
<AudioOscilloscopeVisualizer
770+
size={size}
771+
state={state}
772+
audioTrack={micTrackRef!}
773+
className="mx-auto"
774+
/>
775+
</div>
776+
777+
<div className="flex flex-wrap gap-4">
778+
{states.map((stateType) => (
779+
<Button
780+
key={stateType}
781+
size="sm"
782+
variant={state === stateType ? 'primary' : 'default'}
783+
onClick={() => setState(stateType)}
784+
className={'flex-1'}
785+
>
786+
{stateType}
787+
</Button>
788+
))}
789+
</div>
790+
</Container>
791+
);
792+
},
793+
697794
// Agent control bar
698795
AgentControlBar: () => {
699796
useMicrophone();
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
'use client';
2+
3+
import { useEffect, useMemo, 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 { OscilliscopeShaders, type OscilliscopeShadersProps } from './oscilliscope-shaders';
21+
22+
const DEFAULT_SPEED = 5;
23+
const DEFAULT_AMPLITUDE = 0.025;
24+
const DEFAULT_FREQUENCY = 10;
25+
const DEFAULT_TRANSITION: ValueAnimationTransition = { duration: 0.2, ease: 'easeOut' };
26+
27+
export const audioShaderVisualizerVariants = cva(['aspect-square'], {
28+
variants: {
29+
size: {
30+
icon: 'h-[24px] gap-[2px]',
31+
sm: 'h-[56px] gap-[4px]',
32+
md: 'h-[112px] gap-[8px]',
33+
lg: 'h-[224px] gap-[16px]',
34+
xl: 'h-[448px] gap-[32px]',
35+
},
36+
},
37+
defaultVariants: {
38+
size: 'md',
39+
},
40+
});
41+
42+
interface AudioOscilloscopeVisualizerProps {
43+
state?: AgentState;
44+
audioTrack: TrackReferenceOrPlaceholder;
45+
className?: string;
46+
}
47+
48+
export function AudioOscilloscopeVisualizer({
49+
size = 'lg',
50+
state = 'speaking',
51+
audioTrack,
52+
className,
53+
}: AudioOscilloscopeVisualizerProps &
54+
OscilliscopeShadersProps &
55+
VariantProps<typeof audioShaderVisualizerVariants>) {
56+
const [speed, setSpeed] = useState(DEFAULT_SPEED);
57+
const [amplitude, setAmplitude] = useState(DEFAULT_AMPLITUDE);
58+
const [frequency, setFrequency] = useState(DEFAULT_FREQUENCY);
59+
60+
const amplitudeValue = useMotionValue(DEFAULT_AMPLITUDE);
61+
const frequencyValue = useMotionValue(DEFAULT_FREQUENCY);
62+
63+
const amplitudeControlsRef = useRef<AnimationPlaybackControlsWithThen | null>(null);
64+
const frequencyControlsRef = useRef<AnimationPlaybackControlsWithThen | null>(null);
65+
66+
useMotionValueEvent(amplitudeValue, 'change', (value) => setAmplitude(value));
67+
useMotionValueEvent(frequencyValue, 'change', (value) => setFrequency(value));
68+
69+
const volume = useTrackVolume(audioTrack as TrackReference, {
70+
fftSize: 512,
71+
smoothingTimeConstant: 0.55,
72+
});
73+
74+
useEffect(() => {
75+
switch (state) {
76+
case 'connecting':
77+
setSpeed(DEFAULT_SPEED);
78+
amplitudeControlsRef.current = animate(amplitudeValue, 0.1, DEFAULT_TRANSITION);
79+
frequencyControlsRef.current = animate(frequencyValue, [50, 60], {
80+
duration: 0.33,
81+
repeat: Infinity,
82+
repeatType: 'mirror',
83+
});
84+
return;
85+
case 'initializing':
86+
setSpeed(DEFAULT_SPEED);
87+
amplitudeControlsRef.current = animate(amplitudeValue, 0.05, DEFAULT_TRANSITION);
88+
frequencyControlsRef.current = animate(frequencyValue, [40, 30], {
89+
duration: 0.66,
90+
repeat: Infinity,
91+
repeatType: 'mirror',
92+
});
93+
return;
94+
case 'listening':
95+
setSpeed(DEFAULT_SPEED);
96+
amplitudeControlsRef.current = animate(amplitudeValue, 0.025, DEFAULT_TRANSITION);
97+
frequencyControlsRef.current = animate(frequencyValue, [30, 10], {
98+
duration: 1.5,
99+
repeat: Infinity,
100+
repeatType: 'mirror',
101+
});
102+
return;
103+
case 'thinking':
104+
setSpeed(DEFAULT_SPEED);
105+
amplitudeControlsRef.current = animate(amplitudeValue, 0.025, DEFAULT_TRANSITION);
106+
frequencyControlsRef.current = animate(frequencyValue, [30, 50], {
107+
duration: 0.5,
108+
repeat: Infinity,
109+
repeatType: 'mirror',
110+
});
111+
return;
112+
default:
113+
setSpeed(DEFAULT_SPEED);
114+
amplitudeControlsRef.current = animate(amplitudeValue, 0.025, DEFAULT_TRANSITION);
115+
frequencyControlsRef.current = animate(frequencyValue, 10, DEFAULT_TRANSITION);
116+
return;
117+
}
118+
}, [state, amplitudeValue, frequencyValue]);
119+
120+
useEffect(() => {
121+
if (state === 'speaking' && volume > 0) {
122+
amplitudeControlsRef.current?.stop();
123+
frequencyControlsRef.current?.stop();
124+
125+
amplitudeValue.set(0.02 + 0.4 * volume);
126+
frequencyValue.set(20 + 60 * volume);
127+
}
128+
}, [state, volume, amplitudeValue, frequencyValue]);
129+
130+
return (
131+
<OscilliscopeShaders
132+
speed={speed}
133+
amplitude={amplitude}
134+
frequency={frequency}
135+
lineWidth={0.005}
136+
smoothing={0}
137+
className={cn(
138+
audioShaderVisualizerVariants({ size }),
139+
'[mask-image:linear-gradient(90deg,rgba(0,0,0,0)_0%,rgba(0,0,0,1)_20%,rgba(0,0,0,1)_80%,rgba(0,0,0,0)_100%)]',
140+
'overflow-hidden rounded-full',
141+
className
142+
)}
143+
/>
144+
);
145+
}

0 commit comments

Comments
 (0)