Update framework stuff

master
Talon 2025-07-22 23:52:43 +01:00
parent 40425066a1
commit ac172e4cd4
21 changed files with 344 additions and 41 deletions

View File

@ -34,6 +34,7 @@ export default class vec3 {
multiplyByMat3(matrix: mat3, dest?: vec3): vec3;
multiplyByQuat(quaternion: quat, dest?: vec3): vec3;
toQuat(dest?: quat): quat;
rotateAroundAxis(axis: vec3, angle: number): vec3;
static cross(vector: vec3, vector2: vec3, dest?: vec3): vec3;
static dot(vector: vec3, vector2: vec3): number;
static distance(vector: vec3, vector2: vec3): number;
@ -45,4 +46,7 @@ export default class vec3 {
static product(vector: vec3, vector2: vec3, dest?: vec3): vec3;
static quotient(vector: vec3, vector2: vec3, dest?: vec3): vec3;
static rotate(value: vec3, rotation: quat, dest?: vec3): vec3;
static max(a: vec3, b: vec3): vec3;
static min(a: vec3, b: vec3): vec3;
static lerp(a: vec3, b: vec3, t: number): vec3;
}

View File

@ -170,6 +170,26 @@ export default class vec3 {
dest.w = c.x * c.y * c.z + s.x * s.y * s.z;
return dest;
}
rotateAroundAxis(axis, angle) {
const normalizedAxis = axis.normalize();
const sinAngle = Math.sin(angle / 2);
const cosAngle = Math.cos(angle / 2);
// Create a quaternion representing the rotation
const rotationQuat = new quat([
normalizedAxis.x * sinAngle,
normalizedAxis.y * sinAngle,
normalizedAxis.z * sinAngle,
cosAngle
]);
// Quaternion for the vector (considering the vector as a quaternion with a w value of 0)
const vecQuat = new quat([this.x, this.y, this.z, 0]);
// Conjugate of the rotation quaternion
const rotationQuatConjugate = rotationQuat.conjugate();
// Rotate the vector using quaternion multiplication: q * v * q^(-1)
const rotatedVecQuat = rotationQuat.multiply(vecQuat).multiply(rotationQuatConjugate);
// Return the rotated vector
return new vec3([rotatedVecQuat.x, rotatedVecQuat.y, rotatedVecQuat.z]);
}
static cross(vector, vector2, dest) {
if (!dest) {
dest = new vec3();
@ -283,6 +303,27 @@ export default class vec3 {
dest.z = value.z + z * rotation.w + (rotation.x * y - rotation.y * x);
return dest;
}
static max(a, b) {
return new vec3([
Math.max(a.x, b.x),
Math.max(a.y, b.y),
Math.max(a.z, b.z),
]);
}
static min(a, b) {
return new vec3([
Math.min(a.x, b.x),
Math.min(a.y, b.y),
Math.min(a.z, b.z),
]);
}
static lerp(a, b, t) {
return new vec3([
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t,
]);
}
}
vec3.zero = new vec3([0, 0, 0]);
vec3.one = new vec3([1, 1, 1]);

View File

@ -27,7 +27,12 @@ export default class ResonatorAudioContext {
}
decodeAudioData(data) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.context.decodeAudioData(data);
try {
return yield this.context.decodeAudioData(data);
}
catch (e) {
console.error(e);
}
});
}
createPanner() {

View File

@ -4,8 +4,8 @@ import BaseEffect from './effects/base-effect';
export default class AudioGraph {
private master;
private effectsBus;
private worldBus;
private secondaryBus;
worldBus: AudioNode;
secondaryBus: AudioNode;
private effects;
private scene;
private context;

View File

@ -5,6 +5,10 @@ export default class Convolver extends BaseEffect {
private buffer;
private channelSplitter;
private channelMerger;
private outputGain;
private volume;
constructor(context: ResonatorAudioContext, graph: AudioGraph, params: any);
setBuffer(buffer: AudioBuffer): void;
setVolume(volume: number): void;
connectInput(node: AudioNode): void;
}

View File

@ -2,10 +2,24 @@ import BaseEffect from './base-effect';
export default class Convolver extends BaseEffect {
constructor(context, graph, params) {
super(context, graph, params);
this.volume = 0.25;
console.log(`Creating convolver`);
this.effectNode = this.context.getContext().createConvolver();
this.effectNode.normalize = true;
this.effectNode.buffer = this.effectParams.buffer;
}
setBuffer(buffer) {
this.buffer = buffer;
if (this.effectNode) {
this.effectNode.buffer = buffer;
}
}
setVolume(volume) {
this.volume = volume;
if (this.outputGain) {
this.outputGain.gain.setValueAtTime(this.volume, this.context.getContext().currentTime);
}
}
connectInput(node) {
this.channelSplitter = this.context.getContext().createChannelSplitter(2);
this.channelMerger = this.context.getContext().createChannelMerger(2);
@ -13,8 +27,11 @@ export default class Convolver extends BaseEffect {
this.channelSplitter.connect(this.channelMerger, 1, 0);
this.channelSplitter.connect(this.channelMerger, 0, 1);
this.channelSplitter.connect(this.channelMerger, 1, 1);
this.outputGain = this.context.getContext().createGain();
this.outputGain.gain.setValueAtTime(this.volume, this.context.getContext().currentTime);
node.connect(this.channelSplitter);
this.channelMerger.connect(this.effectNode);
this.channelMerger.connect(this.outputGain);
this.outputGain.connect(this.effectNode);
this.inputNode = node;
}
}

View File

@ -1,3 +1,5 @@
import ResonatorScene from './scenes/webaudio-scene';
import AudioGraph from './audio-graph';
import AudioSource from './sources/audio-source';
import { BaseLoader } from './loaders/base-loader';
import { BaseSource } from './sources/base-source';
@ -15,8 +17,12 @@ export default class Resonator {
loadImmediate(path: string, type?: SourceType): AudioSource;
stream(path: string, type?: SourceType): StreamingSource;
private createSource;
setEnvironmentImpulse(file: string): Promise<void>;
setEnvironmentImpulse(file: string, volume?: number): Promise<void>;
setEnvironmentImpulseBuffer(file: AudioBuffer, volume?: number): Promise<void>;
setListenerPosition(x: number, y: number, z: number): void;
setListenerOrientation(forward: any, up: any): void;
clearDataPool(): void;
getAudioContext(): AudioContext;
getAudioGraph(): AudioGraph;
getScene(): ResonatorScene;
}

View File

@ -51,18 +51,47 @@ export default class Resonator {
createSource(type, data) {
return new AudioSource(this.graph, this.scene, this.context, data);
}
setEnvironmentImpulse(file) {
setEnvironmentImpulse(file, volume = 0.25) {
return __awaiter(this, void 0, void 0, function* () {
if (this.environmentImpulse) {
this.graph.removeEffect(this.environmentImpulse);
if (file === null || file === '') {
if (this.environmentImpulse) {
this.graph.removeEffect(this.environmentImpulse);
this.environmentImpulse = null;
}
return;
}
if (file === null) {
if (this.environmentImpulse && file !== '') {
const buffer = yield this.dataPool.get(file);
this.environmentImpulse.setBuffer(buffer);
this.environmentImpulse.setVolume(volume);
return;
}
const buffer = yield this.dataPool.get(file);
this.environmentImpulse = new Convolver(this.context, this.graph, {
buffer
});
this.environmentImpulse.setVolume(volume);
this.graph.applyEffect(this.environmentImpulse);
});
}
setEnvironmentImpulseBuffer(file, volume = 0.25) {
return __awaiter(this, void 0, void 0, function* () {
if (file === null) {
if (this.environmentImpulse) {
this.graph.removeEffect(this.environmentImpulse);
this.environmentImpulse = null;
}
return;
}
if (this.environmentImpulse && file) {
this.environmentImpulse.setBuffer(file);
this.environmentImpulse.setVolume(volume);
return;
}
this.environmentImpulse = new Convolver(this.context, this.graph, {
buffer: file
});
this.environmentImpulse.setVolume(volume);
this.graph.applyEffect(this.environmentImpulse);
});
}
@ -75,4 +104,13 @@ export default class Resonator {
clearDataPool() {
this.dataPool.clear();
}
getAudioContext() {
return this.context.getContext();
}
getAudioGraph() {
return this.graph;
}
getScene() {
return this.scene;
}
}

View File

@ -1,9 +1,16 @@
import ResonatorAudioContext from '../audio-context';
import { EventBus } from '../../event-bus';
import ResonatorAudioContext from "../audio-context";
import { EventBus } from "../../event-bus";
export default class ResonatorScene extends EventBus {
scene: GainNode;
context: ResonatorAudioContext;
listener: AudioListener;
position: {
x: number;
y: number;
z: number;
};
orientation: any;
isFirefox: boolean;
constructor(context: ResonatorAudioContext);
init(): void;
createSource(): any;
@ -11,4 +18,5 @@ export default class ResonatorScene extends EventBus {
getInput(): any;
setListenerPosition(x: number, y: number, z: number): void;
setListenerOrientation(forward: any, rawup: any): void;
private checkIfFirefox;
}

View File

@ -1,12 +1,20 @@
// The code that deals with 3d audio
import { EventBus } from '../../event-bus';
import vec3 from '../../math/vec3';
import { EventBus } from "../../event-bus";
import vec3 from "../../math/vec3";
export default class ResonatorScene extends EventBus {
constructor(context) {
super();
this.position = { x: 0, y: 0, z: 0 };
this.isFirefox = false;
this.context = context;
this.scene = this.context.getContext().createGain();
this.listener = this.context.getContext().listener;
this.position = { x: 0, y: 0, z: 0 };
this.orientation = {
up: { x: 0, y: 1, z: 0 },
fwd: { x: 0, y: 0, z: -1 },
};
this.checkIfFirefox();
this.init();
}
init() {
@ -14,8 +22,8 @@ export default class ResonatorScene extends EventBus {
}
createSource() {
const node = this.context.getContext().createPanner();
node.panningModel = 'HRTF';
node.distanceModel = 'linear';
node.panningModel = "HRTF";
node.distanceModel = "linear";
node.maxDistance = 20;
node.refDistance = 2;
node.connect(this.scene);
@ -28,7 +36,17 @@ export default class ResonatorScene extends EventBus {
return this.scene;
}
setListenerPosition(x, y, z) {
this.listener.setPosition(x, y, z);
if (x === this.position.x && y === this.position.y && z == this.position.z)
return;
if (this.isFirefox) {
this.listener.setPosition(x, y, z);
}
else {
this.listener.positionX.setValueAtTime(x, this.context.getContext().currentTime);
this.listener.positionY.setValueAtTime(y, this.context.getContext().currentTime);
this.listener.positionZ.setValueAtTime(z, this.context.getContext().currentTime);
}
this.position = { x, y, z };
}
setListenerOrientation(forward, rawup) {
let fwd = new vec3([forward.x, forward.y, forward.z]);
@ -37,6 +55,25 @@ export default class ResonatorScene extends EventBus {
vec3.cross(up, fwd, up);
fwd.normalize();
up.normalize();
this.listener.setOrientation(fwd.x, fwd.y, fwd.z, up.x, up.y, up.z);
if (fwd.x === this.orientation.fwd.x && fwd.y === this.orientation.fwd.y &&
fwd.z === this.orientation.fwd.z && up.x === this.orientation.up.x &&
up.y === this.orientation.up.y && up.z === this.orientation.up.z)
return;
// this.listener.setOrientation(fwd.x, fwd.y, fwd.z, up.x, up.y, up.z);
if (this.isFirefox) {
this.listener.setOrientation(fwd.x, fwd.y, fwd.z, up.x, up.y, up.z);
}
else {
this.listener.forwardX.setValueAtTime(fwd.x, this.context.getContext().currentTime);
this.listener.forwardY.setValueAtTime(fwd.y, this.context.getContext().currentTime);
this.listener.forwardZ.setValueAtTime(fwd.z, this.context.getContext().currentTime);
this.listener.upX.setValueAtTime(up.x, this.context.getContext().currentTime);
this.listener.upY.setValueAtTime(up.y, this.context.getContext().currentTime);
this.listener.upZ.setValueAtTime(up.z, this.context.getContext().currentTime);
}
this.orientation = { fwd, up };
}
checkIfFirefox() {
this.isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
}
}

View File

@ -2,6 +2,7 @@ import ResonatorAudioContext from '../audio-context';
import AudioGraph from '../audio-graph';
import ResonatorScene from '../scenes/webaudio-scene';
import { BaseSource } from './base-source';
import { DistanceModel } from './distance-model';
import { SourceType } from './source-type';
export default class AudioSource implements BaseSource {
playing: boolean;
@ -18,6 +19,12 @@ export default class AudioSource implements BaseSource {
private volume;
private gain;
private type;
private distanceModel;
private maxDistance;
private refDistance;
private rollOffFactor;
private filter;
private filterFreq;
constructor(graph: AudioGraph, scene: ResonatorScene, context: ResonatorAudioContext, buffer?: AudioBuffer, type?: SourceType);
init(): void;
getBuffer(): AudioBuffer;
@ -34,4 +41,11 @@ export default class AudioSource implements BaseSource {
loop(value: boolean): void;
fadeOut(time: number): void;
fadeIn(time: number): void;
isPlaying(): boolean;
setDistanceModel(distanceModel: DistanceModel): void;
setMaxDistance(distance: number): void;
setRefDistance(ref: number): void;
setRollOffFactor(factor: number): void;
updateSpatialization(): void;
setFilterFrequency(value: number): void;
}

View File

@ -3,6 +3,7 @@
import { SourceType } from './source-type';
export default class AudioSource {
constructor(graph, scene, context, buffer = null, type = SourceType.WorldSource) {
this.filterFreq = 24000;
this.position = {
x: 0,
y: 0,
@ -19,6 +20,10 @@ export default class AudioSource {
}
init() {
this.gain = this.context.createGain();
this.filter = this.context.getContext().createBiquadFilter();
this.filter.type = "highshelf";
this.filter.frequency.value = this.filterFreq;
this.filter.gain.value = -60;
// bind methods so we can add and removve them from event listeners
this.stop = this.stop.bind(this);
}
@ -32,12 +37,15 @@ export default class AudioSource {
this.playOnLoad = false;
}
}
play(when = 0, offset = 0, duration = this.buffer ? this.buffer.duration : 0) {
play(when, offset, duration) {
if (!this.context)
return;
if (this.playing && this.node) {
this.stop();
}
if (!this.buffer) {
this.playOnLoad = true;
this.playing = true;
return;
}
if (!this.node) {
@ -47,7 +55,13 @@ export default class AudioSource {
}
if (this.node) {
this.node.playbackRate.value = this.playbackRate;
this.node.start(when, offset, duration);
// Have to do this, otherwise when we pass duration, the node will stop before it can loop.
if (duration) {
this.node.start(when, offset, duration);
}
else {
this.node.start(when, offset);
}
this.node.loop = this.looping;
this.playing = true;
if (this.sceneNode) {
@ -57,6 +71,8 @@ export default class AudioSource {
}
}
setPosition(x, y, z) {
if (x === this.position.x && y === this.position.y && z === this.position.z)
return;
this.position = {
x,
y,
@ -86,8 +102,10 @@ export default class AudioSource {
case SourceType.WorldSource:
if (!this.sceneNode) {
this.sceneNode = this.scene.createSource();
this.updateSpatialization();
}
this.node.connect(this.gain);
this.node.connect(this.filter);
this.filter.connect(this.gain);
this.gain.connect(this.sceneNode);
break;
case SourceType.UISource:
@ -145,4 +163,41 @@ export default class AudioSource {
}
this.gain.gain.exponentialRampToValueAtTime(this.volume, this.context.getContext().currentTime + time);
}
isPlaying() {
return this.playing;
}
setDistanceModel(distanceModel) {
this.distanceModel = distanceModel;
this.updateSpatialization();
}
setMaxDistance(distance) {
this.maxDistance = this.maxDistance;
this.updateSpatialization();
}
setRefDistance(ref) {
this.refDistance = ref;
this.updateSpatialization();
}
setRollOffFactor(factor) {
this.rollOffFactor = factor;
this.updateSpatialization();
}
updateSpatialization() {
if (this.sceneNode) {
if (this.distanceModel)
this.sceneNode.distanceModel = this.distanceModel;
if (this.refDistance)
this.sceneNode.refDistance = this.refDistance;
if (this.rollOffFactor)
this.sceneNode.rolloffFactor = this.rollOffFactor;
if (this.maxDistance)
this.sceneNode.maxDistance = this.maxDistance;
}
}
setFilterFrequency(value) {
this.filterFreq = value;
if (this.filter) {
this.filter.frequency.value = this.filterFreq;
}
}
}

View File

@ -9,4 +9,5 @@ export interface BaseSource {
fadeOut(time: number): void;
fadeIn(time: number): void;
destroy(): void;
isPlaying(): boolean;
}

View File

@ -0,0 +1,5 @@
export declare enum DistanceModel {
linear = "linear",
inverse = "inverse",
exponential = "exponential"
}

View File

@ -0,0 +1,6 @@
export var DistanceModel;
(function (DistanceModel) {
DistanceModel["linear"] = "linear";
DistanceModel["inverse"] = "inverse";
DistanceModel["exponential"] = "exponential";
})(DistanceModel || (DistanceModel = {}));

View File

@ -3,6 +3,7 @@ import AudioGraph from '../audio-graph';
import ResonatorScene from '../scenes/webaudio-scene';
import ResonatorAudioContext from '../audio-context';
import { SourceType } from './source-type';
import { DistanceModel } from './distance-model';
export declare class StreamingSource implements BaseSource {
private graph;
private scene;
@ -16,6 +17,12 @@ export declare class StreamingSource implements BaseSource {
private sceneNode;
private gain;
private position;
private distanceModel;
private maxDistance;
private refDistance;
private rollOffFactor;
private filter;
private filterFreq;
constructor(graph: AudioGraph, scene: ResonatorScene, context: ResonatorAudioContext, element: HTMLAudioElement, type?: SourceType);
private init;
play(when?: number, offset?: number, duration?: number): void;
@ -30,4 +37,11 @@ export declare class StreamingSource implements BaseSource {
loop(value: boolean): void;
fadeIn(time: number): void;
fadeOut(time: number): void;
isPlaying(): boolean;
setDistanceModel(distanceModel: DistanceModel): void;
setMaxDistance(distance: number): void;
setRefDistance(ref: number): void;
setRollOffFactor(factor: number): void;
updateSpatialization(): void;
setFilterFrequency(value: number): void;
}

View File

@ -6,6 +6,7 @@ export class StreamingSource {
this.context = context;
this.element = element;
this.type = type;
this.filterFreq = 24000;
this.position = {
x: 0,
y: 0,
@ -15,6 +16,10 @@ export class StreamingSource {
}
init() {
this.node = this.context.createMediaElementSource(this.element);
this.filter = this.context.getContext().createBiquadFilter();
this.filter.type = "lowshelf";
this.filter.gain.value = -60;
this.filter.frequency.value = this.filterFreq;
this.gain = this.context.createGain();
this.createConnections();
this.element.addEventListener('canplay', (event) => {
@ -50,8 +55,10 @@ export class StreamingSource {
case SourceType.WorldSource:
if (!this.sceneNode) {
this.sceneNode = this.scene.createSource();
this.updateSpatialization();
}
this.node.connect(this.gain);
this.node.connect(this.filter);
this.filter.connect(this.gain);
this.gain.connect(this.sceneNode);
break;
default:
@ -96,4 +103,37 @@ export class StreamingSource {
this.gain.gain.exponentialRampToValueAtTime(0.0001, this.context.getContext().currentTime + time);
setTimeout(() => this.stop(), time * 1000);
}
isPlaying() {
return this.playing;
}
setDistanceModel(distanceModel) {
this.distanceModel = distanceModel;
this.updateSpatialization();
}
setMaxDistance(distance) {
this.maxDistance = this.maxDistance;
this.updateSpatialization();
}
setRefDistance(ref) {
this.refDistance = ref;
this.updateSpatialization();
}
setRollOffFactor(factor) {
this.rollOffFactor = factor;
this.updateSpatialization();
}
updateSpatialization() {
if (this.sceneNode) {
this.sceneNode.distanceModel = this.distanceModel;
this.sceneNode.refDistance = this.refDistance;
this.sceneNode.rolloffFactor = this.rollOffFactor;
this.sceneNode.maxDistance = this.maxDistance;
}
}
setFilterFrequency(value) {
this.filterFreq = value;
if (this.filter) {
this.filter.frequency.value = this.filterFreq;
}
}
}

View File

@ -1,10 +1,12 @@
import { SchedulerNode } from './node';
export declare class RAFTimer {
isStarted: boolean;
private lastTime;
private currTime;
node: SchedulerNode;
constructor(node: SchedulerNode);
start(): void;
stop(): void;
schedule(): void;
handleResolve(): void;
handleResolve(dt: number): void;
}

View File

@ -1,5 +1,7 @@
export class RAFTimer {
constructor(node) {
this.lastTime = 0;
this.currTime = 0;
this.isStarted = false;
this.node = node;
}
@ -13,9 +15,17 @@ export class RAFTimer {
schedule() {
window.requestAnimationFrame(this.handleResolve.bind(this));
}
handleResolve() {
handleResolve(dt) {
if (this.node) {
this.node.func(1);
if (!this.lastTime) {
this.lastTime = dt;
this.node.func(1);
}
else {
const delta = dt - this.lastTime;
this.lastTime = dt;
this.node.func(delta / 1000);
}
if (this.isStarted) {
this.schedule();
}

View File

@ -10,5 +10,5 @@ export declare class Timer {
start(): void;
stop(): void;
schedule(): void;
handleResolve(): void;
handleResolve(dt: number): void;
}

View File

@ -3,34 +3,30 @@ export class Timer {
this.time = time;
this.node = node;
this.isStarted = false;
this.fluctuation = 0;
}
start() {
this.isStarted = true;
this.lastTime = performance.now();
this.schedule();
}
stop() {
if (this.isStarted) {
if (this.intervalID) {
clearTimeout(this.intervalID);
this.intervalID = null;
this.isStarted = false;
}
clearTimeout(this.intervalID);
this.isStarted = false;
}
}
schedule() {
let toWait = this.time;
if (this.lastTime) {
const fluc = Date.now() - this.lastTime;
this.fluctuation = fluc;
toWait -= fluc;
}
this.lastTime = Date.now();
this.intervalID = setTimeout(this.handleResolve.bind(this), toWait);
const now = performance.now();
const elapsed = now - this.lastTime;
this.fluctuation = elapsed - this.time;
this.lastTime = now;
const toWait = Math.max(0, this.time - this.fluctuation);
this.intervalID = window.setTimeout(() => this.handleResolve(elapsed), toWait);
}
handleResolve() {
this.lastTime = Date.now();
handleResolve(dt) {
if (this.node) {
this.node.func(this.time / this.lastTime);
this.node.func(dt / 1000); // Assuming time is in milliseconds
}
if (this.isStarted) {
this.schedule();