UNPKG

@polygonjs/polygonjs

Version:

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

280 lines (279 loc) 8.47 kB
"use strict"; import { TypedCopNode } from "./_Base"; import { DataTexture } from "three"; import { NodeParamsConfig, ParamConfig } from "../utils/params/ParamsConfig"; import { AUDIO_ANALYSER_NODES, NodeContext } from "../../poly/NodeContext"; import { isBooleanTrue } from "../../../core/Type"; import { NearestFilter, RGBAFormat, FloatType } from "three"; const OFFSET_BY_CHANNEL = { R: 0, G: 1, B: 2, A: 3 }; const CHANNELS = ["R", "G", "B", "A"]; const TEXTURE_ROWS = 2; const BYTE_SIZE = 1; const DEFAULT_SPEED = 0.04; class AudioAnalyserCopParamsConfig extends NodeParamsConfig { constructor() { super(...arguments); /** @param if off, the texture will not be updated */ this.activeR = ParamConfig.BOOLEAN(0); /** @param audio node to read data from, into the RED channel */ this.audioNodeR = ParamConfig.NODE_PATH("", { nodeSelection: { context: NodeContext.AUDIO, types: AUDIO_ANALYSER_NODES }, visibleIf: { activeR: 1 } }); /** @param decibel range */ this.rangeR = ParamConfig.VECTOR2([0, 1], { visibleIf: { activeR: 1 } }); /** @param speed mult */ this.speedMultR = ParamConfig.FLOAT(DEFAULT_SPEED, { visibleIf: { activeR: 1 }, separatorAfter: true }); /** @param if off, the texture will not be updated */ this.activeG = ParamConfig.BOOLEAN(0); /** @param audio node to read data from, into the GREEN channel */ this.audioNodeG = ParamConfig.NODE_PATH("", { nodeSelection: { context: NodeContext.AUDIO, types: AUDIO_ANALYSER_NODES }, visibleIf: { activeG: 1 } }); /** @param decibel range */ this.rangeG = ParamConfig.VECTOR2([0, 1], { visibleIf: { activeG: 1 } }); /** @param speed mult */ this.speedMultG = ParamConfig.FLOAT(DEFAULT_SPEED, { visibleIf: { activeG: 1 }, separatorAfter: true }); /** @param if off, the texture will not be updated */ this.activeB = ParamConfig.BOOLEAN(0); /** @param audio node to read data from, into the BLUE channel */ this.audioNodeB = ParamConfig.NODE_PATH("", { nodeSelection: { context: NodeContext.AUDIO, types: AUDIO_ANALYSER_NODES }, visibleIf: { activeB: 1 } }); /** @param decibel range */ this.rangeB = ParamConfig.VECTOR2([0, 1], { visibleIf: { activeB: 1 } }); /** @param speed mult */ this.speedMultB = ParamConfig.FLOAT(DEFAULT_SPEED, { visibleIf: { activeB: 1 }, separatorAfter: true }); /** @param if off, the texture will not be updated */ this.activeA = ParamConfig.BOOLEAN(0); /** @param audio node to read data from, into the ALPHA channel */ this.audioNodeA = ParamConfig.NODE_PATH("", { nodeSelection: { context: NodeContext.AUDIO, types: AUDIO_ANALYSER_NODES }, visibleIf: { activeA: 1 } }); /** @param decibel range */ this.rangeA = ParamConfig.VECTOR2([0, 1], { visibleIf: { activeA: 1 } }); /** @param speed mult */ this.speedMultA = ParamConfig.FLOAT(DEFAULT_SPEED, { visibleIf: { activeA: 1 }, separatorAfter: true }); } } const ParamsConfig = new AudioAnalyserCopParamsConfig(); export class AudioAnalyserCopNode extends TypedCopNode { constructor() { super(...arguments); this.paramsConfig = ParamsConfig; this._audioNodesByChannel = {}; this._valuesByChannel = {}; } static type() { return "audioAnalyser"; } async cook() { this._initParamsByChannel(); await this._getAudioNodes(); this._registerOnTickHook(); this._updateTexture(1); this.cookController.endCook(); } dispose() { super.dispose(); this._unRegisterOnTickHook(); } async _getAudioNodes() { const promises = [ this._getAudioNode("R"), this._getAudioNode("G"), this._getAudioNode("B"), this._getAudioNode("A") ]; await Promise.all(promises); } async _getAudioNode(channel) { if (!this._paramSetByChannel) { return; } const paramSet = this._paramSetByChannel[channel]; if (!isBooleanTrue(paramSet.active.value)) { return; } const nodeParam = paramSet.node; const audioNode = nodeParam.value.nodeWithContext(NodeContext.AUDIO); if (!audioNode) { this.states.error.set("no audio analyser node found"); this.cookController.endCook(); return; } const audioAnalyserNode = audioNode; if (!audioAnalyserNode.getAnalyserValue) { return; } await audioNode.compute(); this._audioNodesByChannel[channel] = audioAnalyserNode; } _updateTexture(delta) { if (!this._paramSetByChannel) { return; } let maxSize = -1; for (const channel of CHANNELS) { const values = this._valuesForChannel(channel, this._paramSetByChannel[channel]); this._valuesByChannel[channel] = values; if (values) { const size = values.length; if (maxSize < size) { maxSize = size; } } } if (!this._dataTexture) { this._createDataTexture(maxSize); } else { if (this._dataTexture.image.width != maxSize) { this._createDataTexture(maxSize); } } if (!this._dataTexture) { return; } for (const channel of CHANNELS) { const values = this._valuesByChannel[channel]; if (values) { this._updateTextureChannel(channel, this._paramSetByChannel[channel], values, this._dataTexture, delta); } } } _valuesForChannel(channel, paramSet) { const audioNode = this._audioNodesByChannel[channel]; if (!audioNode) { return; } if (!isBooleanTrue(paramSet.active.value)) { return; } return audioNode.getAnalyserValue(); } async _updateTextureChannel(channel, paramSet, values, texture, delta) { if (!this._dataTexture) { return; } const columns = this._dataTexture.image.width; const offset = OFFSET_BY_CHANNEL[channel]; const min = paramSet.range.x.value; const max = paramSet.range.y.value; const data = texture.image.data; const row2Offset = columns * 4; const speedMult = paramSet.speedMult.value; for (let i = 0; i < columns; i++) { const normalized = (values[i] - min) / (max - min); const clamped = Math.max(0, Math.min(1, normalized)); const v = clamped * BYTE_SIZE; const arrayIndex = i * 4 + offset; const prevValue = data[arrayIndex]; data[arrayIndex] = v; const speed = speedMult * (v - prevValue) / delta; data[row2Offset + arrayIndex] = /*HALF_BYTE_SIZE +*/ speed; } texture.needsUpdate = true; } _createDataTexture(valuesSize) { if (valuesSize <= 0) { return; } const height = TEXTURE_ROWS; const width = valuesSize; const size = width * height * 4; const pixelBuffer = new Float32Array(size); pixelBuffer.fill(0); for (let i = 0; i < size; i++) { pixelBuffer[i * 4 + 3] = BYTE_SIZE; } const texture = new DataTexture(pixelBuffer, width, height, RGBAFormat, FloatType); texture.minFilter = NearestFilter; texture.magFilter = NearestFilter; this._dataTexture = texture; this.setTexture(this._dataTexture); } _initParamsByChannel() { this._paramSetByChannel = this._paramSetByChannel || { R: { active: this.p.activeR, node: this.p.audioNodeR, range: this.p.rangeR, speedMult: this.p.speedMultR }, G: { active: this.p.activeG, node: this.p.audioNodeG, range: this.p.rangeG, speedMult: this.p.speedMultG }, B: { active: this.p.activeB, node: this.p.audioNodeB, range: this.p.rangeB, speedMult: this.p.speedMultB }, A: { active: this.p.activeA, node: this.p.audioNodeA, range: this.p.rangeA, speedMult: this.p.speedMultA } }; } /* * REGISTER TICK CALLBACK */ async _registerOnTickHook() { if (this.scene().registeredBeforeTickCallbacks().has(this._tickCallbackName())) { return; } this.scene().registerOnBeforeTick(this._tickCallbackName(), this._updateTexture.bind(this)); } async _unRegisterOnTickHook() { this.scene().unRegisterOnBeforeTick(this._tickCallbackName()); } _tickCallbackName() { return `cop/audioAnalyserNode-${this.graphNodeId()}`; } }