Skip to content

Commit 799952b

Browse files
author
Parsa Azari
committed
Restore missing files: AudioManager.js, PostProcessingManager.js, corridorSystem.js, and shaderStrings.js
1 parent 96a35ae commit 799952b

File tree

3 files changed

+1529
-0
lines changed

3 files changed

+1529
-0
lines changed

src/core/audio/AudioManager.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* @class AudioManager
3+
* @description Handles loading, playback, and management of audio assets using Tone.js.
4+
* Architected for generative, reactive soundscapes.
5+
*/
6+
import * as Tone from 'tone';
7+
8+
// === Just Intonation Ratios for Drone Voices ===
9+
const JUST_RATIOS = [1, 5/4, 3/2, 7/4]; // Unison, major third, perfect fifth, harmonic seventh
10+
const ROOT_CYCLE = [65.406, 69.296, 77.782, 87.307]; // C2, D♭2, E♭2, F2 (Hz)
11+
const ROOT_GLIDE_TIME = 8; // seconds for smooth root transitions
12+
const ROOT_SHIFT_MIN = 60; // seconds
13+
const ROOT_SHIFT_MAX = 180; // seconds
14+
15+
function createDroneVoice(rootFreq, ratio, lfoSettings, reverb, output) {
16+
// Voice frequency
17+
const freq = rootFreq * ratio;
18+
// Oscillator
19+
const osc = new Tone.Oscillator({
20+
frequency: freq,
21+
type: 'sine',
22+
volume: -18
23+
});
24+
// Amplitude
25+
const gain = new Tone.Gain(0.15);
26+
// Spatialization
27+
const pan = new Tone.Panner3D({
28+
panningModel: 'HRTF',
29+
positionX: Math.random() * 6 - 3,
30+
positionY: Math.random() * 2 - 1,
31+
positionZ: Math.random() * 6 - 3
32+
});
33+
// Filter
34+
const filter = new Tone.Filter(800, 'lowpass').set({ Q: 0.7 });
35+
// LFOs
36+
const ampLFO = new Tone.LFO(lfoSettings.ampRate, 0.05, 0.2);
37+
ampLFO.connect(gain.gain);
38+
const panLFO = new Tone.LFO(lfoSettings.panRate, -2, 2);
39+
panLFO.connect(pan.positionX);
40+
const filterLFO = new Tone.LFO(lfoSettings.filterRate, 400, 1200);
41+
filterLFO.connect(filter.frequency);
42+
// Chain
43+
osc.chain(gain, pan, filter, reverb, output);
44+
return { osc, gain, pan, filter, ampLFO, panLFO, filterLFO, ratio };
45+
}
46+
47+
export class AudioManager {
48+
constructor() {
49+
console.log("AudioManager (DroneArt): Initializing...");
50+
this.toneContext = Tone.getContext();
51+
this.masterLimiter = new Tone.Limiter(-1).toDestination();
52+
this.masterReverb = new Tone.Reverb({ decay: 7, preDelay: 0.04, wet: 0.4 }).connect(this.masterLimiter);
53+
this.droneVoices = [];
54+
this.rootIndex = 0;
55+
this.rootFreq = ROOT_CYCLE[this.rootIndex];
56+
this.rootShiftTimer = null;
57+
this.isActive = false;
58+
this.playerSpeed = 0;
59+
this.zone = 'default';
60+
// Pre-create voices (but don't connect yet)
61+
this._initVoices();
62+
}
63+
64+
_initVoices() {
65+
this.droneVoices = [];
66+
for (let i = 0; i < JUST_RATIOS.length; i++) {
67+
const lfoSettings = {
68+
ampRate: 0.02 + Math.random() * 0.04, // 0.02–0.06 Hz
69+
panRate: 0.01 + Math.random() * 0.03, // 0.01–0.04 Hz
70+
filterRate: 0.015 + Math.random() * 0.03 // 0.015–0.045 Hz
71+
};
72+
const voice = createDroneVoice(this.rootFreq, JUST_RATIOS[i], lfoSettings, this.masterReverb, Tone.Destination);
73+
this.droneVoices.push(voice);
74+
}
75+
}
76+
77+
/**
78+
* Resume the AudioContext after user interaction
79+
* Must be called in response to a user gesture (click, tap, keypress)
80+
*/
81+
async resumeAudioContext() {
82+
try {
83+
await Tone.start();
84+
console.log("AudioContext resumed successfully after user gesture");
85+
return true;
86+
} catch (error) {
87+
console.error("Failed to resume AudioContext:", error);
88+
return false;
89+
}
90+
}
91+
92+
async startDroneInstallation() {
93+
if (this.isActive) return;
94+
95+
// First ensure AudioContext is running
96+
const audioContextState = Tone.getContext().state;
97+
if (audioContextState !== "running") {
98+
console.log("AudioContext not running, attempting to resume...");
99+
try {
100+
await this.resumeAudioContext();
101+
} catch (error) {
102+
console.error("Could not resume AudioContext:", error);
103+
return false;
104+
}
105+
}
106+
107+
this.isActive = true;
108+
// Connect and start all voices
109+
for (const v of this.droneVoices) {
110+
v.osc.start();
111+
v.ampLFO.start();
112+
v.panLFO.start();
113+
v.filterLFO.start();
114+
}
115+
this._scheduleRootShift();
116+
console.log("Drone installation started.");
117+
return true;
118+
}
119+
120+
stopDroneInstallation() {
121+
if (!this.isActive) return;
122+
for (const v of this.droneVoices) {
123+
v.osc.stop();
124+
v.ampLFO.stop();
125+
v.filterLFO.stop();
126+
v.panLFO.stop();
127+
v.osc.disconnect();
128+
v.gain.disconnect();
129+
v.pan.disconnect();
130+
v.filter.disconnect();
131+
}
132+
this.isActive = false;
133+
if (this.rootShiftTimer) {
134+
clearTimeout(this.rootShiftTimer);
135+
this.rootShiftTimer = null;
136+
}
137+
console.log("Drone installation stopped.");
138+
}
139+
140+
_scheduleRootShift() {
141+
if (!this.isActive) return;
142+
const nextTime = ROOT_SHIFT_MIN * 1000 + Math.random() * (ROOT_SHIFT_MAX - ROOT_SHIFT_MIN) * 1000;
143+
this.rootShiftTimer = setTimeout(() => {
144+
this._shiftRoot();
145+
this._scheduleRootShift();
146+
}, nextTime);
147+
}
148+
149+
_shiftRoot() {
150+
this.rootIndex = (this.rootIndex + 1) % ROOT_CYCLE.length;
151+
const newRoot = ROOT_CYCLE[this.rootIndex];
152+
for (let i = 0; i < this.droneVoices.length; i++) {
153+
const v = this.droneVoices[i];
154+
const targetFreq = newRoot * v.ratio;
155+
v.osc.frequency.rampTo(targetFreq, ROOT_GLIDE_TIME);
156+
}
157+
this.rootFreq = newRoot;
158+
console.log(`Root shifted to ${newRoot.toFixed(2)} Hz`);
159+
}
160+
161+
setPlayerMovement(speed) {
162+
this.playerSpeed = speed;
163+
// More movement = faster LFOs, brighter filters, more volume
164+
for (const v of this.droneVoices) {
165+
v.ampLFO.frequency.rampTo(0.02 + speed * 0.1, 2);
166+
v.panLFO.frequency.rampTo(0.01 + speed * 0.08, 2);
167+
v.filterLFO.frequency.rampTo(0.015 + speed * 0.09, 2);
168+
v.filter.frequency.rampTo(800 + speed * 800, 3);
169+
v.gain.gain.rampTo(0.15 + speed * 0.1, 2);
170+
}
171+
this.masterReverb.wet.rampTo(0.4 + speed * 0.2, 2);
172+
}
173+
174+
setZone(zoneName) {
175+
this.zone = zoneName;
176+
// Example: breakRoom = more volume, wellness = softer, mdr = more reverb
177+
let wet = 0.4, gain = 0.15;
178+
if (zoneName === 'breakRoom') { wet = 0.55; gain = 0.22; }
179+
if (zoneName === 'wellness') { wet = 0.25; gain = 0.10; }
180+
if (zoneName === 'mdr') { wet = 0.5; gain = 0.18; }
181+
this.masterReverb.wet.rampTo(wet, 3);
182+
for (const v of this.droneVoices) {
183+
v.gain.gain.rampTo(gain, 3);
184+
}
185+
}
186+
187+
onPlayerStillness(duration) {
188+
// The longer the stillness, the slower/darker/softer the sound
189+
const t = Math.min(duration, 30) / 30;
190+
for (const v of this.droneVoices) {
191+
v.ampLFO.frequency.rampTo(0.01 + 0.01 * (1 - t), 4);
192+
v.filter.frequency.rampTo(400 + 200 * (1 - t), 4);
193+
v.gain.gain.rampTo(0.08 + 0.07 * (1 - t), 4);
194+
}
195+
this.masterReverb.wet.rampTo(0.2 + 0.2 * (1 - t), 4);
196+
}
197+
198+
/**
199+
* Clean up all audio resources
200+
*/
201+
dispose() {
202+
this.stopDroneInstallation();
203+
204+
// Dispose of all Tone.js objects
205+
if (this.masterLimiter) {
206+
this.masterLimiter.dispose();
207+
}
208+
209+
if (this.masterReverb) {
210+
this.masterReverb.dispose();
211+
}
212+
213+
// Clear voice references
214+
this.droneVoices = [];
215+
}
216+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
2+
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
3+
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
4+
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
5+
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
6+
import { ColorCorrectionShader } from "three/examples/jsm/shaders/ColorCorrectionShader.js";
7+
import * as THREE from "three";
8+
9+
export class PostProcessingManager {
10+
constructor(scene, camera, renderer, options = {}) {
11+
this.scene = scene;
12+
this.camera = camera;
13+
this.renderer = renderer;
14+
15+
this.options = {
16+
useBloom: true,
17+
useColorCorrection: true,
18+
useAntiAliasing: true,
19+
bloomStrength: 1.0, // Reduced bloom strength for subtlety
20+
bloomRadius: 0.4,
21+
bloomThreshold: 0.85,
22+
...options,
23+
};
24+
25+
this.composer = null;
26+
this.passes = new Map();
27+
}
28+
29+
async initialize() {
30+
this.composer = new EffectComposer(this.renderer);
31+
32+
// Add render pass
33+
const renderPass = new RenderPass(this.scene, this.camera);
34+
this.composer.addPass(renderPass);
35+
this.passes.set("render", renderPass);
36+
37+
// Add optional passes based on options
38+
if (this.options.useBloom) {
39+
await this.setupBloom();
40+
}
41+
42+
if (this.options.useColorCorrection) {
43+
await this.setupColorCorrection();
44+
}
45+
46+
if (this.options.useAntiAliasing) {
47+
await this.setupAntiAliasing();
48+
}
49+
}
50+
51+
async setupBloom() {
52+
const bloomPass = new UnrealBloomPass(
53+
new THREE.Vector2(window.innerWidth, window.innerHeight),
54+
this.options.bloomStrength,
55+
this.options.bloomRadius,
56+
this.options.bloomThreshold
57+
);
58+
this.composer.addPass(bloomPass);
59+
this.passes.set("bloom", bloomPass);
60+
}
61+
62+
async setupColorCorrection() {
63+
const colorCorrectionPass = new ShaderPass(ColorCorrectionShader);
64+
// Adjust for cooler, slightly desaturated Severance look
65+
colorCorrectionPass.uniforms.powRGB.value = new THREE.Vector3(
66+
1.0, // Less power adjustment overall
67+
1.0,
68+
1.05 // Slight boost to blue power
69+
);
70+
colorCorrectionPass.uniforms.mulRGB.value = new THREE.Vector3(
71+
0.95, // Slightly reduce red contribution
72+
1.0, // Keep green contribution
73+
1.05 // Slightly boost blue contribution
74+
);
75+
this.composer.addPass(colorCorrectionPass);
76+
this.passes.set("colorCorrection", colorCorrectionPass);
77+
}
78+
79+
async setupAntiAliasing() {
80+
if (
81+
!this.renderer.capabilities.isWebGL2 &&
82+
!this.renderer.extensions.get("MSAA")
83+
) {
84+
const smaaPass = new SMAAPass(
85+
window.innerWidth * this.renderer.getPixelRatio(),
86+
window.innerHeight * this.renderer.getPixelRatio()
87+
);
88+
this.composer.addPass(smaaPass);
89+
this.passes.set("smaa", smaaPass);
90+
}
91+
}
92+
93+
setSize(width, height) {
94+
this.composer.setSize(width, height);
95+
96+
// Update individual passes if needed
97+
const bloomPass = this.passes.get("bloom");
98+
if (bloomPass) {
99+
bloomPass.resolution.set(width, height);
100+
}
101+
102+
const smaaPass = this.passes.get("smaa");
103+
if (smaaPass) {
104+
smaaPass.setSize(width, height);
105+
}
106+
}
107+
108+
render() {
109+
this.composer.render();
110+
}
111+
112+
dispose() {
113+
this.passes.forEach((pass) => {
114+
if (pass.dispose) {
115+
pass.dispose();
116+
}
117+
});
118+
119+
this.passes.clear();
120+
121+
if (this.composer) {
122+
this.composer.dispose();
123+
}
124+
}
125+
126+
// Utility methods for runtime modifications
127+
setBloomStrength(strength) {
128+
const bloomPass = this.passes.get("bloom");
129+
if (bloomPass) {
130+
bloomPass.strength = strength;
131+
}
132+
}
133+
134+
setBloomRadius(radius) {
135+
const bloomPass = this.passes.get("bloom");
136+
if (bloomPass) {
137+
bloomPass.radius = radius;
138+
}
139+
}
140+
141+
setBloomThreshold(threshold) {
142+
const bloomPass = this.passes.get("bloom");
143+
if (bloomPass) {
144+
bloomPass.threshold = threshold;
145+
}
146+
}
147+
148+
setColorCorrection(powRGB, mulRGB) {
149+
const colorCorrectionPass = this.passes.get("colorCorrection");
150+
if (colorCorrectionPass) {
151+
if (powRGB) colorCorrectionPass.uniforms.powRGB.value.copy(powRGB);
152+
if (mulRGB) colorCorrectionPass.uniforms.mulRGB.value.copy(mulRGB);
153+
}
154+
}
155+
156+
/**
157+
* Set quality level for adaptive post-processing
158+
* @param {number} quality - 0.0 to 1.0
159+
*/
160+
setQualityLevel(quality) {
161+
// Bloom and AA off if quality < 0.7
162+
const bloomPass = this.passes.get("bloom");
163+
if (bloomPass) bloomPass.enabled = quality >= 0.7;
164+
const smaaPass = this.passes.get("smaa");
165+
if (smaaPass) smaaPass.enabled = quality >= 0.7;
166+
// Color correction off if quality < 0.5
167+
const colorCorrectionPass = this.passes.get("colorCorrection");
168+
if (colorCorrectionPass) colorCorrectionPass.enabled = quality >= 0.5;
169+
}
170+
}

0 commit comments

Comments
 (0)