Skip to content

Commit 48546a2

Browse files
committed
set fade in worklet
1 parent 5a8be70 commit 48546a2

File tree

4 files changed

+73
-54
lines changed

4 files changed

+73
-54
lines changed

examples/next-app/components/Voice.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,8 @@ import { ExampleComponent } from '@/components/ExampleComponent';
99
export const Voice = ({ accessToken }: { accessToken: string }) => {
1010
return (
1111
<VoiceProvider
12-
auth={{
13-
type: 'apiKey',
14-
value: 'JesgJYUNkFnqnEDzPSeGXjWMt3Zae2q7o7H1GBqn7A1AOJ2k',
15-
}}
16-
hostname={'test-api.hume.ai'}
17-
queryParams={{
18-
hume_uuid: 'a46eac19-af97-4658-b845-57120c695bef',
19-
}}
20-
configId={'889cff21-cfaf-4269-8e90-b5b9a5f7f92e'}
12+
auth={{ type: 'accessToken', value: accessToken }}
13+
hostname={process.env.NEXT_PUBLIC_HUME_VOICE_HOSTNAME || 'api.hume.ai'}
2114
messageHistoryLimit={10}
2215
onMessage={(message) => {
2316
// eslint-disable-next-line no-console
@@ -125,6 +118,7 @@ export const Voice = ({ accessToken }: { accessToken: string }) => {
125118
});
126119
}
127120
}, [])}
121+
configId={process.env.NEXT_PUBLIC_HUME_VOICE_WEATHER_CONFIG_ID}
128122
onClose={(event) => {
129123
const niceClosure = 1000;
130124
const code = event.code;

packages/react/src/lib/VoiceProvider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ export const VoiceProvider: FC<VoiceProviderProps> = ({
232232
if (player.isPlaying) {
233233
onInterruption.current(message);
234234
}
235-
player.clearQueue();
235+
player.clearQueue({
236+
fadeOut: message.type === 'user_interruption',
237+
});
236238
}
237239

238240
if (

packages/react/src/lib/useSoundPlayer.ts

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import { convertLinearFrequenciesToBark } from './convertFrequencyScale';
55
import { generateEmptyFft } from './generateEmptyFft';
66
import type { AudioOutputMessage } from '../models/messages';
77

8-
const FADE_DURATION = 0.05;
9-
const FADE_TARGET = 0.0001;
10-
118
export const useSoundPlayer = (props: {
129
onError: (message: string) => void;
1310
onPlayAudio: (id: string) => void;
@@ -111,7 +108,7 @@ export const useSoundPlayer = (props: {
111108
const now = audioContext.current.currentTime;
112109
gainNode.current.gain.cancelScheduledValues(now);
113110
gainNode.current.gain.setValueAtTime(
114-
volume,
111+
isAudioMuted ? 0 : volume,
115112
audioContext.current.currentTime,
116113
);
117114
}
@@ -126,28 +123,7 @@ export const useSoundPlayer = (props: {
126123
[isAudioMuted, volume],
127124
);
128125

129-
const fadeOutAndPostMessage = useCallback(async (type: 'end' | 'clear') => {
130-
if (!gainNode.current || !audioContext.current) {
131-
workletNode.current?.port.postMessage({ type });
132-
return;
133-
}
134-
135-
const now = audioContext.current.currentTime;
136-
gainNode.current.gain.cancelScheduledValues(now);
137-
gainNode.current.gain.setValueAtTime(gainNode.current.gain.value, now);
138-
gainNode.current.gain.exponentialRampToValueAtTime(
139-
FADE_TARGET,
140-
now + FADE_DURATION,
141-
);
142-
workletNode.current?.port.postMessage({ type });
143-
/*await new Promise((resolve) => {
144-
setTimeout(() => {
145-
resolve(null);
146-
}, FADE_DURATION * 1000);
147-
});*/
148-
}, []);
149-
150-
const stopAll = useCallback(async () => {
126+
const stopAll = useCallback(() => {
151127
isInitialized.current = false;
152128
isProcessing.current = false;
153129
setIsPlaying(false);
@@ -158,7 +134,8 @@ export const useSoundPlayer = (props: {
158134
window.clearInterval(frequencyDataIntervalId.current);
159135
}
160136

161-
await fadeOutAndPostMessage('end');
137+
workletNode.current?.port.postMessage({ type: 'fade' });
138+
workletNode.current?.port.postMessage({ type: 'end' });
162139

163140
if (analyserNode.current) {
164141
analyserNode.current.disconnect();
@@ -186,14 +163,23 @@ export const useSoundPlayer = (props: {
186163
}
187164

188165
setFft(generateEmptyFft());
189-
}, [fadeOutAndPostMessage]);
166+
}, []);
190167

191-
const clearQueue = useCallback(async () => {
192-
await fadeOutAndPostMessage('clear');
193-
isProcessing.current = false;
194-
setIsPlaying(false);
195-
setFft(generateEmptyFft());
196-
}, [fadeOutAndPostMessage]);
168+
const clearQueue = useCallback(
169+
({
170+
fadeOut,
171+
}: {
172+
fadeOut?: boolean;
173+
} = {}) => {
174+
if (fadeOut) workletNode.current?.port.postMessage({ type: 'fade' });
175+
workletNode.current?.port.postMessage({ type: 'clear' });
176+
177+
isProcessing.current = false;
178+
setIsPlaying(false);
179+
setFft(generateEmptyFft());
180+
},
181+
[],
182+
);
197183

198184
const setVolume = useCallback(
199185
(newLevel: number) => {

packages/react/src/worklets/audio-worklet.js

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ class BufferQueue {
33
this._length = 0;
44
this._buffers = [];
55
this._hasPushed = false;
6+
7+
// For fading out
8+
this._fadeOutDurationMs = 100;
9+
this._sampleRate = sampleRate;
10+
this._fadeOutSamplesCount = Math.floor(
11+
(this._fadeOutDurationMs * this._sampleRate) / 1000,
12+
);
13+
this._fadeOutActive = false;
14+
this._fadeOutCounter = 0;
615
}
716

817
push(buffer) {
@@ -85,8 +94,11 @@ class AudioStreamProcessor extends AudioWorkletProcessor {
8594
case 'end':
8695
this._shouldStop = true;
8796
break;
97+
case 'fade':
98+
this._bq._fadeOutActive = true;
99+
this._bq._fadeOutCounter = 0;
100+
break;
88101
case 'clear':
89-
this._bq.clear();
90102
this._shouldStop = false;
91103
break;
92104
}
@@ -105,19 +117,44 @@ class AudioStreamProcessor extends AudioWorkletProcessor {
105117
for (let ch = 0; ch < chans; ch++) {
106118
const out = output[ch];
107119
for (let i = 0; i < frames; i++) {
108-
out[i] = block[i * chans + ch];
120+
let sample = block[i * chans + ch] ?? 0;
121+
122+
// Apply automatic fade-out if active
123+
if (this._bq._fadeOutActive) {
124+
const fadeProgress =
125+
this._bq._fadeOutCounter / this._bq._fadeOutSamplesCount;
126+
const gain = 1 - Math.min(fadeProgress, 1);
127+
sample *= gain;
128+
}
129+
130+
out[i] = sample;
109131
}
110132
}
111-
} else {
112-
if (this._shouldStop) {
113-
// Stop worklet once we've finished playback
114-
this.port.postMessage({ type: 'ended' });
115-
return false;
116-
}
117133

118-
for (let ch = 0; ch < chans; ch++) {
119-
output[ch].fill(0);
134+
// If we're currently fading out, increment the counter and end if complete
135+
if (this._bq._fadeOutActive) {
136+
this._bq._fadeOutCounter += frames;
137+
138+
if (this._bq._fadeOutCounter >= this._bq._fadeOutSamplesCount) {
139+
this._bq._fadeOutActive = false;
140+
this._bq._fadeOutCounter = 0;
141+
this._bq.clear();
142+
this.port.postMessage({ type: 'ended' });
143+
}
120144
}
145+
146+
return true;
147+
}
148+
149+
if (this._shouldStop) {
150+
// Stop worklet once we've finished playback
151+
this.port.postMessage({ type: 'ended' });
152+
return false;
153+
}
154+
155+
// Fill output with silence during fade-out or between clips
156+
for (let ch = 0; ch < chans; ch++) {
157+
output[ch].fill(0);
121158
}
122159

123160
return true;

0 commit comments

Comments
 (0)