/** * Time-based effects (Delay, Reverb, Chorus, Flanger, Phaser) */ import { getAudioContext } from '../context'; export interface DelayParameters { time: number; // ms - delay time feedback: number; // 0-1 - amount of delayed signal fed back mix: number; // 0-1 - dry/wet mix (0 = dry, 1 = wet) } export interface ReverbParameters { roomSize: number; // 0-1 - size of the reverb room damping: number; // 0-1 - high frequency damping mix: number; // 0-1 - dry/wet mix } export interface ChorusParameters { rate: number; // Hz - LFO rate depth: number; // 0-1 - modulation depth delay: number; // ms - base delay time mix: number; // 0-1 - dry/wet mix } export interface FlangerParameters { rate: number; // Hz - LFO rate depth: number; // 0-1 - modulation depth feedback: number; // 0-1 - feedback amount delay: number; // ms - base delay time mix: number; // 0-1 - dry/wet mix } export interface PhaserParameters { rate: number; // Hz - LFO rate depth: number; // 0-1 - modulation depth feedback: number; // 0-1 - feedback amount stages: number; // 2-12 - number of allpass filters mix: number; // 0-1 - dry/wet mix } /** * Apply delay/echo effect to audio buffer */ export async function applyDelay( buffer: AudioBuffer, params: DelayParameters ): Promise { const audioContext = getAudioContext(); const channels = buffer.numberOfChannels; const length = buffer.length; const sampleRate = buffer.sampleRate; // Calculate delay in samples const delaySamples = Math.floor((params.time / 1000) * sampleRate); // Create output buffer (needs extra length for delay tail) const outputLength = length + delaySamples * 5; // Allow for multiple echoes const outputBuffer = audioContext.createBuffer(channels, outputLength, sampleRate); // Process each channel for (let channel = 0; channel < channels; channel++) { const inputData = buffer.getChannelData(channel); const outputData = outputBuffer.getChannelData(channel); // Copy input and add delayed copies with feedback for (let i = 0; i < outputLength; i++) { let sample = 0; // Add original signal if (i < length) { sample += inputData[i] * (1 - params.mix); } // Add delayed signal with feedback let delayIndex = i; let feedbackGain = params.mix; for (let echo = 0; echo < 10; echo++) { delayIndex -= delaySamples; if (delayIndex >= 0 && delayIndex < length) { sample += inputData[delayIndex] * feedbackGain; } feedbackGain *= params.feedback; if (feedbackGain < 0.001) break; // Stop when feedback is negligible } outputData[i] = sample; } } return outputBuffer; } /** * Apply simple algorithmic reverb to audio buffer */ export async function applyReverb( buffer: AudioBuffer, params: ReverbParameters ): Promise { const audioContext = getAudioContext(); const channels = buffer.numberOfChannels; const length = buffer.length; const sampleRate = buffer.sampleRate; // Reverb uses multiple delay lines (Schroeder reverb algorithm) const combDelays = [1557, 1617, 1491, 1422, 1277, 1356, 1188, 1116].map( d => Math.floor(d * params.roomSize * (sampleRate / 44100)) ); const allpassDelays = [225, 556, 441, 341].map( d => Math.floor(d * (sampleRate / 44100)) ); // Create output buffer with reverb tail const outputLength = length + Math.floor(sampleRate * 3 * params.roomSize); const outputBuffer = audioContext.createBuffer(channels, outputLength, sampleRate); // Process each channel for (let channel = 0; channel < channels; channel++) { const inputData = buffer.getChannelData(channel); const outputData = outputBuffer.getChannelData(channel); // Comb filter buffers const combBuffers = combDelays.map(delay => new Float32Array(delay)); const combIndices = combDelays.map(() => 0); // Allpass filter buffers const allpassBuffers = allpassDelays.map(delay => new Float32Array(delay)); const allpassIndices = allpassDelays.map(() => 0); // Process samples for (let i = 0; i < outputLength; i++) { let input = i < length ? inputData[i] : 0; let combSum = 0; // Parallel comb filters for (let c = 0; c < combDelays.length; c++) { const delayedSample = combBuffers[c][combIndices[c]]; combSum += delayedSample; // Feedback with damping const feedback = delayedSample * (0.84 - params.damping * 0.2); combBuffers[c][combIndices[c]] = input + feedback; combIndices[c] = (combIndices[c] + 1) % combDelays[c]; } // Average comb outputs let sample = combSum / combDelays.length; // Series allpass filters for (let a = 0; a < allpassDelays.length; a++) { const delayed = allpassBuffers[a][allpassIndices[a]]; const output = -sample + delayed; allpassBuffers[a][allpassIndices[a]] = sample + delayed * 0.5; sample = output; allpassIndices[a] = (allpassIndices[a] + 1) % allpassDelays[a]; } // Mix dry and wet outputData[i] = input * (1 - params.mix) + sample * params.mix * 0.5; } } return outputBuffer; } /** * Apply chorus effect to audio buffer */ export async function applyChorus( buffer: AudioBuffer, params: ChorusParameters ): Promise { const audioContext = getAudioContext(); const channels = buffer.numberOfChannels; const length = buffer.length; const sampleRate = buffer.sampleRate; // Create output buffer const outputBuffer = audioContext.createBuffer(channels, length, sampleRate); // Base delay in samples const baseDelaySamples = (params.delay / 1000) * sampleRate; const maxDelaySamples = baseDelaySamples + (params.depth * sampleRate * 0.005); // Process each channel for (let channel = 0; channel < channels; channel++) { const inputData = buffer.getChannelData(channel); const outputData = outputBuffer.getChannelData(channel); // Create delay buffer const delayBuffer = new Float32Array(Math.ceil(maxDelaySamples) + 1); let delayIndex = 0; for (let i = 0; i < length; i++) { const input = inputData[i]; // Calculate LFO (Low Frequency Oscillator) const lfoPhase = (i / sampleRate) * params.rate * 2 * Math.PI; const lfo = Math.sin(lfoPhase); // Modulated delay time const modulatedDelay = baseDelaySamples + (lfo * params.depth * sampleRate * 0.005); // Read from delay buffer with interpolation const readIndex = (delayIndex - modulatedDelay + delayBuffer.length) % delayBuffer.length; const readIndexInt = Math.floor(readIndex); const readIndexFrac = readIndex - readIndexInt; const sample1 = delayBuffer[readIndexInt]; const sample2 = delayBuffer[(readIndexInt + 1) % delayBuffer.length]; const delayedSample = sample1 + (sample2 - sample1) * readIndexFrac; // Write to delay buffer delayBuffer[delayIndex] = input; delayIndex = (delayIndex + 1) % delayBuffer.length; // Mix dry and wet outputData[i] = input * (1 - params.mix) + delayedSample * params.mix; } } return outputBuffer; } /** * Apply flanger effect to audio buffer */ export async function applyFlanger( buffer: AudioBuffer, params: FlangerParameters ): Promise { const audioContext = getAudioContext(); const channels = buffer.numberOfChannels; const length = buffer.length; const sampleRate = buffer.sampleRate; // Create output buffer const outputBuffer = audioContext.createBuffer(channels, length, sampleRate); // Base delay in samples (shorter than chorus) const baseDelaySamples = (params.delay / 1000) * sampleRate; const maxDelaySamples = baseDelaySamples + (params.depth * sampleRate * 0.002); // Process each channel for (let channel = 0; channel < channels; channel++) { const inputData = buffer.getChannelData(channel); const outputData = outputBuffer.getChannelData(channel); // Create delay buffer const delayBuffer = new Float32Array(Math.ceil(maxDelaySamples) + 1); let delayIndex = 0; for (let i = 0; i < length; i++) { const input = inputData[i]; // Calculate LFO const lfoPhase = (i / sampleRate) * params.rate * 2 * Math.PI; const lfo = Math.sin(lfoPhase); // Modulated delay time const modulatedDelay = baseDelaySamples + (lfo * params.depth * sampleRate * 0.002); // Read from delay buffer with interpolation const readIndex = (delayIndex - modulatedDelay + delayBuffer.length) % delayBuffer.length; const readIndexInt = Math.floor(readIndex); const readIndexFrac = readIndex - readIndexInt; const sample1 = delayBuffer[readIndexInt]; const sample2 = delayBuffer[(readIndexInt + 1) % delayBuffer.length]; const delayedSample = sample1 + (sample2 - sample1) * readIndexFrac; // Write to delay buffer with feedback delayBuffer[delayIndex] = input + delayedSample * params.feedback; delayIndex = (delayIndex + 1) % delayBuffer.length; // Mix dry and wet outputData[i] = input * (1 - params.mix) + delayedSample * params.mix; } } return outputBuffer; } /** * Apply phaser effect to audio buffer */ export async function applyPhaser( buffer: AudioBuffer, params: PhaserParameters ): Promise { const audioContext = getAudioContext(); const channels = buffer.numberOfChannels; const length = buffer.length; const sampleRate = buffer.sampleRate; // Create output buffer const outputBuffer = audioContext.createBuffer(channels, length, sampleRate); // Process each channel for (let channel = 0; channel < channels; channel++) { const inputData = buffer.getChannelData(channel); const outputData = outputBuffer.getChannelData(channel); // Allpass filter state for each stage const stages = Math.floor(params.stages); const allpassStates = new Array(stages).fill(0); for (let i = 0; i < length; i++) { let input = inputData[i]; let output = input; // Calculate LFO const lfoPhase = (i / sampleRate) * params.rate * 2 * Math.PI; const lfo = Math.sin(lfoPhase); // Modulated allpass frequency (200Hz to 2000Hz) const baseFreq = 200 + (lfo + 1) * 0.5 * 1800 * params.depth; const omega = (2 * Math.PI * baseFreq) / sampleRate; const alpha = (1 - Math.tan(omega / 2)) / (1 + Math.tan(omega / 2)); // Apply cascaded allpass filters for (let stage = 0; stage < stages; stage++) { const filtered = alpha * output + allpassStates[stage]; allpassStates[stage] = output - alpha * filtered; output = filtered; } // Add feedback output = output + output * params.feedback; // Mix dry and wet outputData[i] = input * (1 - params.mix) + output * params.mix; } } return outputBuffer; }