Skip to content

Commit 93f8dcb

Browse files
committed
Use requestanimation loop to poll time
1 parent dbbaae3 commit 93f8dcb

File tree

1 file changed

+75
-38
lines changed

1 file changed

+75
-38
lines changed

packages/react/src/lib/useSoundPlayer.ts

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import { convertBase64ToBlob } from 'hume';
2-
import { useCallback, useRef, useState } from 'react';
2+
import { useCallback, useEffect, useRef, useState } from 'react';
33

44
import { convertLinearFrequenciesToBark } from './convertFrequencyScale';
55
import { generateEmptyFft } from './generateEmptyFft';
66
import type { AudioOutputMessage } from '../models/messages';
77

8+
const FADE_DURATION = 0.1;
9+
const FADE_TARGET = 0.0001;
10+
811
export const useSoundPlayer = (props: {
912
onError: (message: string) => void;
1013
onPlayAudio: (id: string) => void;
1114
onStopAudio: (id: string) => void;
1215
}) => {
1316
const [isPlaying, setIsPlaying] = useState(false);
1417
const [isAudioMuted, setIsAudioMuted] = useState(false);
18+
const isFadeCancelled = useRef(false);
1519
const [volume, setVolumeState] = useState<number>(1.0);
1620
const [fft, setFft] = useState<number[]>(generateEmptyFft());
1721

@@ -87,46 +91,65 @@ export const useSoundPlayer = (props: {
8791
}
8892
}, []);
8993

90-
const addToQueue = useCallback(async (message: AudioOutputMessage) => {
91-
if (!isInitialized.current || !audioContext.current) {
92-
onError.current('Audio player has not been initialized');
93-
return;
94-
}
95-
96-
try {
97-
const blob = convertBase64ToBlob(message.data);
98-
const arrayBuffer = await blob.arrayBuffer();
99-
const audioBuffer =
100-
await audioContext.current.decodeAudioData(arrayBuffer);
101-
102-
const pcmData = audioBuffer.getChannelData(0);
103-
104-
if (gainNode.current) {
105-
const now = audioContext.current.currentTime;
106-
gainNode.current.gain.cancelScheduledValues(now);
107-
const targetGain = isAudioMuted ? 0 : volume;
108-
gainNode.current.gain.setValueAtTime(targetGain, now);
94+
const addToQueue = useCallback(
95+
async (message: AudioOutputMessage) => {
96+
if (!isInitialized.current || !audioContext.current) {
97+
onError.current('Audio player has not been initialized');
98+
return;
10999
}
110100

111-
workletNode.current?.port.postMessage({ type: 'audio', data: pcmData });
101+
try {
102+
const blob = convertBase64ToBlob(message.data);
103+
const arrayBuffer = await blob.arrayBuffer();
104+
const audioBuffer =
105+
await audioContext.current.decodeAudioData(arrayBuffer);
112106

113-
setIsPlaying(true);
114-
onPlayAudio.current(message.id);
115-
} catch (e) {
116-
const eMessage = e instanceof Error ? e.message : 'Unknown error';
117-
onError.current(`Failed to add clip to queue: ${eMessage}`);
118-
}
119-
}, []);
107+
const pcmData = audioBuffer.getChannelData(0);
108+
109+
if (gainNode.current) {
110+
const now = audioContext.current.currentTime;
111+
gainNode.current.gain.cancelScheduledValues(now);
112+
const targetGain = isAudioMuted ? 0 : volume;
113+
gainNode.current.gain.setValueAtTime(targetGain, now);
114+
}
115+
116+
workletNode.current?.port.postMessage({ type: 'audio', data: pcmData });
120117

121-
const fadeOutAndPostStopMessage = async (type: 'end' | 'clear') => {
122-
const FADE_DURATION = 0.1;
118+
setIsPlaying(true);
119+
onPlayAudio.current(message.id);
120+
} catch (e) {
121+
const eMessage = e instanceof Error ? e.message : 'Unknown error';
122+
onError.current(`Failed to add clip to queue: ${eMessage}`);
123+
}
124+
},
125+
[isAudioMuted, volume],
126+
);
127+
128+
const waitForAudioTime = (
129+
targetTime: number,
130+
ctx: AudioContext,
131+
isCancelled: () => boolean,
132+
): Promise<void> =>
133+
new Promise((resolve) => {
134+
const check = () => {
135+
if (isCancelled()) return;
136+
137+
if (ctx.currentTime >= targetTime) {
138+
resolve();
139+
} else {
140+
requestAnimationFrame(check);
141+
}
142+
};
143+
check();
144+
});
145+
146+
const fadeOutAndPostMessage = useCallback(async (type: 'end' | 'clear') => {
123147
if (!gainNode.current || !audioContext.current) {
124148
workletNode.current?.port.postMessage({ type });
125149
return;
126150
}
127151

128152
const now = audioContext.current.currentTime;
129-
const FADE_TARGET = 0.0001;
130153

131154
gainNode.current.gain.cancelScheduledValues(now);
132155
gainNode.current.gain.setValueAtTime(gainNode.current.gain.value, now);
@@ -135,12 +158,26 @@ export const useSoundPlayer = (props: {
135158
now + FADE_DURATION,
136159
);
137160

138-
await new Promise((resolve) => setTimeout(resolve, FADE_DURATION * 1000));
161+
isFadeCancelled.current = false;
162+
await waitForAudioTime(
163+
now + FADE_DURATION,
164+
audioContext.current,
165+
() => isFadeCancelled.current,
166+
);
139167

140168
workletNode.current?.port.postMessage({ type });
141169

142-
gainNode.current.gain.setValueAtTime(1.0, audioContext.current.currentTime);
143-
};
170+
gainNode.current?.gain.setValueAtTime(
171+
1.0,
172+
audioContext.current?.currentTime || 0,
173+
);
174+
}, []);
175+
176+
useEffect(() => {
177+
return () => {
178+
isFadeCancelled.current = true;
179+
};
180+
}, []);
144181

145182
const stopAll = useCallback(async () => {
146183
isInitialized.current = false;
@@ -153,7 +190,7 @@ export const useSoundPlayer = (props: {
153190
window.clearInterval(frequencyDataIntervalId.current);
154191
}
155192

156-
await fadeOutAndPostStopMessage('end');
193+
await fadeOutAndPostMessage('end');
157194

158195
if (analyserNode.current) {
159196
analyserNode.current.disconnect();
@@ -181,14 +218,14 @@ export const useSoundPlayer = (props: {
181218
}
182219

183220
setFft(generateEmptyFft());
184-
}, []);
221+
}, [fadeOutAndPostMessage]);
185222

186223
const clearQueue = useCallback(() => {
187-
void fadeOutAndPostStopMessage('clear');
224+
void fadeOutAndPostMessage('clear');
188225
isProcessing.current = false;
189226
setIsPlaying(false);
190227
setFft(generateEmptyFft());
191-
}, []);
228+
}, [fadeOutAndPostMessage]);
192229

193230
const setVolume = useCallback(
194231
(newLevel: number) => {

0 commit comments

Comments
 (0)