This commit is contained in:
Andras Schmelczer 2026-05-13 21:07:10 +01:00
commit 39b0160064
136 changed files with 7129 additions and 1950 deletions

View file

@ -1,3 +1,4 @@
import { appConfig } from '../config';
import { clamp } from '../utils/clamp';
import { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
@ -11,6 +12,7 @@ export class GardenAudioGraph {
private delayNode: DelayNode | null = null;
private delayFeedback: GainNode | null = null;
private delayOutput: GainNode | null = null;
private hasUnlocked = false;
public constructor(private readonly config: GardenAudioConfig) {}
@ -50,6 +52,26 @@ export class GardenAudioGraph {
return context;
}
// iOS WebKit (Safari + Chrome iOS) only fully unlocks audio output once
// a buffer source has been started inside a user-gesture handler. Calling
// resume() alone leaves the context "running" but silent.
public unlock(): void {
if (!this.context || this.hasUnlocked) {
return;
}
const buffer = this.context.createBuffer(
1,
appConfig.audioEngine.graph.unlockBufferLength,
appConfig.audioEngine.graph.unlockSampleRate
);
const source = this.context.createBufferSource();
source.buffer = buffer;
source.connect(this.context.destination);
source.start(0);
this.hasUnlocked = true;
}
public setMasterGain(targetGain: number, timeConstantSeconds: number): void {
if (!this.context || !this.masterGain) {
return;
@ -70,7 +92,7 @@ export class GardenAudioGraph {
this.delayNode.delayTime.setTargetAtTime(
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
this.context.currentTime,
0.12
appConfig.audioEngine.graph.delayTimeRampSeconds
);
}
@ -83,17 +105,22 @@ export class GardenAudioGraph {
this.delayNode.delayTime.setTargetAtTime(
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
now,
0.12
appConfig.audioEngine.graph.delayTimeRampSeconds
);
this.delayFeedback.gain.setTargetAtTime(
this.config.delay.enabled
? clamp(this.config.delay.feedback + activity * 0.08, 0.04, 0.32)
: 0,
clamp(
this.config.delay.feedback +
activity * appConfig.audioEngine.graph.delayActivityFeedbackWeight,
appConfig.audioEngine.graph.delayFeedbackMin,
appConfig.audioEngine.graph.delayFeedbackMax
),
now,
this.config.updateRampSeconds
);
this.delayOutput.gain.setTargetAtTime(
this.config.delay.enabled ? this.config.delay.wetGain * (0.65 + activity * 0.5) : 0,
this.config.delay.wetGain *
(appConfig.audioEngine.graph.delayOutputBase +
activity * appConfig.audioEngine.graph.delayOutputActivityWeight),
now,
this.config.updateRampSeconds
);
@ -106,7 +133,11 @@ export class GardenAudioGraph {
}
if (this.masterGain && context.state !== 'closed') {
this.masterGain.gain.setTargetAtTime(0.0001, context.currentTime, 0.015);
this.masterGain.gain.setTargetAtTime(
appConfig.audioEngine.graph.closeGain,
context.currentTime,
appConfig.audioEngine.graph.closeRampSeconds
);
}
this.clearNodes();
@ -123,8 +154,8 @@ export class GardenAudioGraph {
const delayOutput = context.createGain();
delayNode.delayTime.value = this.config.delay.timeSeconds;
delayFeedback.gain.value = this.config.delay.enabled ? this.config.delay.feedback : 0;
delayOutput.gain.value = this.config.delay.enabled ? this.config.delay.wetGain : 0;
delayFeedback.gain.value = this.config.delay.feedback;
delayOutput.gain.value = this.config.delay.wetGain;
delayInput.connect(delayNode);
delayNode.connect(delayFeedback);
@ -140,7 +171,7 @@ export class GardenAudioGraph {
private createBuses(context: AudioContext, masterGain: GainNode): void {
this.eventBus = context.createGain();
this.eventBus.gain.value = 1;
this.eventBus.gain.value = appConfig.audioEngine.graph.eventBusGain;
this.eventBus.connect(masterGain);
}
@ -149,7 +180,10 @@ export class GardenAudioGraph {
const data = buffer.getChannelData(0);
for (let index = 0; index < data.length; index++) {
data[index] = Math.random() * 2 - 1;
data[index] =
appConfig.audioEngine.graph.noiseMin +
Math.random() *
(appConfig.audioEngine.graph.noiseMax - appConfig.audioEngine.graph.noiseMin);
}
return buffer;
@ -164,5 +198,6 @@ export class GardenAudioGraph {
this.delayNode = null;
this.delayFeedback = null;
this.delayOutput = null;
this.hasUnlocked = false;
}
}