playcanvas
Version:
PlayCanvas WebGL game engine
603 lines (600 loc) • 16.9 kB
JavaScript
import { EventHandler } from '../../core/event-handler.js';
import { math } from '../../core/math/math.js';
import { hasAudioContext } from './capabilities.js';
const STATE_PLAYING = 0;
const STATE_PAUSED = 1;
const STATE_STOPPED = 2;
function capTime(time, duration) {
return time % duration || 0;
}
class SoundInstance extends EventHandler {
static{
this.EVENT_PLAY = 'play';
}
static{
this.EVENT_PAUSE = 'pause';
}
static{
this.EVENT_RESUME = 'resume';
}
static{
this.EVENT_STOP = 'stop';
}
static{
this.EVENT_END = 'end';
}
constructor(manager, sound, options){
super(), this.source = null;
this._manager = manager;
this._volume = options.volume !== undefined ? math.clamp(Number(options.volume) || 0, 0, 1) : 1;
this._pitch = options.pitch !== undefined ? Math.max(0.01, Number(options.pitch) || 0) : 1;
this._loop = !!(options.loop !== undefined ? options.loop : false);
this._sound = sound;
this._state = STATE_STOPPED;
this._suspended = false;
this._suspendEndEvent = 0;
this._suspendInstanceEvents = false;
this._playWhenLoaded = true;
this._startTime = Math.max(0, Number(options.startTime) || 0);
this._duration = Math.max(0, Number(options.duration) || 0);
this._startOffset = null;
this._onPlayCallback = options.onPlay;
this._onPauseCallback = options.onPause;
this._onResumeCallback = options.onResume;
this._onStopCallback = options.onStop;
this._onEndCallback = options.onEnd;
if (hasAudioContext()) {
this._startedAt = 0;
this._currentTime = 0;
this._currentOffset = 0;
this._inputNode = null;
this._connectorNode = null;
this._firstNode = null;
this._lastNode = null;
this._waitingContextSuspension = false;
this._initializeNodes();
this._endedHandler = this._onEnded.bind(this);
} else {
this._isReady = false;
this._loadedMetadataHandler = this._onLoadedMetadata.bind(this);
this._timeUpdateHandler = this._onTimeUpdate.bind(this);
this._endedHandler = this._onEnded.bind(this);
this._createSource();
}
}
set currentTime(value) {
if (value < 0) return;
if (this._state === STATE_PLAYING) {
const suspend = this._suspendInstanceEvents;
this._suspendInstanceEvents = true;
this.stop();
this._startOffset = value;
this.play();
this._suspendInstanceEvents = suspend;
} else {
this._startOffset = value;
this._currentTime = value;
}
}
get currentTime() {
if (this._startOffset !== null) {
return this._startOffset;
}
if (this._state === STATE_PAUSED) {
return this._currentTime;
}
if (this._state === STATE_STOPPED || !this.source) {
return 0;
}
this._updateCurrentTime();
return this._currentTime;
}
set duration(value) {
this._duration = Math.max(0, Number(value) || 0);
const isPlaying = this._state === STATE_PLAYING;
this.stop();
if (isPlaying) {
this.play();
}
}
get duration() {
if (!this._sound) {
return 0;
}
if (this._duration) {
return capTime(this._duration, this._sound.duration);
}
return this._sound.duration;
}
get isPaused() {
return this._state === STATE_PAUSED;
}
get isPlaying() {
return this._state === STATE_PLAYING;
}
get isStopped() {
return this._state === STATE_STOPPED;
}
get isSuspended() {
return this._suspended;
}
set loop(value) {
this._loop = !!value;
if (this.source) {
this.source.loop = this._loop;
}
}
get loop() {
return this._loop;
}
set pitch(pitch) {
this._currentOffset = this.currentTime;
this._startedAt = this._manager.context.currentTime;
this._pitch = Math.max(Number(pitch) || 0, 0.01);
if (this.source) {
this.source.playbackRate.value = this._pitch;
}
}
get pitch() {
return this._pitch;
}
set sound(value) {
this._sound = value;
if (this._state !== STATE_STOPPED) {
this.stop();
} else {
this._createSource();
}
}
get sound() {
return this._sound;
}
set startTime(value) {
this._startTime = Math.max(0, Number(value) || 0);
const isPlaying = this._state === STATE_PLAYING;
this.stop();
if (isPlaying) {
this.play();
}
}
get startTime() {
return this._startTime;
}
set volume(volume) {
volume = math.clamp(volume, 0, 1);
this._volume = volume;
if (this.gain) {
this.gain.gain.value = volume * this._manager.volume;
}
}
get volume() {
return this._volume;
}
_onPlay() {
this.fire('play');
if (this._onPlayCallback) {
this._onPlayCallback(this);
}
}
_onPause() {
this.fire('pause');
if (this._onPauseCallback) {
this._onPauseCallback(this);
}
}
_onResume() {
this.fire('resume');
if (this._onResumeCallback) {
this._onResumeCallback(this);
}
}
_onStop() {
this.fire('stop');
if (this._onStopCallback) {
this._onStopCallback(this);
}
}
_onEnded() {
if (this._suspendEndEvent > 0) {
this._suspendEndEvent--;
return;
}
this.fire('end');
if (this._onEndCallback) {
this._onEndCallback(this);
}
this.stop();
}
_onManagerVolumeChange() {
this.volume = this._volume;
}
_onManagerSuspend() {
if (this._state === STATE_PLAYING && !this._suspended) {
this._suspended = true;
this.pause();
}
}
_onManagerResume() {
if (this._suspended) {
this._suspended = false;
this.resume();
}
}
_initializeNodes() {
this.gain = this._manager.context.createGain();
this._inputNode = this.gain;
this._connectorNode = this.gain;
this._connectorNode.connect(this._manager.context.destination);
}
play() {
if (this._state !== STATE_STOPPED) {
this.stop();
}
this._state = STATE_PLAYING;
this._playWhenLoaded = false;
if (this._waitingContextSuspension) {
return false;
}
if (this._manager.suspended) {
this._manager.once('resume', this._playAudioImmediate, this);
this._waitingContextSuspension = true;
return false;
}
this._playAudioImmediate();
return true;
}
_playAudioImmediate() {
this._waitingContextSuspension = false;
if (this._state !== STATE_PLAYING) {
return;
}
if (!this.source) {
this._createSource();
}
let offset = capTime(this._startOffset, this.duration);
offset = capTime(this._startTime + offset, this._sound.duration);
this._startOffset = null;
if (this._duration) {
this.source.start(0, offset, this._duration);
} else {
this.source.start(0, offset);
}
this._startedAt = this._manager.context.currentTime;
this._currentTime = 0;
this._currentOffset = offset;
this.volume = this._volume;
this.loop = this._loop;
this.pitch = this._pitch;
this._manager.on('volumechange', this._onManagerVolumeChange, this);
this._manager.on('suspend', this._onManagerSuspend, this);
this._manager.on('resume', this._onManagerResume, this);
this._manager.on('destroy', this._onManagerDestroy, this);
if (!this._suspendInstanceEvents) {
this._onPlay();
}
}
pause() {
this._playWhenLoaded = false;
if (this._state !== STATE_PLAYING) {
return false;
}
this._state = STATE_PAUSED;
if (this._waitingContextSuspension) {
return true;
}
this._updateCurrentTime();
this._suspendEndEvent++;
this.source.stop(0);
this.source = null;
this._startOffset = null;
if (!this._suspendInstanceEvents) {
this._onPause();
}
return true;
}
resume() {
if (this._state !== STATE_PAUSED) {
return false;
}
let offset = this.currentTime;
this._state = STATE_PLAYING;
if (this._waitingContextSuspension) {
return true;
}
if (!this.source) {
this._createSource();
}
if (this._startOffset !== null) {
offset = capTime(this._startOffset, this.duration);
offset = capTime(this._startTime + offset, this._sound.duration);
this._startOffset = null;
}
if (this._duration) {
this.source.start(0, offset, this._duration);
} else {
this.source.start(0, offset);
}
this._startedAt = this._manager.context.currentTime;
this._currentOffset = offset;
this.volume = this._volume;
this.loop = this._loop;
this.pitch = this._pitch;
this._playWhenLoaded = false;
if (!this._suspendInstanceEvents) {
this._onResume();
}
return true;
}
stop() {
this._playWhenLoaded = false;
if (this._state === STATE_STOPPED) {
return false;
}
const wasPlaying = this._state === STATE_PLAYING;
this._state = STATE_STOPPED;
if (this._waitingContextSuspension) {
return true;
}
this._manager.off('volumechange', this._onManagerVolumeChange, this);
this._manager.off('suspend', this._onManagerSuspend, this);
this._manager.off('resume', this._onManagerResume, this);
this._manager.off('destroy', this._onManagerDestroy, this);
this._startedAt = 0;
this._currentTime = 0;
this._currentOffset = 0;
this._startOffset = null;
this._suspendEndEvent++;
if (wasPlaying && this.source) {
this.source.stop(0);
}
this.source = null;
if (!this._suspendInstanceEvents) {
this._onStop();
}
return true;
}
setExternalNodes(firstNode, lastNode) {
if (!firstNode) {
console.error('The firstNode must be a valid Audio Node');
return;
}
if (!lastNode) {
lastNode = firstNode;
}
const speakers = this._manager.context.destination;
if (this._firstNode !== firstNode) {
if (this._firstNode) {
this._connectorNode.disconnect(this._firstNode);
} else {
this._connectorNode.disconnect(speakers);
}
this._firstNode = firstNode;
this._connectorNode.connect(firstNode);
}
if (this._lastNode !== lastNode) {
if (this._lastNode) {
this._lastNode.disconnect(speakers);
}
this._lastNode = lastNode;
this._lastNode.connect(speakers);
}
}
clearExternalNodes() {
const speakers = this._manager.context.destination;
if (this._firstNode) {
this._connectorNode.disconnect(this._firstNode);
this._firstNode = null;
}
if (this._lastNode) {
this._lastNode.disconnect(speakers);
this._lastNode = null;
}
this._connectorNode.connect(speakers);
}
getExternalNodes() {
return [
this._firstNode,
this._lastNode
];
}
_createSource() {
if (!this._sound) {
return null;
}
const context = this._manager.context;
if (this._sound.buffer) {
this.source = context.createBufferSource();
this.source.buffer = this._sound.buffer;
this.source.connect(this._inputNode);
this.source.onended = this._endedHandler;
this.source.loopStart = capTime(this._startTime, this.source.buffer.duration);
if (this._duration) {
this.source.loopEnd = Math.max(this.source.loopStart, capTime(this._startTime + this._duration, this.source.buffer.duration));
}
}
return this.source;
}
_updateCurrentTime() {
this._currentTime = capTime((this._manager.context.currentTime - this._startedAt) * this._pitch + this._currentOffset, this.duration);
}
_onManagerDestroy() {
if (this.source && this._state === STATE_PLAYING) {
this.source.stop(0);
this.source = null;
}
}
}
if (!hasAudioContext()) {
Object.assign(SoundInstance.prototype, {
play: function() {
if (this._state !== STATE_STOPPED) {
this.stop();
}
if (!this.source) {
if (!this._createSource()) {
return false;
}
}
this.volume = this._volume;
this.pitch = this._pitch;
this.loop = this._loop;
this.source.play();
this._state = STATE_PLAYING;
this._playWhenLoaded = false;
this._manager.on('volumechange', this._onManagerVolumeChange, this);
this._manager.on('suspend', this._onManagerSuspend, this);
this._manager.on('resume', this._onManagerResume, this);
this._manager.on('destroy', this._onManagerDestroy, this);
if (this._manager.suspended) {
this._onManagerSuspend();
}
if (!this._suspendInstanceEvents) {
this._onPlay();
}
return true;
},
pause: function() {
if (!this.source || this._state !== STATE_PLAYING) {
return false;
}
this._suspendEndEvent++;
this.source.pause();
this._playWhenLoaded = false;
this._state = STATE_PAUSED;
this._startOffset = null;
if (!this._suspendInstanceEvents) {
this._onPause();
}
return true;
},
resume: function() {
if (!this.source || this._state !== STATE_PAUSED) {
return false;
}
this._state = STATE_PLAYING;
this._playWhenLoaded = false;
if (this.source.paused) {
this.source.play();
if (!this._suspendInstanceEvents) {
this._onResume();
}
}
return true;
},
stop: function() {
if (!this.source || this._state === STATE_STOPPED) {
return false;
}
this._manager.off('volumechange', this._onManagerVolumeChange, this);
this._manager.off('suspend', this._onManagerSuspend, this);
this._manager.off('resume', this._onManagerResume, this);
this._manager.off('destroy', this._onManagerDestroy, this);
this._suspendEndEvent++;
this.source.pause();
this._playWhenLoaded = false;
this._state = STATE_STOPPED;
this._startOffset = null;
if (!this._suspendInstanceEvents) {
this._onStop();
}
return true;
},
setExternalNodes: function() {},
clearExternalNodes: function() {},
getExternalNodes: function() {
return [
null,
null
];
},
_onLoadedMetadata: function() {
this.source.removeEventListener('loadedmetadata', this._loadedMetadataHandler);
this._isReady = true;
let offset = capTime(this._startOffset, this.duration);
offset = capTime(this._startTime + offset, this._sound.duration);
this._startOffset = null;
this.source.currentTime = offset;
},
_createSource: function() {
if (this._sound && this._sound.audio) {
this._isReady = false;
this.source = this._sound.audio.cloneNode(true);
this.source.addEventListener('loadedmetadata', this._loadedMetadataHandler);
this.source.addEventListener('timeupdate', this._timeUpdateHandler);
this.source.onended = this._endedHandler;
}
return this.source;
},
_onTimeUpdate: function() {
if (!this._duration) {
return;
}
if (this.source.currentTime > capTime(this._startTime + this._duration, this.source.duration)) {
if (this.loop) {
this.source.currentTime = capTime(this._startTime, this.source.duration);
} else {
this.source.removeEventListener('timeupdate', this._timeUpdateHandler);
this.source.pause();
this._onEnded();
}
}
},
_onManagerDestroy: function() {
if (this.source) {
this.source.pause();
}
}
});
Object.defineProperty(SoundInstance.prototype, 'volume', {
get: function() {
return this._volume;
},
set: function(volume) {
volume = math.clamp(volume, 0, 1);
this._volume = volume;
if (this.source) {
this.source.volume = volume * this._manager.volume;
}
}
});
Object.defineProperty(SoundInstance.prototype, 'pitch', {
get: function() {
return this._pitch;
},
set: function(pitch) {
this._pitch = Math.max(Number(pitch) || 0, 0.01);
if (this.source) {
this.source.playbackRate = this._pitch;
}
}
});
Object.defineProperty(SoundInstance.prototype, 'sound', {
get: function() {
return this._sound;
},
set: function(value) {
this.stop();
this._sound = value;
}
});
Object.defineProperty(SoundInstance.prototype, 'currentTime', {
get: function() {
if (this._startOffset !== null) {
return this._startOffset;
}
if (this._state === STATE_STOPPED || !this.source) {
return 0;
}
return this.source.currentTime - this._startTime;
},
set: function(value) {
if (value < 0) return;
this._startOffset = value;
if (this.source && this._isReady) {
this.source.currentTime = capTime(this._startTime + capTime(value, this.duration), this._sound.duration);
this._startOffset = null;
}
}
});
}
export { SoundInstance };