UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

264 lines (261 loc) 9.15 kB
import { math } from '../../core/math/math.js'; import { hasAudioContext } from './capabilities.js'; /** * @import { SoundManager } from '../sound/manager.js' * @import { Sound } from '../sound/sound.js' */ /** * A channel is created when the {@link SoundManager} begins playback of a {@link Sound}. Usually * created internally by {@link SoundManager#playSound} or {@link SoundManager#playSound3d}. * Developers usually won't have to create Channels manually. * * @ignore */ class Channel { /** * Get the current value for the volume. Between 0 and 1. * * @returns {number} The volume of the channel. */ getVolume() { return this.volume; } /** * Get the current looping state of the Channel. * * @returns {boolean} The loop property for the channel. */ getLoop() { return this.loop; } /** * Enable/disable the loop property to make the sound restart from the beginning when it * reaches the end. * * @param {boolean} loop - True to loop the sound, false otherwise. */ setLoop(loop) { this.loop = loop; if (this.source) { this.source.loop = loop; } } /** * Get the current pitch of the Channel. * * @returns {number} The pitch of the channel. */ getPitch() { return this.pitch; } /** * Handle the manager's 'volumechange' event. */ onManagerVolumeChange() { this.setVolume(this.getVolume()); } /** * Handle the manager's 'suspend' event. */ onManagerSuspend() { if (this.isPlaying() && !this.suspended) { this.suspended = true; this.pause(); } } /** * Handle the manager's 'resume' event. */ onManagerResume() { if (this.suspended) { this.suspended = false; this.unpause(); } } /** * Begin playback of sound. */ play() { if (this.source) { throw new Error('Call stop() before calling play()'); } this._createSource(); if (!this.source) { return; } this.startTime = this.manager.context.currentTime; this.source.start(0, this.startOffset % this.source.buffer.duration); // Initialize volume and loop - note moved to be after start() because of Chrome bug this.setVolume(this.volume); this.setLoop(this.loop); this.setPitch(this.pitch); this.manager.on('volumechange', this.onManagerVolumeChange, this); this.manager.on('suspend', this.onManagerSuspend, this); this.manager.on('resume', this.onManagerResume, this); // suspend immediately if manager is suspended if (this.manager.suspended) { this.onManagerSuspend(); } } /** * Pause playback of sound. Call unpause() to resume playback from the same position. */ pause() { if (this.source) { this.paused = true; this.startOffset += this.manager.context.currentTime - this.startTime; this.source.stop(0); this.source = null; } } /** * Resume playback of the sound. Playback resumes at the point that the audio was paused. */ unpause() { if (this.source || !this.paused) { console.warn('Call pause() before unpausing.'); return; } this._createSource(); if (!this.source) { return; } this.startTime = this.manager.context.currentTime; this.source.start(0, this.startOffset % this.source.buffer.duration); // Initialize parameters this.setVolume(this.volume); this.setLoop(this.loop); this.setPitch(this.pitch); this.paused = false; } /** * Stop playback of sound. Calling play() again will restart playback from the beginning of the * sound. */ stop() { if (this.source) { this.source.stop(0); this.source = null; } this.manager.off('volumechange', this.onManagerVolumeChange, this); this.manager.off('suspend', this.onManagerSuspend, this); this.manager.off('resume', this.onManagerResume, this); } /** * Set the volume of playback between 0 and 1. * * @param {number} volume - The volume of the sound. Will be clamped between 0 and 1. */ setVolume(volume) { volume = math.clamp(volume, 0, 1); this.volume = volume; if (this.gain) { this.gain.gain.value = volume * this.manager.volume; } } setPitch(pitch) { this.pitch = pitch; if (this.source) { this.source.playbackRate.value = pitch; } } isPlaying() { return !this.paused && this.source.playbackState === this.source.PLAYING_STATE; } getDuration() { return this.source ? this.source.buffer.duration : 0; } _createSource() { var context = this.manager.context; if (this.sound.buffer) { this.source = context.createBufferSource(); this.source.buffer = this.sound.buffer; // Connect up the nodes this.source.connect(this.gain); this.gain.connect(context.destination); if (!this.loop) { // mark source as paused when it ends this.source.onended = this.pause.bind(this); } } } /** * Create a new Channel instance. * * @param {SoundManager} manager - The SoundManager instance. * @param {Sound} sound - The sound to playback. * @param {object} [options] - Optional options object. * @param {number} [options.volume] - The playback volume, between 0 and 1. Defaults to 1. * @param {number} [options.pitch] - The relative pitch. Defaults to 1 (plays at normal pitch). * @param {boolean} [options.loop] - Whether the sound should loop when it reaches the * end or not. Defaults to false. */ constructor(manager, sound, options = {}){ var _options_volume; this.volume = (_options_volume = options.volume) != null ? _options_volume : 1; var _options_loop; this.loop = (_options_loop = options.loop) != null ? _options_loop : false; var _options_pitch; this.pitch = (_options_pitch = options.pitch) != null ? _options_pitch : 1; this.sound = sound; this.paused = false; this.suspended = false; this.manager = manager; /** @type {globalThis.Node | null} */ this.source = null; if (hasAudioContext()) { this.startTime = 0; this.startOffset = 0; var context = manager.context; this.gain = context.createGain(); } else if (sound.audio) { // handle the case where sound was this.source = sound.audio.cloneNode(false); this.source.pause(); // not initially playing } } } if (!hasAudioContext()) { Object.assign(Channel.prototype, { play: function play() { if (this.source) { this.paused = false; this.setVolume(this.volume); this.setLoop(this.loop); this.setPitch(this.pitch); this.source.play(); } this.manager.on('volumechange', this.onManagerVolumeChange, this); this.manager.on('suspend', this.onManagerSuspend, this); this.manager.on('resume', this.onManagerResume, this); // suspend immediately if manager is suspended if (this.manager.suspended) { this.onManagerSuspend(); } }, pause: function pause() { if (this.source) { this.paused = true; this.source.pause(); } }, unpause: function unpause() { if (this.source) { this.paused = false; this.source.play(); } }, stop: function stop() { if (this.source) { this.source.pause(); } this.manager.off('volumechange', this.onManagerVolumeChange, this); this.manager.off('suspend', this.onManagerSuspend, this); this.manager.off('resume', this.onManagerResume, this); }, setVolume: function setVolume(volume) { volume = math.clamp(volume, 0, 1); this.volume = volume; if (this.source) { this.source.volume = volume * this.manager.volume; } }, setPitch: function setPitch(pitch) { this.pitch = pitch; if (this.source) { this.source.playbackRate = pitch; } }, getDuration: function getDuration() { return this.source && !isNaN(this.source.duration) ? this.source.duration : 0; }, isPlaying: function isPlaying() { return !this.source.paused; } }); } export { Channel };