Phase 6.5 Advanced Effects: - Add Pitch Shifter with semitones and cents adjustment - Add Time Stretch with pitch preservation using overlap-add - Add Distortion with soft/hard/tube types and tone control - Add Bitcrusher with bit depth and sample rate reduction - Add AdvancedParameterDialog with real-time waveform visualization - Add 4 professional presets per effect type Improvements: - Fix undefined parameter errors by adding nullish coalescing operators - Add global custom scrollbar styling with color-mix transparency - Add custom-scrollbar utility class for side panel - Improve theme-aware scrollbar appearance in light/dark modes - Fix parameter initialization when switching effect types Integration: - All advanced effects support undo/redo via EffectCommand - Effects accessible via command palette and side panel - Selection-based processing support - Toast notifications for all effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
/**
|
|
* 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<AudioBuffer> {
|
|
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<AudioBuffer> {
|
|
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<AudioBuffer> {
|
|
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<AudioBuffer> {
|
|
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<AudioBuffer> {
|
|
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;
|
|
}
|