@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
228 lines (212 loc) • 6.44 kB
text/typescript
/**
* Get the current waveform data of the connected audio source
*
*
*/
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {AudioBuilder} from '../../../core/audio/AudioBuilder';
import {Waveform} from 'tone/build/esm/component/analysis/Waveform';
import {AudioNodeAnalyserType} from '../../poly/NodeContext';
import {BaseAnalyserAudioNode} from './_BaseAnalyser';
import {dbToGain} from 'tone/build/esm/core/type/Conversions';
import {isNumber, isBooleanTrue} from '../../../core/Type';
import {BaseNodeType} from '../_Base';
import {Number2} from '../../../types/GlobalTypes';
const DEFAULTS = {
// normalRange: false,
size: 1024,
smoothing: 0.8,
}; //FFT.getDefaults();
const RANGE_DEFAULT: Number2 = [10000, -10000];
class WaveformAudioParamsConfig extends NodeParamsConfig {
/** @param array size will be 2**sizeExponent */
sizeExponent = ParamConfig.INTEGER(10, {
range: [4, 14],
rangeLocked: [true, true],
});
/** @param array size */
arraySize = ParamConfig.INTEGER(`2**ch('sizeExponent')`, {
editable: false,
});
/** @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],
cook: false,
// ...effectParamsOptions(paramCallback),
});
/** @param normalizes the output between 0 and 1. The value will be in decibel otherwise. */
normalRange = ParamConfig.BOOLEAN(1, {
cook: false,
// ...effectParamsOptions(paramCallback)
});
/** @param display range param */
updateRangeParam = ParamConfig.BOOLEAN(1, {
cook: false,
callback: (node: BaseNodeType) => {
WaveformAudioNode.PARAM_CALLBACK_updateUpdateRangeParam(node as WaveformAudioNode);
},
});
/** @param range value */
range = ParamConfig.VECTOR2([0, 0], {
visibleIf: {updateRangeParam: 1},
editable: false,
cook: false,
});
/** @param accumulated range */
maxRange = ParamConfig.VECTOR2(RANGE_DEFAULT, {
visibleIf: {updateRangeParam: 1},
editable: false,
cook: false,
});
/** @param resetMaxRange */
resetMaxRange = ParamConfig.BUTTON(null, {
visibleIf: {updateRangeParam: 1},
callback: (node) => {
WaveformAudioNode.PARAM_CALLBACK_resetMaxRange(node as WaveformAudioNode);
},
});
}
const ParamsConfig = new WaveformAudioParamsConfig();
export class WaveformAudioNode extends BaseAnalyserAudioNode<WaveformAudioParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return AudioNodeAnalyserType.WAVEFORM;
}
override initializeNode() {
this.io.inputs.setCount(1);
}
override cook(inputContents: AudioBuilder[]) {
const audioBuilder = inputContents[0];
this._resetEffect();
const waveform = this._effect();
this._updateOnTickHook();
const inputNode = audioBuilder.audioNode();
if (inputNode) {
inputNode.connect(waveform);
}
audioBuilder.setAudioNode(waveform);
this.setAudioBuilder(audioBuilder);
}
private _previousValue: Float32Array | undefined;
getAnalyserValue() {
const currentValue = this._getWaveFormValue();
if (!currentValue) {
return;
}
const blendedValue =
this._previousValue != null && currentValue.length == this._previousValue.length
? this._blendValue(currentValue, this._previousValue)
: currentValue;
if (!this._previousValue || currentValue.length != this._previousValue.length) {
this._previousValue = new Float32Array(currentValue.length);
}
this._previousValue.set(currentValue);
return blendedValue;
}
private _getWaveFormValue() {
if (this.__effect__) {
const values = this.__effect__.getValue();
if (isBooleanTrue(this.pv.normalRange)) {
return values.map((v) => dbToGain(v));
} else {
return values;
}
}
}
private _blendValue(currentValue: Float32Array, previousValue: Float32Array) {
const smoothing = this.pv.smoothing;
if (smoothing == 0) {
return currentValue;
}
if (smoothing == 1) {
return previousValue;
}
const count = currentValue.length;
for (let i = 0; i < count; i++) {
currentValue[i] = smoothing * previousValue[i] + (1 - smoothing) * currentValue[i];
}
return currentValue;
}
private __effect__: Waveform | undefined;
private _effect() {
return (this.__effect__ = this.__effect__ || this._createEffect());
}
private _createEffect() {
const effect = new Waveform({
size: this._effectSize(),
});
// Using the smoothing here does not seem to work.
// so I blend the value instead in .getAnalyserValue
// const analyser = effect.getAnalyser();
// analyser.smoothing = this.pv.smoothing;
return effect;
}
private _resetEffect() {
if (this.__effect__) {
this.__effect__.dispose();
this.__effect__ = undefined;
}
}
// static PARAM_CALLBACK_updateEffect(node: WaveformAudioNode) {
// node._updateEffect();
// }
// private _updateEffect() {
// // const effect = this._effect();
// // const analyser = effect.getAnalyser();
// // analyser.smoothing = this.pv.smoothing;
// }
private _effectSize() {
return 2 ** this.pv.sizeExponent;
}
/*
* UPDATE RANGE PARAM
*/
static PARAM_CALLBACK_updateUpdateRangeParam(node: WaveformAudioNode) {
node._updateRangeParam();
node._updateOnTickHook();
}
static PARAM_CALLBACK_resetMaxRange(node: WaveformAudioNode) {
node.p.maxRange.set(RANGE_DEFAULT);
}
private _updateRangeParam() {
if (!this.__effect__) {
return;
}
const values = this.getAnalyserValue();
if (!values) {
return;
}
const min = Math.min(...values);
const max = Math.max(...values);
this.p.range.set([min, max]);
if (min < this.pv.maxRange.x && isNumber(min) && isFinite(min)) {
this.p.maxRange.x.set(min);
}
if (max > this.pv.maxRange.y && isNumber(max) && isFinite(max)) {
this.p.maxRange.y.set(max);
}
}
/*
* REGISTER TICK CALLBACK
*/
private _updateOnTickHook() {
if (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._updateRangeParam.bind(this));
}
private async _unRegisterOnTickHook() {
this.scene().unRegisterOnBeforeTick(this._tickCallbackName());
}
private _tickCallbackName() {
return `audio/FFT-${this.graphNodeId()}`;
}
}