Add WIP sound generation
This commit is contained in:
parent
cb1df6f29e
commit
34ac200437
10 changed files with 1542 additions and 0 deletions
168
src/audio/garden-audio-graph.ts
Normal file
168
src/audio/garden-audio-graph.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { clamp } from '../utils/clamp';
|
||||
import { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
|
||||
export class GardenAudioGraph {
|
||||
public context: AudioContext | null = null;
|
||||
public eventBus: GainNode | null = null;
|
||||
public delayInput: GainNode | null = null;
|
||||
public noiseBuffer: AudioBuffer | null = null;
|
||||
|
||||
private masterGain: GainNode | null = null;
|
||||
private delayNode: DelayNode | null = null;
|
||||
private delayFeedback: GainNode | null = null;
|
||||
private delayOutput: GainNode | null = null;
|
||||
|
||||
public constructor(private readonly config: GardenAudioConfig) {}
|
||||
|
||||
public ensureContext(canCreate: boolean): AudioContext | null {
|
||||
if (this.context) {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
if (!canCreate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context = new AudioContext({ latencyHint: 'interactive' });
|
||||
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);
|
||||
|
||||
this.context = context;
|
||||
this.masterGain = masterGain;
|
||||
this.noiseBuffer = this.createNoiseBuffer(context);
|
||||
this.createDelay(context, masterGain);
|
||||
this.createBuses(context, masterGain);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
public setMasterGain(targetGain: number, timeConstantSeconds: number): void {
|
||||
if (!this.context || !this.masterGain) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.masterGain.gain.setTargetAtTime(
|
||||
targetGain,
|
||||
this.context.currentTime,
|
||||
timeConstantSeconds
|
||||
);
|
||||
}
|
||||
|
||||
public applyDelayProfile(profile: GardenAudioVibeProfile): void {
|
||||
if (!this.context || !this.delayNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.delayNode.delayTime.setTargetAtTime(
|
||||
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
|
||||
this.context.currentTime,
|
||||
0.12
|
||||
);
|
||||
}
|
||||
|
||||
public updateDelay(profile: GardenAudioVibeProfile, activity: number): void {
|
||||
if (!this.context || !this.delayNode || !this.delayFeedback || !this.delayOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = this.context.currentTime;
|
||||
this.delayNode.delayTime.setTargetAtTime(
|
||||
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
|
||||
now,
|
||||
0.12
|
||||
);
|
||||
this.delayFeedback.gain.setTargetAtTime(
|
||||
this.config.delay.enabled
|
||||
? clamp(this.config.delay.feedback + activity * 0.08, 0.04, 0.32)
|
||||
: 0,
|
||||
now,
|
||||
this.config.updateRampSeconds
|
||||
);
|
||||
this.delayOutput.gain.setTargetAtTime(
|
||||
this.config.delay.enabled ? this.config.delay.wetGain * (0.65 + activity * 0.5) : 0,
|
||||
now,
|
||||
this.config.updateRampSeconds
|
||||
);
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
const context = this.context;
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.masterGain && context.state !== 'closed') {
|
||||
this.masterGain.gain.setTargetAtTime(0.0001, context.currentTime, 0.015);
|
||||
}
|
||||
|
||||
this.clearNodes();
|
||||
|
||||
if (context.state !== 'closed') {
|
||||
await context.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private createDelay(context: AudioContext, masterGain: GainNode): void {
|
||||
const delayInput = context.createGain();
|
||||
const delayNode = context.createDelay(2);
|
||||
const delayFeedback = context.createGain();
|
||||
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;
|
||||
|
||||
delayInput.connect(delayNode);
|
||||
delayNode.connect(delayFeedback);
|
||||
delayFeedback.connect(delayNode);
|
||||
delayNode.connect(delayOutput);
|
||||
delayOutput.connect(masterGain);
|
||||
|
||||
this.delayInput = delayInput;
|
||||
this.delayNode = delayNode;
|
||||
this.delayFeedback = delayFeedback;
|
||||
this.delayOutput = delayOutput;
|
||||
}
|
||||
|
||||
private createBuses(context: AudioContext, masterGain: GainNode): void {
|
||||
this.eventBus = context.createGain();
|
||||
this.eventBus.gain.value = 1;
|
||||
this.eventBus.connect(masterGain);
|
||||
}
|
||||
|
||||
private createNoiseBuffer(context: AudioContext): AudioBuffer {
|
||||
const buffer = context.createBuffer(1, context.sampleRate, context.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
data[index] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private clearNodes(): void {
|
||||
this.context = null;
|
||||
this.eventBus = null;
|
||||
this.delayInput = null;
|
||||
this.noiseBuffer = null;
|
||||
this.masterGain = null;
|
||||
this.delayNode = null;
|
||||
this.delayFeedback = null;
|
||||
this.delayOutput = null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue