Adding more effects
This commit is contained in:
251
src/lib/audio/processors/ConvolutionReverb.ts
Normal file
251
src/lib/audio/processors/ConvolutionReverb.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
|
||||
type RoomType = 'small' | 'medium' | 'large' | 'hall' | 'plate' | 'chamber';
|
||||
|
||||
interface ReverbParams {
|
||||
roomType: RoomType;
|
||||
decayTime: number;
|
||||
preDelay: number;
|
||||
wetMix: number;
|
||||
earlyReflections: number;
|
||||
diffusion: number;
|
||||
}
|
||||
|
||||
export class ConvolutionReverb implements AudioProcessor {
|
||||
private readonly sampleRate = 44100;
|
||||
|
||||
getName(): string {
|
||||
return 'Convolution Reverb';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Realistic room ambience using Web Audio ConvolverNode with synthetic impulse responses';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
): Promise<[Float32Array, Float32Array]> {
|
||||
const inputLength = leftChannel.length;
|
||||
const params = this.randomParams();
|
||||
|
||||
// Create offline context for processing
|
||||
const offlineContext = new OfflineAudioContext(2, inputLength, this.sampleRate);
|
||||
|
||||
// Create input buffer from our channels (copy to ensure proper ArrayBuffer type)
|
||||
const inputBuffer = offlineContext.createBuffer(2, inputLength, this.sampleRate);
|
||||
inputBuffer.copyToChannel(new Float32Array(leftChannel), 0);
|
||||
inputBuffer.copyToChannel(new Float32Array(rightChannel), 1);
|
||||
|
||||
// Generate impulse response with slight stereo differences
|
||||
const irBuffer = this.generateImpulseResponseBuffer(offlineContext, params);
|
||||
|
||||
// Create audio graph: source -> convolver -> destination
|
||||
const source = offlineContext.createBufferSource();
|
||||
source.buffer = inputBuffer;
|
||||
|
||||
const convolver = offlineContext.createConvolver();
|
||||
convolver.buffer = irBuffer;
|
||||
convolver.normalize = false; // We normalize the IR ourselves for consistent gain
|
||||
|
||||
const dryGain = offlineContext.createGain();
|
||||
const wetGain = offlineContext.createGain();
|
||||
|
||||
// Equal-power crossfade
|
||||
dryGain.gain.value = Math.sqrt(1 - params.wetMix);
|
||||
wetGain.gain.value = Math.sqrt(params.wetMix);
|
||||
|
||||
// Connect dry path
|
||||
source.connect(dryGain);
|
||||
dryGain.connect(offlineContext.destination);
|
||||
|
||||
// Connect wet path
|
||||
source.connect(convolver);
|
||||
convolver.connect(wetGain);
|
||||
wetGain.connect(offlineContext.destination);
|
||||
|
||||
source.start(0);
|
||||
|
||||
// Render offline
|
||||
const renderedBuffer = await offlineContext.startRendering();
|
||||
|
||||
// Extract channels and ensure exact length match
|
||||
const outLeft = new Float32Array(inputLength);
|
||||
const outRight = new Float32Array(inputLength);
|
||||
|
||||
renderedBuffer.copyFromChannel(outLeft, 0);
|
||||
renderedBuffer.copyFromChannel(outRight, 1);
|
||||
|
||||
// Normalize to prevent clipping while maintaining dynamics
|
||||
this.normalizeInPlace(outLeft, outRight);
|
||||
|
||||
return [outLeft, outRight];
|
||||
}
|
||||
|
||||
private randomParams(): ReverbParams {
|
||||
const roomTypes: RoomType[] = ['small', 'medium', 'large', 'hall', 'plate', 'chamber'];
|
||||
const roomType = roomTypes[Math.floor(Math.random() * roomTypes.length)];
|
||||
|
||||
let decayTime: number;
|
||||
let diffusion: number;
|
||||
|
||||
switch (roomType) {
|
||||
case 'small':
|
||||
decayTime = Math.random() * 0.3 + 0.2;
|
||||
diffusion = Math.random() * 0.3 + 0.5;
|
||||
break;
|
||||
case 'medium':
|
||||
decayTime = Math.random() * 0.5 + 0.4;
|
||||
diffusion = Math.random() * 0.2 + 0.6;
|
||||
break;
|
||||
case 'large':
|
||||
decayTime = Math.random() * 0.7 + 0.6;
|
||||
diffusion = Math.random() * 0.2 + 0.7;
|
||||
break;
|
||||
case 'hall':
|
||||
decayTime = Math.random() * 0.5 + 0.8;
|
||||
diffusion = Math.random() * 0.15 + 0.75;
|
||||
break;
|
||||
case 'plate':
|
||||
decayTime = Math.random() * 0.4 + 0.5;
|
||||
diffusion = Math.random() * 0.3 + 0.6;
|
||||
break;
|
||||
case 'chamber':
|
||||
decayTime = Math.random() * 0.6 + 0.5;
|
||||
diffusion = Math.random() * 0.2 + 0.65;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
roomType,
|
||||
decayTime,
|
||||
preDelay: Math.random() * 0.025 + 0.005,
|
||||
wetMix: Math.random() * 0.4 + 0.3,
|
||||
earlyReflections: Math.random() * 0.3 + 0.4,
|
||||
diffusion
|
||||
};
|
||||
}
|
||||
|
||||
private generateImpulseResponseBuffer(context: OfflineAudioContext, params: ReverbParams): AudioBuffer {
|
||||
// IR length based on decay time, with reasonable maximum
|
||||
const irDuration = Math.min(params.decayTime * 4, 4.0);
|
||||
const irLength = Math.floor(irDuration * this.sampleRate);
|
||||
|
||||
const irBuffer = context.createBuffer(2, irLength, this.sampleRate);
|
||||
const irLeft = new Float32Array(irLength);
|
||||
const irRight = new Float32Array(irLength);
|
||||
|
||||
// Generate stereo IRs with slight differences for width
|
||||
this.generateImpulseResponse(irLeft, params, 0);
|
||||
this.generateImpulseResponse(irRight, params, 1);
|
||||
|
||||
irBuffer.copyToChannel(irLeft, 0);
|
||||
irBuffer.copyToChannel(irRight, 1);
|
||||
|
||||
return irBuffer;
|
||||
}
|
||||
|
||||
private generateImpulseResponse(ir: Float32Array, params: ReverbParams, channel: number): void {
|
||||
const irLength = ir.length;
|
||||
const preDelaySamples = Math.floor(params.preDelay * this.sampleRate);
|
||||
|
||||
// Room-specific parameters
|
||||
const roomSizes: Record<RoomType, number> = {
|
||||
small: 0.2,
|
||||
medium: 0.4,
|
||||
large: 0.6,
|
||||
hall: 0.85,
|
||||
plate: 0.5,
|
||||
chamber: 0.55
|
||||
};
|
||||
|
||||
const roomSize = roomSizes[params.roomType];
|
||||
const earlyDecayTime = roomSize * 0.05;
|
||||
|
||||
// Generate early reflections as sparse impulses
|
||||
const numEarlyReflections = Math.floor(roomSize * 30) + 8;
|
||||
const earlyReflectionTime = earlyDecayTime * this.sampleRate;
|
||||
|
||||
for (let i = 0; i < numEarlyReflections; i++) {
|
||||
const time = preDelaySamples + Math.pow(Math.random(), 1.5) * earlyReflectionTime;
|
||||
const index = Math.floor(time);
|
||||
|
||||
if (index < irLength) {
|
||||
const distanceAttenuation = 1.0 / (1.0 + time / this.sampleRate);
|
||||
const amplitude = params.earlyReflections * distanceAttenuation * (0.5 + Math.random() * 0.5);
|
||||
|
||||
// Stereo width variation
|
||||
const stereoOffset = (channel === 0 ? -1 : 1) * Math.random() * 0.3;
|
||||
ir[index] += amplitude * (1.0 + stereoOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate diffuse tail using filtered noise
|
||||
const diffuseStart = preDelaySamples + Math.floor(earlyReflectionTime);
|
||||
const decaySamples = params.decayTime * this.sampleRate;
|
||||
|
||||
// Multi-band diffusion for realistic reverb character
|
||||
const numBands = 4;
|
||||
const bandGains = new Float32Array(numBands);
|
||||
|
||||
for (let band = 0; band < numBands; band++) {
|
||||
bandGains[band] = 0.7 + Math.random() * 0.3;
|
||||
}
|
||||
|
||||
// Generate noise with exponential decay and frequency-dependent damping
|
||||
for (let i = diffuseStart; i < irLength; i++) {
|
||||
const t = (i - diffuseStart) / decaySamples;
|
||||
const envelope = Math.exp(-t * 6.0);
|
||||
|
||||
if (envelope < 0.001) break;
|
||||
|
||||
let sample = 0;
|
||||
|
||||
// Multi-band noise for frequency-dependent decay
|
||||
for (let band = 0; band < numBands; band++) {
|
||||
const noise = (Math.random() * 2 - 1) * bandGains[band];
|
||||
const bandEnvelope = Math.exp(-t * (3.0 + band * 2.0));
|
||||
sample += noise * bandEnvelope;
|
||||
}
|
||||
|
||||
sample *= envelope * params.diffusion;
|
||||
|
||||
// Apply low-pass filtering for natural high-frequency damping
|
||||
if (i > diffuseStart) {
|
||||
const damping = params.roomType === 'plate' ? 0.4 : 0.5;
|
||||
sample = sample * (1 - damping) + ir[i - 1] * damping;
|
||||
}
|
||||
|
||||
ir[i] += sample;
|
||||
}
|
||||
|
||||
// Normalize IR to consistent level
|
||||
let maxAmp = 0;
|
||||
for (let i = 0; i < irLength; i++) {
|
||||
maxAmp = Math.max(maxAmp, Math.abs(ir[i]));
|
||||
}
|
||||
|
||||
if (maxAmp > 0) {
|
||||
const normGain = 0.5 / maxAmp;
|
||||
for (let i = 0; i < irLength; i++) {
|
||||
ir[i] *= normGain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeInPlace(left: Float32Array, right: Float32Array): void {
|
||||
let maxAmp = 0;
|
||||
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
maxAmp = Math.max(maxAmp, Math.abs(left[i]), Math.abs(right[i]));
|
||||
}
|
||||
|
||||
if (maxAmp > 0.98) {
|
||||
const gain = 0.95 / maxAmp;
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
left[i] *= gain;
|
||||
right[i] *= gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user