UNPKG

@polygonjs/polygonjs

Version:

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

238 lines (224 loc) 7.02 kB
/** * creates a fast fourier transform analyser * * */ import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig'; import {AudioBuilder} from '../../../core/audio/AudioBuilder'; import {FFT} from 'tone/build/esm/component/analysis/FFT'; import {AudioNodeAnalyserType} from '../../poly/NodeContext'; import {BaseNodeType} from '../_Base'; import {effectParamsOptions} from './utils/EffectsController'; import {isNumber, isBooleanTrue} from '../../../core/Type'; import {BaseAnalyserAudioNode} from './_BaseAnalyser'; import {Number2} from '../../../types/GlobalTypes'; import { convertFrequencyBandsToOctaveBands, convertFrequencyBandsToOctaveBandsDivisions2, } from '../../../core/audio/AudioConversion'; const DEFAULTS = { normalRange: false, size: 1024, smoothing: 0.8, }; //FFT.getDefaults(); const paramCallback = (node: BaseNodeType) => { FFTAudioNode.PARAM_CALLBACK_updateEffect(node as FFTAudioNode); }; const RANGE_DEFAULT: Number2 = [10000, -10000]; class FFTAudioParamsConfig 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], ...effectParamsOptions(paramCallback), }); /** @param normalizes the output between 0 and 1. The value will be in decibel otherwise. */ normalRange = ParamConfig.BOOLEAN(1, effectParamsOptions(paramCallback)); /** @param groups the FFT frequency bands into octave bands */ asOctaves = ParamConfig.BOOLEAN(0, { // if cook is false, the materials reading the COP/audioAnalyser // do not seem to update the texture when this is changed. // I should investigate, as this should not be different than changing normalRange. //cook:false, }); octaveDivisions = ParamConfig.INTEGER(1, { range: [1, 2], rangeLocked: [true, true], visibleIf: {asOctaves: 1}, separatorAfter: true, }); /** @param display range param */ updateRangeParam = ParamConfig.BOOLEAN(1, { cook: false, callback: (node: BaseNodeType) => { FFTAudioNode.PARAM_CALLBACK_updateUpdateRangeParam(node as FFTAudioNode); }, }); /** @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) => { FFTAudioNode.PARAM_CALLBACK_resetMaxRange(node as FFTAudioNode); }, }); } const ParamsConfig = new FFTAudioParamsConfig(); export class FFTAudioNode extends BaseAnalyserAudioNode<FFTAudioParamsConfig> { override paramsConfig = ParamsConfig; static override type() { return AudioNodeAnalyserType.FFT; } override initializeNode() { this.io.inputs.setCount(1); } override cook(inputContents: AudioBuilder[]) { const audioBuilder = inputContents[0]; this._resetEffect(); const fft = this._effect(); this._updateOnTickHook(); const inputNode = audioBuilder.audioNode(); if (inputNode) { inputNode.connect(fft); } audioBuilder.setAudioNode(fft); this.setAudioBuilder(audioBuilder); } getAnalyserValue() { if (this.__effect__) { const fftValues = this.__effect__.getValue(); if (isBooleanTrue(this.pv.asOctaves)) { // if (isBooleanTrue(this.pv.normalRange)) { // const timeConstant = this.__effect__.getFrequencyOfIndex(1); // const mult = 2 ** this.pv.sizeExponent; // console.log('mult', mult); // const valuesCount = fftValues.length; // for (let i = 0; i < valuesCount; i++) { // fftValues[i] *= mult; // } // } const octaveValues = this._octaveValues(); if (this._octaveDivisions() == 1) { convertFrequencyBandsToOctaveBands(fftValues, octaveValues); } else { convertFrequencyBandsToOctaveBandsDivisions2(fftValues, octaveValues, this._octaveDivisions()); } return octaveValues; } else { return fftValues; } } } private __octaveValues: Float32Array | undefined; private _octaveDivisions() { return 2 ** (this.pv.octaveDivisions - 1); } private _octaveValues() { const requiredSize = isBooleanTrue(this.pv.asOctaves) ? this.pv.sizeExponent * this._octaveDivisions() : this.pv.sizeExponent; // const requiredSize = this.pv.sizeExponent; if (this.__octaveValues && this.__octaveValues.length != requiredSize) { this.__octaveValues = undefined; } return (this.__octaveValues = this.__octaveValues || new Float32Array(requiredSize)); } private __effect__: FFT | undefined; private _effect() { return (this.__effect__ = this.__effect__ || this._createEffect()); } private _createEffect() { return new FFT({ size: this._FFTSize(), normalRange: isBooleanTrue(this.pv.normalRange), smoothing: this.pv.smoothing, }); } private _resetEffect() { if (this.__effect__) { this.__effect__.dispose(); this.__effect__ = undefined; } } static PARAM_CALLBACK_updateEffect(node: FFTAudioNode) { node._updateEffect(); } private _updateEffect() { const effect = this._effect(); effect.normalRange = isBooleanTrue(this.pv.normalRange); effect.smoothing = this.pv.smoothing; } private _FFTSize() { return 2 ** this.pv.sizeExponent; } /* * UPDATE RANGE PARAM */ static PARAM_CALLBACK_updateUpdateRangeParam(node: FFTAudioNode) { node._updateRangeParam(); node._updateOnTickHook(); } static PARAM_CALLBACK_resetMaxRange(node: FFTAudioNode) { 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()}`; } }