[Web] Fix audio issues with samples and GodotPositionReportingProcessor

This commit is contained in:
Adam Scott
2025-01-27 10:23:28 -05:00
parent 71d80b26a4
commit c558c8a5f1
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. */
/**************************************************************************/
const POST_THRESHOLD_S = 0.1;
class GodotPositionReportingProcessor extends AudioWorkletProcessor {
constructor() {
super();
constructor(...args) {
super(...args);
this.lastPostTime = currentTime;
this.position = 0;
this.port.onmessage = (event) => {
if (event?.data?.type === 'clear') {
this.lastPostTime = currentTime;
this.position = 0;
}
};
}
process(inputs, _outputs, _parameters) {
@ -39,10 +49,15 @@ class GodotPositionReportingProcessor extends AudioWorkletProcessor {
const input = inputs[0];
if (input.length > 0) {
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;
}
}

View File

@ -460,7 +460,11 @@ class SampleNode {
const sampleNodeBus = this.getSampleNodeBus(bus);
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
* If the worklet module is not loaded in, it will be added
*/
connectPositionWorklet(start) {
try {
this._positionWorklet = this.createPositionWorklet();
this._source.connect(this._positionWorklet);
if (start) {
this.start();
}
} catch (error) {
if (error?.name !== 'InvalidStateError') {
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);
});
async connectPositionWorklet(start) {
await GodotAudio.audioPositionWorkletPromise;
if (this.isCanceled) {
return;
}
this.getPositionWorklet();
this._source.connect(this._positionWorklet);
if (start) {
this.start();
}
}
/**
* Creates the AudioWorkletProcessor used to track playback position.
* @returns {AudioWorkletNode}
* Get a AudioWorkletProcessor from the pool, or create one if no processor is available.
*/
createPositionWorklet() {
const worklet = new AudioWorkletNode(
GodotAudio.ctx,
'godot-position-reporting-processor'
);
worklet.port.onmessage = (event) => {
getPositionWorklet() {
if (this._positionWorklet != null) {
return;
}
if (GodotAudio.audioPositionWorkletPool.length == 0) {
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']) {
case 'position':
this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
@ -658,7 +652,6 @@ class SampleNode {
// Do nothing.
}
};
return worklet;
}
/**
@ -688,6 +681,8 @@ class SampleNode {
if (this._positionWorklet) {
this._positionWorklet.disconnect();
this._positionWorklet.port.onmessage = null;
this._positionWorklet.port.postMessage({ type: 'clear' });
GodotAudio.audioPositionWorkletPool.push(this._positionWorklet);
this._positionWorklet = null;
}
@ -731,7 +726,10 @@ class SampleNode {
const pauseTime = this.isPaused
? this.pauseTime
: 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.isStarted = true;
}
@ -1199,6 +1197,10 @@ const _GodotAudio = {
driver: null,
interval: 0,
/** @type {Promise} */
audioPositionWorkletPromise: null,
audioPositionWorkletPool: [],
/**
* Converts linear volume to Db.
* @param {number} linear Linear value to convert.
@ -1265,6 +1267,10 @@ const _GodotAudio = {
onlatencyupdate(computed_latency);
}, 1000);
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;
},