@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
197 lines (186 loc) • 5.5 kB
text/typescript
/**
* creates a meter, which can read the raw value of the signal
*
*
*/
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {AudioBuilder} from '../../../core/audio/AudioBuilder';
import {Meter} from 'tone/build/esm/component/analysis/Meter';
import {AudioNodeAnalyserType} from '../../poly/NodeContext';
import {isNumber, isBooleanTrue} from '../../../core/Type';
import {effectParamsOptions} from './utils/EffectsController';
import {BaseNodeType} from '../_Base';
import {BaseAnalyserAudioNode} from './_BaseAnalyser';
import {Number2} from '../../../types/GlobalTypes';
const DEFAULTS = {
smoothing: 0.8,
}; //Meter.getDefaults();
const paramCallback = (node: BaseNodeType) => {
MeterAudioNode.PARAM_CALLBACK_updateEffect(node as MeterAudioNode);
};
const RANGE_DEFAULT: Number2 = [10000, -10000];
class MeterAudioParamsConfig extends NodeParamsConfig {
/** @param a value from between 0 and 1 where 0 represents no time averaging with the last analysis frame */
smoothing = ParamConfig.FLOAT(DEFAULTS.smoothing, {
range: [0, 1],
rangeLocked: [true, true],
...effectParamsOptions(paramCallback),
});
/** @param normalizes the output between 0 and 1. The value will be in decibel otherwise. */
normalRange = ParamConfig.BOOLEAN(1, effectParamsOptions(paramCallback));
/** @param display meter param */
updateValueParam = ParamConfig.BOOLEAN(1, {
cook: false,
callback: (node: BaseNodeType) => {
MeterAudioNode.PARAM_CALLBACK_updateUpdateMeterParam(node as MeterAudioNode);
},
});
/** @param meter value */
value = ParamConfig.FLOAT(0, {
visibleIf: {updateValueParam: 1},
range: [-100, 100],
editable: false,
cook: false,
});
/** @param display meter param */
updateRangeParam = ParamConfig.BOOLEAN(1, {
cook: false,
callback: (node: BaseNodeType) => {
MeterAudioNode.PARAM_CALLBACK_updateUpdateMeterParam(node as MeterAudioNode);
},
});
/** @param accumulated range */
maxRange = ParamConfig.VECTOR2([10000, -10000], {
visibleIf: {updateRangeParam: 1},
editable: false,
cook: false,
});
/** @param resetMaxRange */
resetMaxRange = ParamConfig.BUTTON(null, {
visibleIf: {updateRangeParam: 1},
callback: (node) => {
MeterAudioNode.PARAM_CALLBACK_resetMaxRange(node as MeterAudioNode);
},
});
}
const ParamsConfig = new MeterAudioParamsConfig();
export class MeterAudioNode extends BaseAnalyserAudioNode<MeterAudioParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return AudioNodeAnalyserType.METER;
}
override initializeNode() {
this.io.inputs.setCount(1);
}
override cook(inputContents: AudioBuilder[]) {
const audioBuilder = inputContents[0];
this._resetEffect();
const meter = this._effect();
this._updateOnTickHook();
const inputNode = audioBuilder.audioNode();
if (inputNode) {
inputNode.connect(meter);
}
audioBuilder.setAudioNode(meter);
this.setAudioBuilder(audioBuilder);
}
private _arrayValue = new Float32Array(1);
getAnalyserValue() {
if (this.__effect__) {
const value = this.__effect__.getValue();
if (isNumber(value)) {
this._arrayValue[0] = value;
return this._arrayValue;
} else {
return value;
}
}
}
private __effect__: Meter | undefined;
private _effect() {
return (this.__effect__ = this.__effect__ || this._createEffect());
}
private _createEffect() {
return new Meter({
smoothing: this.pv.smoothing,
normalRange: isBooleanTrue(this.pv.normalRange),
});
}
private _resetEffect() {
if (this.__effect__) {
this.__effect__.dispose();
this.__effect__ = undefined;
}
}
static PARAM_CALLBACK_updateEffect(node: MeterAudioNode) {
node._updateEffect();
}
private _updateEffect() {
const effect = this._effect();
effect.normalRange = isBooleanTrue(this.pv.normalRange);
effect.smoothing = this.pv.smoothing;
}
/*
* UPDATE METER PARAM
*/
static PARAM_CALLBACK_updateUpdateMeterParam(node: MeterAudioNode) {
node._updateMeterParam();
node._updateOnTickHook();
}
static PARAM_CALLBACK_resetMaxRange(node: MeterAudioNode) {
node.p.maxRange.set(RANGE_DEFAULT);
}
private _updateMeterParam() {
if (!this.__effect__) {
return;
}
const value = this.getAnalyserValue();
if (!value) {
return;
}
const valueN = value[0];
if (!isFinite(valueN)) {
return;
}
// we check that we have a number again in case meter.getValue()
// returns Infinity
if (isBooleanTrue(this.pv.updateValueParam)) {
if (isNumber(valueN)) {
this.p.value.set(valueN);
}
}
if (isBooleanTrue(this.pv.updateRangeParam)) {
// update max range
const newVal = valueN;
if (newVal < this.pv.maxRange.x) {
this.p.maxRange.x.set(newVal);
} else {
if (newVal > this.pv.maxRange.y) {
this.p.maxRange.y.set(newVal);
}
}
}
}
/*
* REGISTER TICK CALLBACK
*/
private _updateOnTickHook() {
if (isBooleanTrue(this.pv.updateValueParam) || isBooleanTrue(this.pv.updateRangeParam)) {
this._registerOnTickHook();
} else {
this._unRegisterOnTickHook();
}
}
private async _registerOnTickHook() {
if (this.scene().registeredBeforeTickCallbacks().has(this._tickCallbackName())) {
return;
}
this.scene().registerOnBeforeTick(this._tickCallbackName(), this._updateMeterParam.bind(this));
}
private async _unRegisterOnTickHook() {
this.scene().unRegisterOnBeforeTick(this._tickCallbackName());
}
private _tickCallbackName() {
return `audio/Meter-${this.graphNodeId()}`;
}
}