@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
369 lines (368 loc) • 10.5 kB
JavaScript
"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);
}
}