,
This commit is contained in:
parent
70423851ba
commit
1fe5015056
55 changed files with 2036 additions and 685 deletions
|
|
@ -2,6 +2,9 @@ import type { GardenAudioEngineConfig } from '../config';
|
|||
import { clamp } from '../utils/clamp';
|
||||
import { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
|
||||
const UNLOCK_TICK_SECONDS = 0.035;
|
||||
const UNLOCK_TICK_FREQUENCY_HZ = 440;
|
||||
|
||||
export class GardenAudioGraph {
|
||||
public context: AudioContext | null = null;
|
||||
public eventBus: GainNode | null = null;
|
||||
|
|
@ -12,7 +15,6 @@ 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,
|
||||
|
|
@ -28,23 +30,26 @@ export class GardenAudioGraph {
|
|||
return null;
|
||||
}
|
||||
|
||||
const context = new AudioContext({ latencyHint: 'interactive' });
|
||||
const AudioContextConstructor = globalThis.AudioContext;
|
||||
if (!AudioContextConstructor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let context: AudioContext;
|
||||
try {
|
||||
context = new AudioContextConstructor({ latencyHint: 'interactive' });
|
||||
} catch {
|
||||
context = new AudioContextConstructor();
|
||||
}
|
||||
const masterGain = context.createGain();
|
||||
const highPass = context.createBiquadFilter();
|
||||
const compressor = context.createDynamicsCompressor();
|
||||
|
||||
masterGain.gain.value = 0;
|
||||
highPass.type = 'highpass';
|
||||
highPass.frequency.value = this.config.highPassFrequencyHz;
|
||||
compressor.threshold.value = this.config.compressor.thresholdDb;
|
||||
compressor.knee.value = this.config.compressor.kneeDb;
|
||||
compressor.ratio.value = this.config.compressor.ratio;
|
||||
compressor.attack.value = this.config.compressor.attackSeconds;
|
||||
compressor.release.value = this.config.compressor.releaseSeconds;
|
||||
|
||||
masterGain.connect(highPass);
|
||||
highPass.connect(compressor);
|
||||
compressor.connect(context.destination);
|
||||
highPass.connect(context.destination);
|
||||
|
||||
this.context = context;
|
||||
this.masterGain = masterGain;
|
||||
|
|
@ -55,24 +60,37 @@ 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.
|
||||
// iOS WebKit can report "running" while the hardware output is still silent.
|
||||
// A very short, nearly inaudible oscillator in the gesture stack is more
|
||||
// reliable than a fully silent buffer on recent Safari versions.
|
||||
public unlock(): void {
|
||||
if (!this.context || this.hasUnlocked) {
|
||||
if (!this.context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = this.context.createBuffer(
|
||||
1,
|
||||
this.engineConfig.graph.unlockBufferLength,
|
||||
this.engineConfig.graph.unlockSampleRate
|
||||
const now = this.context.currentTime;
|
||||
const source = this.context.createOscillator();
|
||||
const gain = this.context.createGain();
|
||||
|
||||
source.type = 'sine';
|
||||
source.frequency.setValueAtTime(UNLOCK_TICK_FREQUENCY_HZ, now);
|
||||
gain.gain.setValueAtTime(this.engineConfig.piano.minGain, now);
|
||||
gain.gain.exponentialRampToValueAtTime(
|
||||
this.engineConfig.piano.minGain,
|
||||
now + UNLOCK_TICK_SECONDS
|
||||
);
|
||||
source.connect(gain);
|
||||
gain.connect(this.context.destination);
|
||||
source.start(now);
|
||||
source.stop(now + UNLOCK_TICK_SECONDS);
|
||||
source.addEventListener(
|
||||
'ended',
|
||||
() => {
|
||||
source.disconnect();
|
||||
gain.disconnect();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
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 {
|
||||
|
|
@ -201,6 +219,5 @@ export class GardenAudioGraph {
|
|||
this.delayNode = null;
|
||||
this.delayFeedback = null;
|
||||
this.delayOutput = null;
|
||||
this.hasUnlocked = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue