Skip to content

Commit ee3b31d

Browse files
committed
Merge pull request #93362 from adamscott/fix-web-audio-pause
Fix pausing issues when using Web Audio samples
2 parents 4a9dc72 + 57db018 commit ee3b31d

File tree

1 file changed

+117
-59
lines changed

1 file changed

+117
-59
lines changed

platform/web/js/libs/library_godot_audio.js

Lines changed: 117 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -423,68 +423,38 @@ class SampleNode {
423423
this.streamObjectId = params.streamObjectId;
424424
/** @type {number} */
425425
this.offset = options.offset ?? 0;
426-
/** @type {LoopMode} */
426+
/** @type {number} */
427427
this.startTime = options.startTime ?? 0;
428+
/** @type {boolean} */
429+
this.isPaused = false;
428430
/** @type {number} */
429431
this.pauseTime = 0;
430432
/** @type {number} */
431433
this._playbackRate = 44100;
432434
/** @type {LoopMode} */
433-
this._loopMode = 'disabled';
435+
this.loopMode = 'disabled';
434436
/** @type {number} */
435437
this._pitchScale = 1;
438+
/** @type {number} */
439+
this._sourceStartTime = 0;
436440
/** @type {Map<Bus, SampleNodeBus>} */
437441
this._sampleNodeBuses = new Map();
438-
/** @type {AudioBufferSourceNode} */
442+
/** @type {AudioBufferSourceNode | null} */
439443
this._source = GodotAudio.ctx.createBufferSource();
444+
/** @type {AudioBufferSourceNode["onended"]} */
445+
this._onended = null;
440446

441447
this.setPlaybackRate(options.playbackRate ?? 44100);
442-
this.setLoopMode(options.loopMode ?? this.getSample().loopMode ?? 'disabled');
448+
this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';
443449
this._source.buffer = this.getSample().getAudioBuffer();
444450

445-
/** @type {SampleNode} */
446-
// eslint-disable-next-line consistent-this
447-
const self = this;
448-
this._source.addEventListener('ended', (_) => {
449-
switch (self.getSample().loopMode) {
450-
case 'disabled':
451-
GodotAudio.SampleNode.stopSampleNode(self.id);
452-
break;
453-
default:
454-
// do nothing
455-
}
456-
});
451+
this._addEndedListener();
457452

458453
const bus = GodotAudio.Bus.getBus(params.busIndex);
459454
const sampleNodeBus = this.getSampleNodeBus(bus);
460455
sampleNodeBus.setVolume(options.volume);
461456
}
462457

463-
/**
464-
* Gets the loop mode of the current instance.
465-
* @returns {LoopMode}
466-
*/
467-
getLoopMode() {
468-
return this._loopMode;
469-
}
470-
471-
/**
472-
* Sets the loop mode of the current instance.
473-
* @param {LoopMode} val Value to set.
474-
* @returns {void}
475-
*/
476-
setLoopMode(val) {
477-
this._loopMode = val;
478-
switch (val) {
479-
case 'forward':
480-
case 'backward':
481-
this._source.loop = true;
482-
break;
483-
default:
484-
this._source.loop = false;
485-
}
486-
}
487-
488458
/**
489459
* Gets the playback rate.
490460
* @returns {number}
@@ -542,40 +512,40 @@ class SampleNode {
542512
* @returns {void}
543513
*/
544514
start() {
545-
this._source.start(this.offset);
515+
this._resetSourceStartTime();
516+
this._source.start(this.startTime, this.offset);
546517
}
547518

548519
/**
549520
* Stops the `SampleNode`.
550521
* @returns {void}
551522
*/
552523
stop() {
553-
this._source.stop();
554524
this.clear();
555525
}
556526

527+
/**
528+
* Restarts the `SampleNode`.
529+
*/
530+
restart() {
531+
this.isPaused = false;
532+
this.pauseTime = 0;
533+
this._resetSourceStartTime();
534+
this._restart();
535+
}
536+
557537
/**
558538
* Pauses the `SampleNode`.
559539
* @param {boolean} [enable=true] State of the pause.
560540
* @returns {void}
561541
*/
562542
pause(enable = true) {
563543
if (enable) {
564-
this.pauseTime = (GodotAudio.ctx.currentTime - this.startTime) / this.playbackRate;
565-
this._source.stop();
566-
return;
567-
}
568-
569-
if (this.pauseTime === 0) {
544+
this._pause();
570545
return;
571546
}
572547

573-
this._source.disconnect();
574-
this._source = GodotAudio.ctx.createBufferSource();
575-
576-
this._source.buffer = this.getSample().getAudioBuffer();
577-
this._source.connect(this._gain);
578-
this._source.start(this.offset + this.pauseTime);
548+
this._unpause();
579549
}
580550

581551
/**
@@ -623,26 +593,114 @@ class SampleNode {
623593
* @returns {void}
624594
*/
625595
clear() {
626-
this._source.stop();
627-
this._source.disconnect();
628-
this._source = null;
596+
this.isPaused = false;
597+
this.pauseTime = 0;
598+
599+
if (this._source != null) {
600+
this._source.removeEventListener('ended', this._onended);
601+
this._onended = null;
602+
this._source.stop();
603+
this._source.disconnect();
604+
this._source = null;
605+
}
629606

630607
for (const sampleNodeBus of this._sampleNodeBuses.values()) {
631608
sampleNodeBus.clear();
632609
}
633610
this._sampleNodeBuses.clear();
634-
this._sampleNodeBuses = null;
635611

636612
GodotAudio.SampleNode.delete(this.id);
637613
}
638614

615+
/**
616+
* Resets the source start time
617+
* @returns {void}
618+
*/
619+
_resetSourceStartTime() {
620+
this._sourceStartTime = GodotAudio.ctx.currentTime;
621+
}
622+
639623
/**
640624
* Syncs the `AudioNode` playback rate based on the `SampleNode` playback rate and pitch scale.
641625
* @returns {void}
642626
*/
643627
_syncPlaybackRate() {
644628
this._source.playbackRate.value = this.getPlaybackRate() * this.getPitchScale();
645629
}
630+
631+
/**
632+
* Restarts the `SampleNode`.
633+
* Honors `isPaused` and `pauseTime`.
634+
* @returns {void}
635+
*/
636+
_restart() {
637+
this._source.disconnect();
638+
this._source = GodotAudio.ctx.createBufferSource();
639+
this._source.buffer = this.getSample().getAudioBuffer();
640+
641+
// Make sure that we connect the new source to the sample node bus.
642+
for (const sampleNodeBus of this._sampleNodeBuses.values()) {
643+
this.connect(sampleNodeBus.getInputNode());
644+
}
645+
646+
this._addEndedListener();
647+
const pauseTime = this.isPaused
648+
? this.pauseTime
649+
: 0;
650+
this._source.start(this.startTime, this.offset + pauseTime);
651+
}
652+
653+
/**
654+
* Pauses the `SampleNode`.
655+
* @returns {void}
656+
*/
657+
_pause() {
658+
this.isPaused = true;
659+
this.pauseTime = (GodotAudio.ctx.currentTime - this._sourceStartTime) / this.getPlaybackRate();
660+
this._source.stop();
661+
}
662+
663+
/**
664+
* Unpauses the `SampleNode`.
665+
* @returns {void}
666+
*/
667+
_unpause() {
668+
this._restart();
669+
this.isPaused = false;
670+
this.pauseTime = 0;
671+
}
672+
673+
/**
674+
* Adds an "ended" listener to the source node to repeat it if necessary.
675+
* @returns {void}
676+
*/
677+
_addEndedListener() {
678+
if (this._onended != null) {
679+
this._source.removeEventListener('ended', this._onended);
680+
}
681+
682+
/** @type {SampleNode} */
683+
// eslint-disable-next-line consistent-this
684+
const self = this;
685+
this._onended = (_) => {
686+
if (self.isPaused) {
687+
return;
688+
}
689+
690+
switch (self.getSample().loopMode) {
691+
case 'disabled':
692+
self.stop();
693+
break;
694+
case 'forward':
695+
case 'backward':
696+
self.restart();
697+
break;
698+
default:
699+
// do nothing
700+
}
701+
};
702+
this._source.addEventListener('ended', this._onended);
703+
}
646704
}
647705

648706
/**

0 commit comments

Comments
 (0)