@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
253 lines (236 loc) • 8.44 kB
text/typescript
/**
* Creates an audio source.
*
* @remarks
* This node also has its own transforms. And if it is set as input of other nodes, their objects will be added as children to the object of this node.
*
*/
import {TypedObjNode} from './_Base';
import {Group} from 'three';
import {TransformedParamConfig, TransformController} from './utils/TransformController';
import {FlagsControllerD} from '../utils/FlagsController';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {HierarchyController} from './utils/HierarchyController';
import {ObjType} from '../../poly/registers/nodes/types/Obj';
import {NodeContext} from '../../poly/NodeContext';
import {isBooleanTrue} from '../../../core/BooleanValue';
import {CorePositionalAudio, DistanceModel, DISTANCE_MODELS} from '../../../core/audio/PositionalAudio';
import {CorePositionalAudioHelper} from '../../../core/audio/PositionalAudioHelper';
import {BaseNodeType} from '../_Base';
import {AudioNodeChildrenMap} from '../../poly/registers/nodes/Audio';
import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController';
import {Constructor, valueof} from '../../../types/GlobalTypes';
import {BaseAudioNodeType} from '../audio/_Base';
const paramCallback = () => {
return {
cook: false,
callback: (node: BaseNodeType) => {
PositionalAudioObjNode.PARAM_CALLBACK_updatePositionalAudio(node as PositionalAudioObjNode);
},
};
};
class PositionalAudioParamConfig extends TransformedParamConfig(NodeParamsConfig) {
audio = ParamConfig.FOLDER();
/** @param audio node */
audioNode = ParamConfig.NODE_PATH('', {
nodeSelection: {
context: NodeContext.AUDIO,
},
// dependentOnFoundNode: false,
});
/** @param refDistance. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/refDistance*/
refDistance = ParamConfig.FLOAT(10, {
range: [0, 10],
rangeLocked: [true, false],
...paramCallback(),
});
/** @param rolloffFactor. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/rolloffFactor */
rolloffFactor = ParamConfig.FLOAT(10, {
range: [0, 10],
rangeLocked: [true, false],
...paramCallback(),
});
/** @param maxDistance. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/maxDistance */
maxDistance = ParamConfig.FLOAT(100, {
range: [0.001, 100],
rangeLocked: [true, false],
...paramCallback(),
});
/** @param distanceModel. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel */
distanceModel = ParamConfig.INTEGER(DISTANCE_MODELS.indexOf(DistanceModel.LINEAR), {
menu: {
entries: DISTANCE_MODELS.map((name, value) => {
return {name, value};
}),
},
...paramCallback(),
});
/** @param coneInnerAngle. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode */
coneInnerAngle = ParamConfig.FLOAT(360, {
range: [0, 360],
rangeLocked: [true, true],
...paramCallback(),
});
/** @param coneOuterAngle. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode */
coneOuterAngle = ParamConfig.FLOAT(230, {
range: [0, 360],
rangeLocked: [true, true],
...paramCallback(),
});
/** @param coneOuterGain. See https://developer.mozilla.org/en-US/docs/Web/API/PannerNode */
coneOuterGain = ParamConfig.FLOAT(0.1, {
range: [0, 1],
rangeLocked: [true, true],
...paramCallback(),
});
/** @param ensures the transform of the audio listener is updated on very frame */
listenerTransformAutoUpdate = ParamConfig.BOOLEAN(1);
/** @param show helper */
showHelper = ParamConfig.BOOLEAN(1);
/** @param helper size */
helperSize = ParamConfig.FLOAT(1, {
range: [0, 10],
rangeLocked: [true, false],
visibleIf: {showHelper: true},
...paramCallback(),
});
}
const ParamsConfig = new PositionalAudioParamConfig();
function createPositionalAudio() {
const positionalAudio = new CorePositionalAudio();
positionalAudio.matrixAutoUpdate = false;
return positionalAudio;
}
export class PositionalAudioObjNode extends TypedObjNode<Group, PositionalAudioParamConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return ObjType.POSITIONAL_AUDIO;
}
override readonly hierarchyController: HierarchyController = new HierarchyController(this);
override readonly transformController: TransformController = new TransformController(this);
public override readonly flags: FlagsControllerD = new FlagsControllerD(this);
private _positionalAudio = createPositionalAudio();
private _helper: CorePositionalAudioHelper | undefined;
override createObject() {
const group = new Group();
group.matrixAutoUpdate = false;
group.add(this._positionalAudio);
return group;
}
override initializeNode() {
this.hierarchyController.initializeNode();
this.transformController.initializeNode();
this._updateHelperHierarchy();
this.flags.display.onUpdate(async () => {
await this._updateToDestination();
await this._updateHelperHierarchy();
});
}
private async _updateToDestination() {
if (this.flags.display.active()) {
const listener = this.root().audioController.audioListeners()[0];
if (!listener) {
this.states.error.set('a listener is required in the scene');
return;
}
// this._positionalAudio.disconnect();
listener.object.addInput(this._positionalAudio);
} else {
this._positionalAudio.disconnect();
}
}
private _updateHelperHierarchy() {
if (!this._helper) {
return;
}
if (this.flags.display.active()) {
this.object.add(this._helper);
} else {
this.object.remove(this._helper);
}
}
override async cook() {
this.transformController.update();
await this._updateToDestination();
this._updatePositionalAudio();
this._connectAudioNode();
this.cookController.endCook();
}
private async _updatePositionalAudio() {
this._positionalAudio.setRefDistance(this.pv.refDistance);
this._positionalAudio.setRolloffFactor(this.pv.rolloffFactor);
this._positionalAudio.setMaxDistance(this.pv.maxDistance);
this._positionalAudio.setDistanceModel(DISTANCE_MODELS[this.pv.distanceModel]);
this._positionalAudio.setDirectionalCone(this.pv.coneInnerAngle, this.pv.coneOuterAngle, this.pv.coneOuterGain);
this._positionalAudio.listenerTransformAutoUpdate = isBooleanTrue(this.pv.listenerTransformAutoUpdate);
if (isBooleanTrue(this.pv.showHelper)) {
this._helper = this._helper || this._createHelper(this._positionalAudio);
this.object.add(this._helper);
}
if (this._helper) {
this._helper.visible = isBooleanTrue(this.pv.showHelper);
this._helper.range = this.pv.helperSize;
this._helper.update();
}
}
private _createHelper(positionalAudio: CorePositionalAudio) {
const helper = new CorePositionalAudioHelper(positionalAudio);
helper.matrixAutoUpdate = false;
return helper;
}
private _resetAudioNode() {
this._positionalAudio.setInput(undefined);
}
private async _connectAudioNode() {
this._resetAudioNode();
const baseAudioNode = this.pv.audioNode.nodeWithContext(NodeContext.AUDIO);
if (!baseAudioNode) {
this.states.error.set('no audio node found');
this._resetAudioNode();
return;
}
const audioContainer = await baseAudioNode.compute();
const audioBuilder = audioContainer.coreContent();
if (!audioBuilder) {
this.states.error.set('invalid audio node');
this._resetAudioNode();
return;
}
const toneAudioNode = audioBuilder.audioNode();
if (!toneAudioNode) {
this.states.error.set('no valid audio node given');
this._resetAudioNode();
return;
}
this._positionalAudio.setInput(toneAudioNode);
}
static PARAM_CALLBACK_updatePositionalAudio(node: PositionalAudioObjNode) {
node._updatePositionalAudio();
}
/*
*
* CHILDREN
*
*/
protected override _childrenControllerContext = NodeContext.AUDIO;
override createNode<S extends keyof AudioNodeChildrenMap>(
node_class: S,
options?: NodeCreateOptions
): AudioNodeChildrenMap[S];
override createNode<K extends valueof<AudioNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K;
override createNode<K extends valueof<AudioNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K {
return super.createNode(node_class, options) as K;
}
override children() {
return super.children() as BaseAudioNodeType[];
}
override nodesByType<K extends keyof AudioNodeChildrenMap>(type: K): AudioNodeChildrenMap[K][] {
return super.nodesByType(type) as AudioNodeChildrenMap[K][];
}
}