UNPKG

@polygonjs/polygonjs

Version:

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

210 lines (209 loc) 6.59 kB
"use strict"; import { NodeParamsConfig, ParamConfig } from "../utils/params/ParamsConfig"; import { FFT } from "tone/build/esm/component/analysis/FFT"; import { AudioNodeAnalyserType } from "../../poly/NodeContext"; import { effectParamsOptions } from "./utils/EffectsController"; import { isNumber, isBooleanTrue } from "../../../core/Type"; import { BaseAnalyserAudioNode } from "./_BaseAnalyser"; import { convertFrequencyBandsToOctaveBands, convertFrequencyBandsToOctaveBandsDivisions2 } from "../../../core/audio/AudioConversion"; const DEFAULTS = { normalRange: false, size: 1024, smoothing: 0.8 }; const paramCallback = (node) => { FFTAudioNode.PARAM_CALLBACK_updateEffect(node); }; const RANGE_DEFAULT = [1e4, -1e4]; class FFTAudioParamsConfig extends NodeParamsConfig { constructor() { super(...arguments); /** @param array size will be 2**sizeExponent */ this.sizeExponent = ParamConfig.INTEGER(10, { range: [4, 14], rangeLocked: [true, true] }); /** @param array size */ this.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 */ this.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. */ this.normalRange = ParamConfig.BOOLEAN(1, effectParamsOptions(paramCallback)); /** @param groups the FFT frequency bands into octave bands */ this.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, }); this.octaveDivisions = ParamConfig.INTEGER(1, { range: [1, 2], rangeLocked: [true, true], visibleIf: { asOctaves: 1 }, separatorAfter: true }); /** @param display range param */ this.updateRangeParam = ParamConfig.BOOLEAN(1, { cook: false, callback: (node) => { FFTAudioNode.PARAM_CALLBACK_updateUpdateRangeParam(node); } }); /** @param range value */ this.range = ParamConfig.VECTOR2([0, 0], { visibleIf: { updateRangeParam: 1 }, editable: false, cook: false }); /** @param accumulated range */ this.maxRange = ParamConfig.VECTOR2(RANGE_DEFAULT, { visibleIf: { updateRangeParam: 1 }, editable: false, cook: false }); /** @param resetMaxRange */ this.resetMaxRange = ParamConfig.BUTTON(null, { visibleIf: { updateRangeParam: 1 }, callback: (node) => { FFTAudioNode.PARAM_CALLBACK_resetMaxRange(node); } }); } } const ParamsConfig = new FFTAudioParamsConfig(); export class FFTAudioNode extends BaseAnalyserAudioNode { constructor() { super(...arguments); this.paramsConfig = ParamsConfig; } static type() { return AudioNodeAnalyserType.FFT; } initializeNode() { this.io.inputs.setCount(1); } cook(inputContents) { 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)) { const octaveValues = this._octaveValues(); if (this._octaveDivisions() == 1) { convertFrequencyBandsToOctaveBands(fftValues, octaveValues); } else { convertFrequencyBandsToOctaveBandsDivisions2(fftValues, octaveValues, this._octaveDivisions()); } return octaveValues; } else { return fftValues; } } } _octaveDivisions() { return 2 ** (this.pv.octaveDivisions - 1); } _octaveValues() { const requiredSize = isBooleanTrue(this.pv.asOctaves) ? this.pv.sizeExponent * this._octaveDivisions() : this.pv.sizeExponent; if (this.__octaveValues && this.__octaveValues.length != requiredSize) { this.__octaveValues = void 0; } return this.__octaveValues = this.__octaveValues || new Float32Array(requiredSize); } _effect() { return this.__effect__ = this.__effect__ || this._createEffect(); } _createEffect() { return new FFT({ size: this._FFTSize(), normalRange: isBooleanTrue(this.pv.normalRange), smoothing: this.pv.smoothing }); } _resetEffect() { if (this.__effect__) { this.__effect__.dispose(); this.__effect__ = void 0; } } static PARAM_CALLBACK_updateEffect(node) { node._updateEffect(); } _updateEffect() { const effect = this._effect(); effect.normalRange = isBooleanTrue(this.pv.normalRange); effect.smoothing = this.pv.smoothing; } _FFTSize() { return 2 ** this.pv.sizeExponent; } /* * UPDATE RANGE PARAM */ static PARAM_CALLBACK_updateUpdateRangeParam(node) { node._updateRangeParam(); node._updateOnTickHook(); } static PARAM_CALLBACK_resetMaxRange(node) { node.p.maxRange.set(RANGE_DEFAULT); } _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 */ _updateOnTickHook() { if (isBooleanTrue(this.pv.updateRangeParam)) { this._registerOnTickHook(); } else { this._unRegisterOnTickHook(); } } async _registerOnTickHook() { if (this.scene().registeredBeforeTickCallbacks().has(this._tickCallbackName())) { return; } this.scene().registerOnBeforeTick(this._tickCallbackName(), this._updateRangeParam.bind(this)); } async _unRegisterOnTickHook() { this.scene().unRegisterOnBeforeTick(this._tickCallbackName()); } _tickCallbackName() { return `audio/FFT-${this.graphNodeId()}`; } }