import { createAudioPanNode } from './audio-pan-node'; import type { GardenAudioGraph } from './garden-audio-graph'; import type { NoiseBurst } from './garden-audio-types'; const noiseBurstTuning = { attackSeconds: 0.004, filterQ: 1.4, offsetRandomSeconds: 0.4, scheduleAheadSeconds: 0.002, silentGain: 0.0001, filterType: 'bandpass' as BiquadFilterType, }; export class NoiseBurstPlayer { public constructor(private readonly graph: GardenAudioGraph) {} public play({ startTime, durationSeconds, gain, filterHz, pan }: NoiseBurst): void { const { context, eventBus, noiseBus, noiseBuffer } = this.graph; const outputBus = noiseBus ?? eventBus; if (!context || !outputBus || !noiseBuffer) { return; } const scheduledStart = Math.max( context.currentTime + noiseBurstTuning.scheduleAheadSeconds, startTime ); const source = context.createBufferSource(); const filter = context.createBiquadFilter(); const envelope = context.createGain(); const panNode = createAudioPanNode(context, pan, scheduledStart); const stopAt = scheduledStart + durationSeconds; source.buffer = noiseBuffer; filter.type = noiseBurstTuning.filterType; filter.frequency.setValueAtTime(filterHz, scheduledStart); filter.Q.value = noiseBurstTuning.filterQ; envelope.gain.setValueAtTime(noiseBurstTuning.silentGain, scheduledStart); envelope.gain.exponentialRampToValueAtTime( Math.max(noiseBurstTuning.silentGain, gain), scheduledStart + noiseBurstTuning.attackSeconds ); envelope.gain.exponentialRampToValueAtTime(noiseBurstTuning.silentGain, stopAt); source.connect(filter); filter.connect(envelope); envelope.connect(panNode.input); panNode.output.connect(outputBus); source.start(scheduledStart, Math.random() * noiseBurstTuning.offsetRandomSeconds); source.stop(stopAt); source.addEventListener( 'ended', () => { source.disconnect(); filter.disconnect(); envelope.disconnect(); panNode.disconnect(); }, { once: true } ); } }