Adding two engines and one processor
This commit is contained in:
528
src/lib/audio/engines/DustNoise.ts
Normal file
528
src/lib/audio/engines/DustNoise.ts
Normal file
@ -0,0 +1,528 @@
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
interface DustNoiseParams {
|
||||
// Dust density and character
|
||||
dustDensity: number;
|
||||
crackleAmount: number;
|
||||
popDensity: number;
|
||||
|
||||
// Dust particle characteristics
|
||||
particleDecay: number;
|
||||
particlePitchRange: number;
|
||||
particleResonance: number;
|
||||
|
||||
// Background texture
|
||||
backgroundNoise: number;
|
||||
noiseColor: number;
|
||||
noiseFilter: number;
|
||||
|
||||
// Pops and clicks
|
||||
popIntensity: number;
|
||||
popPitchRange: number;
|
||||
clickAmount: number;
|
||||
|
||||
// Dynamics and variation
|
||||
dynamicRange: number;
|
||||
irregularity: number;
|
||||
|
||||
// Stereo field
|
||||
stereoWidth: number;
|
||||
|
||||
// Global envelope
|
||||
globalAttack: number;
|
||||
globalDecay: number;
|
||||
}
|
||||
|
||||
export class DustNoise implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Pond';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Vinyl dust, crackle, and particle noise generator';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): DustNoiseParams {
|
||||
const characterBias = Math.random();
|
||||
|
||||
let dustDensity: number;
|
||||
let crackleAmount: number;
|
||||
let popDensity: number;
|
||||
let backgroundNoise: number;
|
||||
|
||||
if (characterBias < 0.5) {
|
||||
// Very sparse, minimal particles
|
||||
dustDensity = 0.01 + Math.random() * 0.08;
|
||||
crackleAmount = Math.random() * 0.12;
|
||||
popDensity = 0.01 + Math.random() * 0.05;
|
||||
backgroundNoise = Math.random() * 0.08;
|
||||
} else if (characterBias < 0.8) {
|
||||
// Sparse, clean with occasional pops
|
||||
dustDensity = 0.1 + Math.random() * 0.15;
|
||||
crackleAmount = Math.random() * 0.25;
|
||||
popDensity = 0.06 + Math.random() * 0.1;
|
||||
backgroundNoise = 0.05 + Math.random() * 0.15;
|
||||
} else {
|
||||
// Medium vinyl character (was heavy)
|
||||
dustDensity = 0.3 + Math.random() * 0.25;
|
||||
crackleAmount = 0.25 + Math.random() * 0.3;
|
||||
popDensity = 0.18 + Math.random() * 0.15;
|
||||
backgroundNoise = 0.15 + Math.random() * 0.25;
|
||||
}
|
||||
|
||||
const particleDecay = 0.3 + Math.random() * 0.6;
|
||||
const particlePitchRange = 0.2 + Math.random() * 0.7;
|
||||
const particleResonance = Math.random() * 0.6;
|
||||
|
||||
const noiseColor = Math.random();
|
||||
const noiseFilter = Math.random() * 0.8;
|
||||
|
||||
const popIntensity = 0.3 + Math.random() * 0.6;
|
||||
const popPitchRange = 0.2 + Math.random() * 0.7;
|
||||
const clickAmount = Math.random() * 0.7;
|
||||
|
||||
const dynamicRange = 0.3 + Math.random() * 0.6;
|
||||
const irregularity = Math.random() * 0.7;
|
||||
|
||||
const stereoWidth = Math.random() * 0.8;
|
||||
|
||||
const globalAttack = Math.random() * 0.08;
|
||||
const globalDecay = 0.3 + Math.random() * 0.5;
|
||||
|
||||
return {
|
||||
dustDensity,
|
||||
crackleAmount,
|
||||
popDensity,
|
||||
particleDecay,
|
||||
particlePitchRange,
|
||||
particleResonance,
|
||||
backgroundNoise,
|
||||
noiseColor,
|
||||
noiseFilter,
|
||||
popIntensity,
|
||||
popPitchRange,
|
||||
clickAmount,
|
||||
dynamicRange,
|
||||
irregularity,
|
||||
stereoWidth,
|
||||
globalAttack,
|
||||
globalDecay
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: DustNoiseParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): DustNoiseParams {
|
||||
const mutate = (value: number, amount: number = 0.15): number => {
|
||||
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
|
||||
return {
|
||||
dustDensity: mutate(params.dustDensity, 0.2),
|
||||
crackleAmount: mutate(params.crackleAmount, 0.25),
|
||||
popDensity: mutate(params.popDensity, 0.2),
|
||||
particleDecay: mutate(params.particleDecay, 0.2),
|
||||
particlePitchRange: pitchLock?.enabled ? params.particlePitchRange : mutate(params.particlePitchRange, 0.25),
|
||||
particleResonance: mutate(params.particleResonance, 0.2),
|
||||
backgroundNoise: mutate(params.backgroundNoise, 0.2),
|
||||
noiseColor: mutate(params.noiseColor, 0.25),
|
||||
noiseFilter: mutate(params.noiseFilter, 0.2),
|
||||
popIntensity: mutate(params.popIntensity, 0.2),
|
||||
popPitchRange: pitchLock?.enabled ? params.popPitchRange : mutate(params.popPitchRange, 0.25),
|
||||
clickAmount: mutate(params.clickAmount, 0.2),
|
||||
dynamicRange: mutate(params.dynamicRange, 0.2),
|
||||
irregularity: mutate(params.irregularity, 0.2),
|
||||
stereoWidth: mutate(params.stereoWidth, 0.2),
|
||||
globalAttack: mutate(params.globalAttack, 0.15),
|
||||
globalDecay: mutate(params.globalDecay, 0.2)
|
||||
};
|
||||
}
|
||||
|
||||
generate(params: DustNoiseParams, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const left = new Float32Array(numSamples);
|
||||
const right = new Float32Array(numSamples);
|
||||
|
||||
// Generate dust particles
|
||||
const avgDustPerSecond = 5 + params.dustDensity * 120;
|
||||
const totalDust = Math.floor(avgDustPerSecond * duration);
|
||||
|
||||
// Generate pops
|
||||
const avgPopsPerSecond = 0.5 + params.popDensity * 12;
|
||||
const totalPops = Math.floor(avgPopsPerSecond * duration);
|
||||
|
||||
// Create dust particles
|
||||
const dustParticles: Array<{
|
||||
startTime: number;
|
||||
decay: number;
|
||||
pitch: number;
|
||||
amplitude: number;
|
||||
resonance: number;
|
||||
stereoOffset: number;
|
||||
}> = [];
|
||||
|
||||
const baseDustPitch = pitchLock?.enabled ? pitchLock.frequency : 800 + params.particlePitchRange * 2000;
|
||||
const basePopPitch = pitchLock?.enabled ? pitchLock.frequency : 200 + params.popPitchRange * 1000;
|
||||
|
||||
for (let i = 0; i < totalDust; i++) {
|
||||
const startTime = Math.random() * duration;
|
||||
const decay = (0.001 + params.particleDecay * 0.02) * (0.5 + Math.random() * 0.5);
|
||||
const pitchVariation = pitchLock?.enabled ? 0.2 : params.particlePitchRange;
|
||||
const pitchFreq = baseDustPitch + (Math.random() - 0.5) * pitchVariation * baseDustPitch;
|
||||
const amplitude = (0.3 + Math.random() * 0.7) * (0.5 + params.dynamicRange * 0.5);
|
||||
const resonance = params.particleResonance * (0.5 + Math.random() * 0.5);
|
||||
const stereoOffset = (Math.random() - 0.5) * params.stereoWidth * 0.3;
|
||||
|
||||
dustParticles.push({
|
||||
startTime,
|
||||
decay,
|
||||
pitch: pitchFreq,
|
||||
amplitude,
|
||||
resonance,
|
||||
stereoOffset
|
||||
});
|
||||
}
|
||||
|
||||
// Create pops
|
||||
const pops: Array<{
|
||||
startTime: number;
|
||||
intensity: number;
|
||||
pitch: number;
|
||||
isClick: boolean;
|
||||
stereoOffset: number;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < totalPops; i++) {
|
||||
const startTime = Math.random() * duration;
|
||||
const intensity = params.popIntensity * (0.5 + Math.random() * 0.5);
|
||||
const pitchVariation = pitchLock?.enabled ? 0.2 : params.popPitchRange;
|
||||
const pitchFreq = basePopPitch + (Math.random() - 0.5) * pitchVariation * basePopPitch;
|
||||
const isClick = Math.random() < params.clickAmount;
|
||||
const stereoOffset = (Math.random() - 0.5) * params.stereoWidth * 0.5;
|
||||
|
||||
pops.push({
|
||||
startTime,
|
||||
intensity,
|
||||
pitch: pitchFreq,
|
||||
isClick,
|
||||
stereoOffset
|
||||
});
|
||||
}
|
||||
|
||||
// Sort events by time
|
||||
dustParticles.sort((a, b) => a.startTime - b.startTime);
|
||||
pops.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
// Noise state
|
||||
const pinkStateL = new Float32Array(7);
|
||||
const pinkStateR = new Float32Array(7);
|
||||
let brownStateL = 0;
|
||||
let brownStateR = 0;
|
||||
|
||||
// Filter state for background noise
|
||||
let bgFilterStateL1 = 0;
|
||||
let bgFilterStateL2 = 0;
|
||||
let bgFilterStateR1 = 0;
|
||||
let bgFilterStateR2 = 0;
|
||||
|
||||
// Active particles
|
||||
let dustIndex = 0;
|
||||
let popIndex = 0;
|
||||
const activeDust: Array<{
|
||||
particle: typeof dustParticles[0];
|
||||
startSample: number;
|
||||
phase: number;
|
||||
}> = [];
|
||||
const activePops: Array<{
|
||||
pop: typeof pops[0];
|
||||
startSample: number;
|
||||
phase: number;
|
||||
}> = [];
|
||||
|
||||
// Crackle state (for vinyl crackle texture)
|
||||
let cracklePhase = 0;
|
||||
const crackleFreq = 20 + params.crackleAmount * 80;
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const t = i / sampleRate;
|
||||
|
||||
// Add new dust particles
|
||||
while (dustIndex < dustParticles.length && dustParticles[dustIndex].startTime <= t) {
|
||||
activeDust.push({
|
||||
particle: dustParticles[dustIndex],
|
||||
startSample: i,
|
||||
phase: Math.random() * Math.PI * 2
|
||||
});
|
||||
dustIndex++;
|
||||
}
|
||||
|
||||
// Add new pops
|
||||
while (popIndex < pops.length && pops[popIndex].startTime <= t) {
|
||||
activePops.push({
|
||||
pop: pops[popIndex],
|
||||
startSample: i,
|
||||
phase: Math.random() * Math.PI * 2
|
||||
});
|
||||
popIndex++;
|
||||
}
|
||||
|
||||
// Global envelope
|
||||
const globalEnv = this.globalEnvelope(
|
||||
i,
|
||||
numSamples,
|
||||
params.globalAttack,
|
||||
params.globalDecay,
|
||||
duration,
|
||||
sampleRate
|
||||
);
|
||||
|
||||
// Background noise
|
||||
const whiteL = Math.random() * 2 - 1;
|
||||
const whiteR = Math.random() * 2 - 1;
|
||||
|
||||
brownStateL = this.updateBrownState(brownStateL, whiteL);
|
||||
brownStateR = this.updateBrownState(brownStateR, whiteR);
|
||||
|
||||
let bgNoiseL = this.selectNoiseColor(params.noiseColor, whiteL, pinkStateL, brownStateL);
|
||||
let bgNoiseR = this.selectNoiseColor(params.noiseColor, whiteR, pinkStateR, brownStateR);
|
||||
|
||||
// Filter background noise
|
||||
if (params.noiseFilter > 0.1) {
|
||||
const filterFreq = 500 + params.noiseFilter * 3000;
|
||||
const filtered = this.stateVariableFilter(
|
||||
bgNoiseL,
|
||||
filterFreq,
|
||||
1,
|
||||
sampleRate,
|
||||
bgFilterStateL1,
|
||||
bgFilterStateL2
|
||||
);
|
||||
bgFilterStateL1 = filtered.state1;
|
||||
bgFilterStateL2 = filtered.state2;
|
||||
bgNoiseL = filtered.output;
|
||||
|
||||
const filteredR = this.stateVariableFilter(
|
||||
bgNoiseR,
|
||||
filterFreq,
|
||||
1,
|
||||
sampleRate,
|
||||
bgFilterStateR1,
|
||||
bgFilterStateR2
|
||||
);
|
||||
bgFilterStateR1 = filteredR.state1;
|
||||
bgFilterStateR2 = filteredR.state2;
|
||||
bgNoiseR = filteredR.output;
|
||||
}
|
||||
|
||||
// Crackle modulation
|
||||
cracklePhase += (2 * Math.PI * crackleFreq) / sampleRate;
|
||||
const crackleMod = Math.sin(cracklePhase) * 0.5 + 0.5;
|
||||
const crackleEnv = Math.pow(crackleMod, 3) * params.crackleAmount;
|
||||
|
||||
bgNoiseL *= params.backgroundNoise * (1 + crackleEnv);
|
||||
bgNoiseR *= params.backgroundNoise * (1 + crackleEnv);
|
||||
|
||||
// Render dust particles
|
||||
let dustL = 0;
|
||||
let dustR = 0;
|
||||
|
||||
for (let d = activeDust.length - 1; d >= 0; d--) {
|
||||
const active = activeDust[d];
|
||||
const particle = active.particle;
|
||||
const elapsed = (i - active.startSample) / sampleRate;
|
||||
|
||||
if (elapsed > particle.decay * 5) {
|
||||
activeDust.splice(d, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const env = Math.exp(-elapsed / particle.decay);
|
||||
const phaseInc = (2 * Math.PI * particle.pitch) / sampleRate;
|
||||
active.phase += phaseInc;
|
||||
|
||||
let signal = Math.sin(active.phase);
|
||||
|
||||
// Add resonance (filter-like character)
|
||||
if (particle.resonance > 0.1) {
|
||||
signal = signal * (1 - particle.resonance) +
|
||||
Math.sin(active.phase * 2) * particle.resonance * 0.3 +
|
||||
Math.sin(active.phase * 3) * particle.resonance * 0.15;
|
||||
}
|
||||
|
||||
const output = signal * env * particle.amplitude;
|
||||
|
||||
const panL = 0.5 - particle.stereoOffset;
|
||||
const panR = 0.5 + particle.stereoOffset;
|
||||
|
||||
dustL += output * panL;
|
||||
dustR += output * panR;
|
||||
}
|
||||
|
||||
// Render pops and clicks
|
||||
let popL = 0;
|
||||
let popR = 0;
|
||||
|
||||
for (let p = activePops.length - 1; p >= 0; p--) {
|
||||
const active = activePops[p];
|
||||
const pop = active.pop;
|
||||
const elapsed = (i - active.startSample) / sampleRate;
|
||||
|
||||
const maxDuration = pop.isClick ? 0.001 : 0.008;
|
||||
|
||||
if (elapsed > maxDuration) {
|
||||
activePops.splice(p, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const env = Math.exp(-elapsed * (pop.isClick ? 2000 : 300));
|
||||
|
||||
let signal: number;
|
||||
if (pop.isClick) {
|
||||
// Sharp click (very short impulse)
|
||||
signal = (Math.random() * 2 - 1) * (elapsed < 0.0003 ? 1 : 0.3);
|
||||
} else {
|
||||
// Pop with pitch
|
||||
const phaseInc = (2 * Math.PI * pop.pitch) / sampleRate;
|
||||
active.phase += phaseInc;
|
||||
signal = Math.sin(active.phase) * 0.7 + (Math.random() * 2 - 1) * 0.3;
|
||||
}
|
||||
|
||||
const output = signal * env * pop.intensity;
|
||||
|
||||
const panL = 0.5 - pop.stereoOffset;
|
||||
const panR = 0.5 + pop.stereoOffset;
|
||||
|
||||
popL += output * panL;
|
||||
popR += output * panR;
|
||||
}
|
||||
|
||||
// Combine all elements
|
||||
let sampleL = bgNoiseL + dustL + popL;
|
||||
let sampleR = bgNoiseR + dustR + popR;
|
||||
|
||||
// Apply irregularity (random amplitude modulation)
|
||||
if (params.irregularity > 0.1) {
|
||||
const irregMod = 1 + (Math.random() - 0.5) * params.irregularity * 0.3;
|
||||
sampleL *= irregMod;
|
||||
sampleR *= irregMod;
|
||||
}
|
||||
|
||||
// Apply global envelope
|
||||
sampleL *= globalEnv;
|
||||
sampleR *= globalEnv;
|
||||
|
||||
// Soft clipping
|
||||
left[i] = this.softClip(sampleL * 0.6);
|
||||
right[i] = this.softClip(sampleR * 0.6);
|
||||
}
|
||||
|
||||
// Normalize
|
||||
let peak = 0;
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
|
||||
}
|
||||
|
||||
if (peak > 0.001) {
|
||||
const normGain = 0.95 / peak;
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
left[i] *= normGain;
|
||||
right[i] *= normGain;
|
||||
}
|
||||
}
|
||||
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
private globalEnvelope(
|
||||
sample: number,
|
||||
totalSamples: number,
|
||||
attack: number,
|
||||
decay: number,
|
||||
duration: number,
|
||||
sampleRate: number
|
||||
): number {
|
||||
const attackSamples = Math.floor(attack * duration * sampleRate);
|
||||
const phase = sample / totalSamples;
|
||||
|
||||
if (sample < attackSamples && attackSamples > 0) {
|
||||
const attackPhase = sample / attackSamples;
|
||||
return attackPhase * attackPhase * (3 - 2 * attackPhase);
|
||||
}
|
||||
|
||||
const decayRate = Math.max(decay, 0.1);
|
||||
const decayPhase = (sample - attackSamples) / (totalSamples - attackSamples);
|
||||
return Math.exp(-decayPhase / decayRate);
|
||||
}
|
||||
|
||||
private updateBrownState(brownState: number, whiteNoise: number): number {
|
||||
return (brownState + whiteNoise * 0.02) * 0.98;
|
||||
}
|
||||
|
||||
private selectNoiseColor(
|
||||
colorParam: number,
|
||||
whiteNoise: number,
|
||||
pinkState: Float32Array,
|
||||
brownState: number
|
||||
): number {
|
||||
if (colorParam < 0.33) {
|
||||
return whiteNoise;
|
||||
} else if (colorParam < 0.66) {
|
||||
pinkState[0] = 0.99886 * pinkState[0] + whiteNoise * 0.0555179;
|
||||
pinkState[1] = 0.99332 * pinkState[1] + whiteNoise * 0.0750759;
|
||||
pinkState[2] = 0.96900 * pinkState[2] + whiteNoise * 0.1538520;
|
||||
pinkState[3] = 0.86650 * pinkState[3] + whiteNoise * 0.3104856;
|
||||
pinkState[4] = 0.55000 * pinkState[4] + whiteNoise * 0.5329522;
|
||||
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
|
||||
|
||||
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
|
||||
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
|
||||
pinkState[6] = whiteNoise * 0.115926;
|
||||
|
||||
return pink * 0.11;
|
||||
} else {
|
||||
return brownState * 2.5;
|
||||
}
|
||||
}
|
||||
|
||||
private stateVariableFilter(
|
||||
input: number,
|
||||
cutoff: number,
|
||||
resonance: number,
|
||||
sampleRate: number,
|
||||
state1: number,
|
||||
state2: number
|
||||
): { output: number; state1: number; state2: number } {
|
||||
const normalizedFreq = Math.min(cutoff / sampleRate, 0.48);
|
||||
const f = 2 * Math.sin(Math.PI * normalizedFreq);
|
||||
const q = Math.max(1 / Math.min(resonance, 10), 0.02);
|
||||
|
||||
const lowpass = state2 + f * state1;
|
||||
const highpass = input - lowpass - q * state1;
|
||||
const bandpass = f * highpass + state1;
|
||||
|
||||
const newState1 = Math.max(-2, Math.min(2, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
|
||||
const newState2 = Math.max(-2, Math.min(2, Math.abs(lowpass) > 1e-10 ? lowpass : 0));
|
||||
|
||||
return {
|
||||
output: bandpass,
|
||||
state1: newState1,
|
||||
state2: newState2
|
||||
};
|
||||
}
|
||||
|
||||
private softClip(x: number): number {
|
||||
if (x > 1) {
|
||||
return 1;
|
||||
} else if (x < -1) {
|
||||
return -1;
|
||||
} else if (x > 0.66) {
|
||||
return (3 - (2 - 3 * x) ** 2) / 3;
|
||||
} else if (x < -0.66) {
|
||||
return -(3 - (2 - 3 * -x) ** 2) / 3;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user