@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
280 lines (279 loc) • 8.47 kB
JavaScript
"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()}`;
}
}