Files
Foundry-VTT-Docker/resources/app/client/data/documents/playlist-sound.js
2025-01-04 00:34:03 +01:00

240 lines
7.3 KiB
JavaScript

/**
* The client-side PlaylistSound document which extends the common BasePlaylistSound model.
* Each PlaylistSound belongs to the sounds collection of a Playlist document.
* @extends foundry.documents.BasePlaylistSound
* @mixes ClientDocumentMixin
*
* @see {@link Playlist} The Playlist document which contains PlaylistSound embedded documents
* @see {@link PlaylistSoundConfig} The PlaylistSound configuration application
* @see {@link foundry.audio.Sound} The Sound API which manages web audio playback
*/
class PlaylistSound extends ClientDocumentMixin(foundry.documents.BasePlaylistSound) {
/**
* The debounce tolerance for processing rapid volume changes into database updates in milliseconds
* @type {number}
*/
static VOLUME_DEBOUNCE_MS = 100;
/**
* The Sound which manages playback for this playlist sound.
* The Sound is created lazily when playback is required.
* @type {Sound|null}
*/
sound;
/**
* A debounced function, accepting a single volume parameter to adjust the volume of this sound
* @type {function(number): void}
* @param {number} volume The desired volume level
*/
debounceVolume = foundry.utils.debounce(volume => {
this.update({volume}, {diff: false, render: false});
}, PlaylistSound.VOLUME_DEBOUNCE_MS);
/* -------------------------------------------- */
/**
* Create a Sound used to play this PlaylistSound document
* @returns {Sound|null}
* @protected
*/
_createSound() {
if ( game.audio.locked ) {
throw new Error("You may not call PlaylistSound#_createSound until after game audio is unlocked.");
}
if ( !(this.id && this.path) ) return null;
const sound = game.audio.create({src: this.path, context: this.context, singleton: false});
sound.addEventListener("play", this._onStart.bind(this));
sound.addEventListener("end", this._onEnd.bind(this));
sound.addEventListener("stop", this._onStop.bind(this));
return sound;
}
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* Determine the fade duration for this PlaylistSound based on its own configuration and that of its parent.
* @type {number}
*/
get fadeDuration() {
if ( !this.sound.duration ) return 0;
const halfDuration = Math.ceil(this.sound.duration / 2) * 1000;
return Math.clamp(this.fade ?? this.parent.fade ?? 0, 0, halfDuration);
}
/**
* The audio context within which this sound is played.
* This will be undefined if the audio context is not yet active.
* @type {AudioContext|undefined}
*/
get context() {
const channel = (this.channel || this.parent.channel) ?? "music";
return game.audio[channel];
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/**
* Synchronize playback for this particular PlaylistSound instance.
*/
sync() {
// Conclude playback
if ( !this.playing ) {
if ( this.sound?.playing ) {
this.sound.stop({fade: this.pausedTime ? 0 : this.fadeDuration, volume: 0});
}
return;
}
// Create a Sound if necessary
this.sound ||= this._createSound();
const sound = this.sound;
if ( !sound || sound.failed ) return;
// Update an already playing sound
if ( sound.playing ) {
sound.loop = this.repeat;
sound.fade(this.volume, {duration: 500});
return;
}
// Begin playback
sound.load({autoplay: true, autoplayOptions: {
loop: this.repeat,
volume: this.volume,
fade: this.fade,
offset: this.pausedTime && !sound.playing ? this.pausedTime : undefined
}});
}
/* -------------------------------------------- */
/**
* Load the audio for this sound for the current client.
* @returns {Promise<void>}
*/
async load() {
this.sound ||= this._createSound();
await this.sound.load();
}
/* -------------------------------------------- */
/** @inheritdoc */
toAnchor({classes=[], ...options}={}) {
if ( this.playing ) classes.push("playing");
if ( !this.isOwner ) classes.push("disabled");
return super.toAnchor({classes, ...options});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onClickDocumentLink(event) {
if ( this.playing ) return this.parent.stopSound(this);
return this.parent.playSound(this);
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritDoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if ( this.parent ) this.parent._playbackOrder = undefined;
}
/* -------------------------------------------- */
/** @inheritDoc */
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
if ( "path" in changed ) {
if ( this.sound ) this.sound.stop();
this.sound = this._createSound();
}
if ( ("sort" in changed) && this.parent ) {
this.parent._playbackOrder = undefined;
}
this.sync();
}
/* -------------------------------------------- */
/** @inheritDoc */
_onDelete(options, userId) {
super._onDelete(options, userId);
if ( this.parent ) this.parent._playbackOrder = undefined;
this.playing = false;
this.sync();
}
/* -------------------------------------------- */
/**
* Special handling that occurs when playback of a PlaylistSound is started.
* @protected
*/
async _onStart() {
if ( !this.playing ) return this.sound.stop();
const {volume, fadeDuration} = this;
// Immediate fade-in
if ( fadeDuration ) {
// noinspection ES6MissingAwait
this.sound.fade(volume, {duration: fadeDuration});
}
// Schedule fade-out
if ( !this.repeat && Number.isFinite(this.sound.duration) ) {
const fadeOutTime = this.sound.duration - (fadeDuration / 1000);
const fadeOut = () => this.sound.fade(0, {duration: fadeDuration});
// noinspection ES6MissingAwait
this.sound.schedule(fadeOut, fadeOutTime);
}
// Playlist-level orchestration actions
return this.parent._onSoundStart(this);
}
/* -------------------------------------------- */
/**
* Special handling that occurs when a PlaylistSound reaches the natural conclusion of its playback.
* @protected
*/
async _onEnd() {
if ( !this.parent.isOwner ) return;
return this.parent._onSoundEnd(this);
}
/* -------------------------------------------- */
/**
* Special handling that occurs when a PlaylistSound is manually stopped before its natural conclusion.
* @protected
*/
async _onStop() {}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* The effective volume at which this playlist sound is played, incorporating the global playlist volume setting.
* @type {number}
*/
get effectiveVolume() {
foundry.utils.logCompatibilityWarning("PlaylistSound#effectiveVolume is deprecated in favor of using"
+ " PlaylistSound#volume directly", {since: 12, until: 14});
return this.volume;
}
}