UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

369 lines (368 loc) 10.5 kB
"use strict"; import { TypedAudioNode } from "./_Base"; import { NodeParamsConfig, ParamConfig } from "../utils/params/ParamsConfig"; import { AudioBuilder } from "../../../core/audio/AudioBuilder"; import { Player } from "tone/build/esm/source/buffer/Player"; import { CoreLoaderAudio } from "../../../core/loader/Audio"; import { isBooleanTrue } from "../../../core/Type"; import { Poly } from "../../Poly"; import { AudioPlayerCallbacksManager } from "./../../../core/audio/PlayerCallbacksManager"; import { AudioType } from "../../poly/registers/nodes/types/Audio"; import { EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT } from "../../../core/loader/FileExtensionRegister"; import { NodeContext } from "../../poly/NodeContext"; const EPSILON = 1e-6; const LOOP_OPTIONS = { cook: false, callback: (node) => { FileAudioNode.PARAM_CALLBACK_updateLoop(node); } }; class FileAudioParamsConfig extends NodeParamsConfig { constructor() { super(...arguments); /** @param url to fetch the audio file from */ this.url = ParamConfig.STRING("", { fileBrowse: { extensions: EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT[NodeContext.AUDIO][AudioType.FILE] } }); /** @param auto start */ this.autostart = ParamConfig.BOOLEAN(1); /** @param duration */ this.duration = ParamConfig.FLOAT(-1, { cook: false, editable: false }); /** @param display currentTime param */ this.updateCurrentTimeParam = ParamConfig.BOOLEAN(0, { cook: false, callback: (node) => { FileAudioNode.PARAM_CALLBACK_updateUpdateCurrentTimeParam(node); } }); /** @param currentTime */ this.currentTime = ParamConfig.FLOAT(0, { visibleIf: { updateCurrentTimeParam: 1 }, range: [0, 100], editable: false, cook: false }); /** @param loop */ this.loop = ParamConfig.BOOLEAN(1, { ...LOOP_OPTIONS }); /** @param useLoopRange */ // useLoopRange = ParamConfig.BOOLEAN(0, { // visibleIf: {loop: 1}, // ...LOOP_OPTIONS, // }); /** @param loop Range */ // loopRange = ParamConfig.VECTOR2([-1, -1], { // visibleIf: {loop: 1, useLoopRange: 1}, // ...LOOP_OPTIONS, // }); /** @param play the audio */ this.play = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_play(node); }, hidden: true }); /** @param stop the audio */ this.pause = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_pause(node); }, hidden: true }); /** @param restart the audio */ this.restart = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_restart(node); }, hidden: true }); /** @param seek 10 seconds back */ this.seekM10 = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_seekOffset(node, -10); }, hidden: true }); /** @param seek 5 seconds back */ this.seekM5 = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_seekOffset(node, -5); }, hidden: true }); /** @param seek 5 seconds forward */ this.seekP5 = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_seekOffset(node, 5); }, hidden: true }); /** @param seek 10 seconds forward */ this.seekP10 = ParamConfig.BUTTON(null, { callback: (node) => { FileAudioNode.PARAM_CALLBACK_seekOffset(node, 10); }, hidden: true }); } } const ParamsConfig = new FileAudioParamsConfig(); export class FileAudioNode extends TypedAudioNode { constructor() { super(...arguments); this.paramsConfig = ParamsConfig; this._stoppedAt = 0; /* * HOOKS */ this._playerCallbacks = {}; } static type() { return AudioType.FILE; } initializeNode() { this.io.inputs.setCount(0); } dispose() { super.dispose(); if (this._player) { this.pause(); this._player.dispose(); } Poly.blobs.clearBlobsForNode(this); } async cook(inputContents) { await this._loadUrl(); this._updateOnTickHook(); if (this._player) { const audioBuilder = new AudioBuilder(); audioBuilder.setSource(this._player); this.setAudioBuilder(audioBuilder); } else { this.cookController.endCook(); } } async _loadUrl() { try { const loader = new CoreLoaderAudio(this.pv.url, this); const buffer = await loader.load(); return new Promise((resolve) => { if (this._player) { this._player.dispose(); } const player = new Player({ url: buffer, loop: isBooleanTrue(this.pv.loop), volume: 0, // no onload event if a buffer is provided instead of a url // onload: () => { // resolve(player); // }, // make sure to have the param loop set to false for the onstop callbacks to be run. onstop: () => { this._runOnStop(player); } }); this._player = player; this._reset(); this.p.duration.set(buffer.duration); if (isBooleanTrue(this.pv.autostart)) { this.play(); } AudioPlayerCallbacksManager.registerPlayer(this._player); resolve(this._player); }); } catch (err) { this.states.error.set(`failed to load url '${this.pv.url}'`); return; } } async play() { if (!this._player) { return; } const offset = isBooleanTrue(this.pv.updateCurrentTimeParam) ? this.pv.currentTime : this._currentTime(); const sanitizedOffset = Math.max(offset + 2 * EPSILON, 0); try { this._runOnBeforePlay(this._player, sanitizedOffset); this._player.start(0, sanitizedOffset); this._runOnPlaySuccess(this._player); } catch (err) { console.error(err); this._runOnPlayError(this._player, err); } this._startedAt = this._player.now() - this._stoppedAt; this._stoppedAt = 0; } async pause() { if (!this._player) { console.warn("no player"); return; } if (this._player.state != "started") { console.warn(`player state is not "started", but "${this._player.state}"`); return; } if (this._startedAt != null) { const elapsed = this._player.now() - this._startedAt; this._stoppedAt = elapsed; } this._player.stop(); } _reset() { this._stoppedAt = 0; this._startedAt = void 0; this.p.currentTime.set(0); } async restart() { if (!this._player) { return; } this._player.seek(0, 0); this._reset(); this.play(); } seekOffset(offset) { if (!this._player) { return; } if (this._startedAt == null) { return; } const currentTime = this._currentTime(); this._startedAt -= offset; this._player.seek(currentTime + offset); } /* * UPDATE CURRENT TIME PARAM */ static PARAM_CALLBACK_updateUpdateCurrentTimeParam(node) { node._updateCurrentTimeParam(); node._updateOnTickHook(); } _updateCurrentTimeParam() { if (!this._player) { return; } const currentTime = this._currentTime(); this.p.currentTime.set(currentTime); } _currentTime() { if (this._stoppedAt) { return this._stoppedAt; } if (this._player && this._startedAt != null) { let current = this._player.now() - this._startedAt; const duration = this.pv.duration; if (current > duration) { current -= duration; } return current; } return 0; } /* * LOOP */ static PARAM_CALLBACK_updateLoop(node) { node._updateLoop(); } _updateLoop() { if (!this._player) { return; } this._player.loop = this.pv.loop; } /* * REGISTER TICK CALLBACK */ _updateOnTickHook() { if (isBooleanTrue(this.pv.updateCurrentTimeParam)) { this._registerOnTickHook(); } else { this._unRegisterOnTickHook(); } } async _registerOnTickHook() { if (this.scene().registeredBeforeTickCallbacks().has(this._tickCallbackName())) { return; } this.scene().registerOnBeforeTick(this._tickCallbackName(), this._updateCurrentTimeParam.bind(this)); } async _unRegisterOnTickHook() { this.scene().unRegisterOnBeforeTick(this._tickCallbackName()); } _tickCallbackName() { return `audio/File-${this.graphNodeId()}`; } /* * STATIC CALLBACKS */ static PARAM_CALLBACK_seekOffset(node, offset) { node.seekOffset(offset); } static PARAM_CALLBACK_play(node) { node.play(); } static PARAM_CALLBACK_pause(node) { node.pause(); } static PARAM_CALLBACK_restart(node) { node.restart(); } // onBeforePlay onBeforePlay(callback) { this._on("onBeforePlay", callback); } _runOnBeforePlay(player, offset) { var _a; (_a = this._playerCallbacks.onBeforePlay) == null ? void 0 : _a.forEach((callback) => callback(offset)); AudioPlayerCallbacksManager.runOnBeforePlayCallbacks(player, offset); } // onPlaySuccess onPlaySuccess(callback) { this._on("onPlaySuccess", callback); } _runOnPlaySuccess(player) { var _a; (_a = this._playerCallbacks.onPlaySuccess) == null ? void 0 : _a.forEach((callback) => callback()); AudioPlayerCallbacksManager.runOnPlaySuccessCallbacks(player); } // onPlayError onPlayError(callback) { this._on("onPlayError", callback); } _runOnPlayError(player, err) { var _a; (_a = this._playerCallbacks.onPlayError) == null ? void 0 : _a.forEach((callback) => callback(err)); AudioPlayerCallbacksManager.runOnPlayErrorCallbacks(player, err); } // onStop onStop(callback) { this._on("onStop", callback); } removeOnStop(callback) { this._removeCallback("onStop", callback); } _runOnStop(player) { var _a; (_a = this._playerCallbacks.onStop) == null ? void 0 : _a.forEach((callback) => callback()); AudioPlayerCallbacksManager.runOnStopCallbacks(player); } // generic _on(eventName, callback) { var _a; this._playerCallbacks[eventName] = this._playerCallbacks[eventName] || /* @__PURE__ */ new Set(); (_a = this._playerCallbacks[eventName]) == null ? void 0 : _a.add(callback); } _removeCallback(eventName, callback) { var _a; (_a = this._playerCallbacks[eventName]) == null ? void 0 : _a.delete(callback); } }