Skip to content

Commit a7e5469

Browse files
committed
Merge pull request godotengine#102163 from adamscott/fix-glitched-audio-web
[Web] Fix audio issues with samples and GodotPositionReportingProcessor
2 parents 3d6821b + c558c8a commit a7e5469

File tree

2 files changed

+61
-40
lines changed

2 files changed

+61
-40
lines changed

platform/web/js/libs/audio.position.worklet.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,36 @@
2828
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
2929
/**************************************************************************/
3030

31+
const POST_THRESHOLD_S = 0.1;
32+
3133
class GodotPositionReportingProcessor extends AudioWorkletProcessor {
32-
constructor() {
33-
super();
34+
constructor(...args) {
35+
super(...args);
36+
this.lastPostTime = currentTime;
3437
this.position = 0;
38+
39+
this.port.onmessage = (event) => {
40+
if (event?.data?.type === 'clear') {
41+
this.lastPostTime = currentTime;
42+
this.position = 0;
43+
}
44+
};
3545
}
3646

3747
process(inputs, _outputs, _parameters) {
3848
if (inputs.length > 0) {
3949
const input = inputs[0];
4050
if (input.length > 0) {
4151
this.position += input[0].length;
42-
this.port.postMessage({ 'type': 'position', 'data': this.position });
43-
return true;
4452
}
4553
}
54+
55+
// Posting messages is expensive. Let's limit the number of posts.
56+
if (currentTime - this.lastPostTime > POST_THRESHOLD_S) {
57+
this.lastPostTime = currentTime;
58+
this.port.postMessage({ 'type': 'position', 'data': this.position });
59+
}
60+
4661
return true;
4762
}
4863
}

platform/web/js/libs/library_godot_audio.js

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,11 @@ class SampleNode {
460460
const sampleNodeBus = this.getSampleNodeBus(bus);
461461
sampleNodeBus.setVolume(options.volume);
462462

463-
this.connectPositionWorklet(options.start);
463+
this.connectPositionWorklet(options.start).catch((err) => {
464+
const newErr = new Error('Failed to create PositionWorklet.');
465+
newErr.cause = err;
466+
GodotRuntime.error(newErr);
467+
});
464468
}
465469

466470
/**
@@ -612,44 +616,34 @@ class SampleNode {
612616
* Sets up and connects the source to the GodotPositionReportingProcessor
613617
* If the worklet module is not loaded in, it will be added
614618
*/
615-
connectPositionWorklet(start) {
616-
try {
617-
this._positionWorklet = this.createPositionWorklet();
618-
this._source.connect(this._positionWorklet);
619-
if (start) {
620-
this.start();
621-
}
622-
} catch (error) {
623-
if (error?.name !== 'InvalidStateError') {
624-
throw error;
625-
}
626-
const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
627-
GodotAudio.ctx.audioWorklet
628-
.addModule(path)
629-
.then(() => {
630-
if (!this.isCanceled) {
631-
this._positionWorklet = this.createPositionWorklet();
632-
this._source.connect(this._positionWorklet);
633-
if (start) {
634-
this.start();
635-
}
636-
}
637-
}).catch((addModuleError) => {
638-
GodotRuntime.error('Failed to create PositionWorklet.', addModuleError);
639-
});
619+
async connectPositionWorklet(start) {
620+
await GodotAudio.audioPositionWorkletPromise;
621+
if (this.isCanceled) {
622+
return;
623+
}
624+
this.getPositionWorklet();
625+
this._source.connect(this._positionWorklet);
626+
if (start) {
627+
this.start();
640628
}
641629
}
642630

643631
/**
644-
* Creates the AudioWorkletProcessor used to track playback position.
645-
* @returns {AudioWorkletNode}
632+
* Get a AudioWorkletProcessor from the pool, or create one if no processor is available.
646633
*/
647-
createPositionWorklet() {
648-
const worklet = new AudioWorkletNode(
649-
GodotAudio.ctx,
650-
'godot-position-reporting-processor'
651-
);
652-
worklet.port.onmessage = (event) => {
634+
getPositionWorklet() {
635+
if (this._positionWorklet != null) {
636+
return;
637+
}
638+
if (GodotAudio.audioPositionWorkletPool.length == 0) {
639+
this._positionWorklet = new AudioWorkletNode(
640+
GodotAudio.ctx,
641+
'godot-position-reporting-processor'
642+
);
643+
} else {
644+
this._positionWorklet = GodotAudio.audioPositionWorkletPool.pop();
645+
}
646+
this._positionWorklet.port.onmessage = (event) => {
653647
switch (event.data['type']) {
654648
case 'position':
655649
this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
@@ -658,7 +652,6 @@ class SampleNode {
658652
// Do nothing.
659653
}
660654
};
661-
return worklet;
662655
}
663656

664657
/**
@@ -688,6 +681,8 @@ class SampleNode {
688681
if (this._positionWorklet) {
689682
this._positionWorklet.disconnect();
690683
this._positionWorklet.port.onmessage = null;
684+
this._positionWorklet.port.postMessage({ type: 'clear' });
685+
GodotAudio.audioPositionWorkletPool.push(this._positionWorklet);
691686
this._positionWorklet = null;
692687
}
693688

@@ -731,7 +726,10 @@ class SampleNode {
731726
const pauseTime = this.isPaused
732727
? this.pauseTime
733728
: 0;
734-
this.connectPositionWorklet();
729+
if (this._positionWorklet != null) {
730+
this._positionWorklet.port.postMessage({ type: 'clear' });
731+
this._source.connect(this._positionWorklet);
732+
}
735733
this._source.start(this.startTime, this.offset + pauseTime);
736734
this.isStarted = true;
737735
}
@@ -1199,6 +1197,10 @@ const _GodotAudio = {
11991197
driver: null,
12001198
interval: 0,
12011199

1200+
/** @type {Promise} */
1201+
audioPositionWorkletPromise: null,
1202+
audioPositionWorkletPool: [],
1203+
12021204
/**
12031205
* Converts linear volume to Db.
12041206
* @param {number} linear Linear value to convert.
@@ -1265,6 +1267,10 @@ const _GodotAudio = {
12651267
onlatencyupdate(computed_latency);
12661268
}, 1000);
12671269
GodotOS.atexit(GodotAudio.close_async);
1270+
1271+
const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
1272+
GodotAudio.audioPositionWorkletPromise = ctx.audioWorklet.addModule(path);
1273+
12681274
return ctx.destination.channelCount;
12691275
},
12701276

0 commit comments

Comments
 (0)