Skip to content

Commit 6e952a8

Browse files
authored
Enable short fadeout when stopping playback to prevent artifacts (#314)
1 parent 4c13a0a commit 6e952a8

File tree

5 files changed

+64
-18
lines changed

5 files changed

+64
-18
lines changed

packages/embed-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@humeai/voice-embed-react",
3-
"version": "0.2.0-beta.2",
3+
"version": "0.2.0-beta.3",
44
"description": "",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

packages/embed/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@humeai/voice-embed",
3-
"version": "0.2.0-beta.2",
3+
"version": "0.2.0-beta.3",
44
"description": "",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

packages/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@humeai/voice-react",
3-
"version": "0.2.0-beta.2",
3+
"version": "0.2.0-beta.3",
44
"description": "",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

packages/react/src/lib/useSoundPlayer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const useSoundPlayer = (props: {
5252

5353
await initAudioContext.audioWorklet
5454
.addModule(
55-
'https://storage.googleapis.com/evi-react-sdk-assets/audio-worklet.js',
55+
'https://storage.googleapis.com/evi-react-sdk-assets/audio-worklet-20250506.js',
5656
)
5757
.catch((e) => {
5858
console.log(e);
@@ -100,7 +100,6 @@ export const useSoundPlayer = (props: {
100100
await audioContext.current.decodeAudioData(arrayBuffer);
101101

102102
const pcmData = audioBuffer.getChannelData(0);
103-
104103
workletNode.current?.port.postMessage({ type: 'audio', data: pcmData });
105104

106105
setIsPlaying(true);
@@ -122,6 +121,9 @@ export const useSoundPlayer = (props: {
122121
window.clearInterval(frequencyDataIntervalId.current);
123122
}
124123

124+
workletNode.current?.port.postMessage({ type: 'fadeAndClear' });
125+
workletNode.current?.port.postMessage({ type: 'end' });
126+
125127
if (analyserNode.current) {
126128
analyserNode.current.disconnect();
127129
analyserNode.current = null;
@@ -142,7 +144,6 @@ export const useSoundPlayer = (props: {
142144
}
143145

144146
if (workletNode.current) {
145-
workletNode.current.port.postMessage({ type: 'end' });
146147
workletNode.current.port.close();
147148
workletNode.current.disconnect();
148149
workletNode.current = null;
@@ -152,7 +153,10 @@ export const useSoundPlayer = (props: {
152153
}, []);
153154

154155
const clearQueue = useCallback(() => {
155-
workletNode.current?.port.postMessage({ type: 'clear' });
156+
workletNode.current?.port.postMessage({
157+
type: 'fadeAndClear',
158+
});
159+
156160
isProcessing.current = false;
157161
setIsPlaying(false);
158162
setFft(generateEmptyFft());

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

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,43 +81,85 @@ class AudioStreamProcessor extends AudioWorkletProcessor {
8181
switch (e.data?.type) {
8282
case 'audio':
8383
this._bq.push(new Float32Array(e.data.data));
84+
if (this._fadeOutActive) {
85+
this._fadeOutActive = false;
86+
this._fadeOutCounter = 0;
87+
}
8488
break;
8589
case 'end':
8690
this._shouldStop = true;
8791
break;
92+
case 'fadeAndClear':
93+
this._fadeOutActive = true;
94+
this._fadeOutCounter = 0;
95+
break;
8896
case 'clear':
8997
this._bq.clear();
9098
this._shouldStop = false;
9199
break;
92100
}
93101
};
94102
this._shouldStop = false;
103+
104+
this._fadeOutDurationMs = 30;
105+
// sampleRate is part of AudioWorkletGlobalScope
106+
// eslint-disable-next-line no-undef
107+
this._sampleRate = sampleRate;
108+
this._fadeOutSamplesCount = Math.floor(
109+
(this._fadeOutDurationMs * this._sampleRate) / 1000,
110+
);
111+
this._fadeOutActive = false;
112+
this._fadeOutCounter = 0;
95113
}
96114

97115
process(inputs, outputs) {
98116
const output = outputs[0];
99117
const frames = output[0].length;
100-
const chans = output.length;
118+
const channels = output.length;
101119

102120
const block = this._bq.read();
103121

104122
if (block) {
105-
for (let ch = 0; ch < chans; ch++) {
123+
for (let ch = 0; ch < channels; ch++) {
106124
const out = output[ch];
107125
for (let i = 0; i < frames; i++) {
108-
out[i] = block[i * chans + ch];
126+
let sample = block[i * channels + ch] ?? 0;
127+
128+
// Apply fade out if active
129+
if (this._fadeOutActive) {
130+
const fadeProgress =
131+
this._fadeOutCounter / this._fadeOutSamplesCount;
132+
const gain = 1 - Math.min(fadeProgress, 1);
133+
sample *= gain;
134+
}
135+
136+
out[i] = sample;
109137
}
110138
}
111-
} else {
112-
if (this._shouldStop) {
113-
// Stop worklet once we've finished playback
114-
this.port.postMessage({ type: 'ended' });
115-
return false;
116-
}
117139

118-
for (let ch = 0; ch < chans; ch++) {
119-
output[ch].fill(0);
140+
// If we're currently fading out,
141+
// increment the counter and end if complete
142+
if (this._fadeOutActive) {
143+
this._fadeOutCounter += frames;
144+
145+
if (this._fadeOutCounter >= this._fadeOutSamplesCount) {
146+
this._fadeOutActive = false;
147+
this._fadeOutCounter = 0;
148+
this._bq.clear();
149+
this.port.postMessage({ type: 'ended' });
150+
}
120151
}
152+
153+
return true;
154+
}
155+
156+
if (this._shouldStop) {
157+
this.port.postMessage({ type: 'ended' });
158+
return false;
159+
}
160+
161+
for (let ch = 0; ch < channels; ch++) {
162+
output[ch].fill(0);
121163
}
122164

123165
return true;

0 commit comments

Comments
 (0)