Merge pull request #102163 from adamscott/fix-glitched-audio-web

[Web] Fix audio issues with samples and GodotPositionReportingProcessor
This commit is contained in:
Thaddeus Crews
2025-01-31 09:15:49 -06:00
2 changed files with 61 additions and 40 deletions

View File

@ -28,10 +28,20 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/ /**************************************************************************/
const POST_THRESHOLD_S = 0.1;
class GodotPositionReportingProcessor extends AudioWorkletProcessor { class GodotPositionReportingProcessor extends AudioWorkletProcessor {
constructor() { constructor(...args) {
super(); super(...args);
this.lastPostTime = currentTime;
this.position = 0; this.position = 0;
this.port.onmessage = (event) => {
if (event?.data?.type === 'clear') {
this.lastPostTime = currentTime;
this.position = 0;
}
};
} }
process(inputs, _outputs, _parameters) { process(inputs, _outputs, _parameters) {
@ -39,10 +49,15 @@ class GodotPositionReportingProcessor extends AudioWorkletProcessor {
const input = inputs[0]; const input = inputs[0];
if (input.length > 0) { if (input.length > 0) {
this.position += input[0].length; this.position += input[0].length;
this.port.postMessage({ 'type': 'position', 'data': this.position });
return true;
} }
} }
// Posting messages is expensive. Let's limit the number of posts.
if (currentTime - this.lastPostTime > POST_THRESHOLD_S) {
this.lastPostTime = currentTime;
this.port.postMessage({ 'type': 'position', 'data': this.position });
}
return true; return true;
} }
} }

View File

@ -460,7 +460,11 @@ class SampleNode {
const sampleNodeBus = this.getSampleNodeBus(bus); const sampleNodeBus = this.getSampleNodeBus(bus);
sampleNodeBus.setVolume(options.volume); sampleNodeBus.setVolume(options.volume);
this.connectPositionWorklet(options.start); this.connectPositionWorklet(options.start).catch((err) => {
const newErr = new Error('Failed to create PositionWorklet.');
newErr.cause = err;
GodotRuntime.error(newErr);
});
} }
/** /**
@ -612,44 +616,34 @@ class SampleNode {
* Sets up and connects the source to the GodotPositionReportingProcessor * Sets up and connects the source to the GodotPositionReportingProcessor
* If the worklet module is not loaded in, it will be added * If the worklet module is not loaded in, it will be added
*/ */
connectPositionWorklet(start) { async connectPositionWorklet(start) {
try { await GodotAudio.audioPositionWorkletPromise;
this._positionWorklet = this.createPositionWorklet(); if (this.isCanceled) {
this._source.connect(this._positionWorklet); return;
if (start) { }
this.start(); this.getPositionWorklet();
} this._source.connect(this._positionWorklet);
} catch (error) { if (start) {
if (error?.name !== 'InvalidStateError') { this.start();
throw error;
}
const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
GodotAudio.ctx.audioWorklet
.addModule(path)
.then(() => {
if (!this.isCanceled) {
this._positionWorklet = this.createPositionWorklet();
this._source.connect(this._positionWorklet);
if (start) {
this.start();
}
}
}).catch((addModuleError) => {
GodotRuntime.error('Failed to create PositionWorklet.', addModuleError);
});
} }
} }
/** /**
* Creates the AudioWorkletProcessor used to track playback position. * Get a AudioWorkletProcessor from the pool, or create one if no processor is available.
* @returns {AudioWorkletNode}
*/ */
createPositionWorklet() { getPositionWorklet() {
const worklet = new AudioWorkletNode( if (this._positionWorklet != null) {
GodotAudio.ctx, return;
'godot-position-reporting-processor' }
); if (GodotAudio.audioPositionWorkletPool.length == 0) {
worklet.port.onmessage = (event) => { this._positionWorklet = new AudioWorkletNode(
GodotAudio.ctx,
'godot-position-reporting-processor'
);
} else {
this._positionWorklet = GodotAudio.audioPositionWorkletPool.pop();
}
this._positionWorklet.port.onmessage = (event) => {
switch (event.data['type']) { switch (event.data['type']) {
case 'position': case 'position':
this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset; this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
@ -658,7 +652,6 @@ class SampleNode {
// Do nothing. // Do nothing.
} }
}; };
return worklet;
} }
/** /**
@ -688,6 +681,8 @@ class SampleNode {
if (this._positionWorklet) { if (this._positionWorklet) {
this._positionWorklet.disconnect(); this._positionWorklet.disconnect();
this._positionWorklet.port.onmessage = null; this._positionWorklet.port.onmessage = null;
this._positionWorklet.port.postMessage({ type: 'clear' });
GodotAudio.audioPositionWorkletPool.push(this._positionWorklet);
this._positionWorklet = null; this._positionWorklet = null;
} }
@ -731,7 +726,10 @@ class SampleNode {
const pauseTime = this.isPaused const pauseTime = this.isPaused
? this.pauseTime ? this.pauseTime
: 0; : 0;
this.connectPositionWorklet(); if (this._positionWorklet != null) {
this._positionWorklet.port.postMessage({ type: 'clear' });
this._source.connect(this._positionWorklet);
}
this._source.start(this.startTime, this.offset + pauseTime); this._source.start(this.startTime, this.offset + pauseTime);
this.isStarted = true; this.isStarted = true;
} }
@ -1199,6 +1197,10 @@ const _GodotAudio = {
driver: null, driver: null,
interval: 0, interval: 0,
/** @type {Promise} */
audioPositionWorkletPromise: null,
audioPositionWorkletPool: [],
/** /**
* Converts linear volume to Db. * Converts linear volume to Db.
* @param {number} linear Linear value to convert. * @param {number} linear Linear value to convert.
@ -1265,6 +1267,10 @@ const _GodotAudio = {
onlatencyupdate(computed_latency); onlatencyupdate(computed_latency);
}, 1000); }, 1000);
GodotOS.atexit(GodotAudio.close_async); GodotOS.atexit(GodotAudio.close_async);
const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
GodotAudio.audioPositionWorkletPromise = ctx.audioWorklet.addModule(path);
return ctx.destination.channelCount; return ctx.destination.channelCount;
}, },