Files
rsgp/src/lib/audio/engines/Snare.ts

239 lines
7.1 KiB
TypeScript

import type { PitchLock, SynthEngine } from './base/SynthEngine';
interface SnareParams {
// Core frequency (base pitch of the snare)
baseFreq: number;
// Tone control (0 = more tonal, 1 = more noise)
tone: number;
// Snap amount (pitch modulation intensity)
snap: number;
// Decay times
tonalDecay: number;
noiseDecay: number;
// Accent (volume boost)
accent: number;
// Tuning offset
tuning: number;
}
export class Snare implements SynthEngine {
getName(): string {
return 'Noise Snare';
}
getDescription(): string {
return 'Classic snare drum with triangle wave and noise';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): SnareParams {
return {
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
tone: 0.4 + Math.random() * 0.4,
snap: 0.5 + Math.random() * 0.5,
tonalDecay: 0.15 + Math.random() * 0.35,
noiseDecay: 0.4 + Math.random() * 0.4,
accent: 0.5 + Math.random() * 0.5,
tuning: pitchLock ? 0.5 : 0.4 + Math.random() * 0.2,
};
}
mutateParams(params: SnareParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): SnareParams {
const mutate = (value: number, amount: number = mutationAmount): number => {
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
};
return {
baseFreq: pitchLock ? params.baseFreq : mutate(params.baseFreq, 0.2),
tone: mutate(params.tone, 0.25),
snap: mutate(params.snap, 0.2),
tonalDecay: mutate(params.tonalDecay, 0.2),
noiseDecay: mutate(params.noiseDecay, 0.2),
accent: mutate(params.accent, 0.2),
tuning: pitchLock ? params.tuning : mutate(params.tuning, 0.15),
};
}
generate(
params: SnareParams,
sampleRate: number,
duration: number,
pitchLock?: PitchLock
): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
// Base frequency: 100Hz to 400Hz
const baseFreq = pitchLock ? pitchLock.frequency : 100 + params.baseFreq * 300;
// Tuning offset: -12% to +12%
const tuningFactor = 0.88 + params.tuning * 0.24;
const tunedFreq = baseFreq * tuningFactor;
// Pitch modulation parameters (starts 4x higher, drops over 10ms)
const pitchMultiplier = 1 + params.snap * 3; // 1x to 4x
const modDuration = 0.008 + params.snap * 0.007; // 8ms to 15ms
// Decay times scaled by duration
const tonalDecayTime = (0.05 + params.tonalDecay * 0.15) * duration; // 50ms to 200ms
const noiseDecayTime = (0.15 + params.noiseDecay * 0.35) * duration; // 150ms to 500ms
// Volume and gain staging
const baseVolume = 0.5;
const accentGain = baseVolume * (1 + params.accent);
// Attack time (5ms ramp)
const attackTime = 0.005;
// Notch filter parameters for noise (fixed at 1000Hz)
const notchFreq = 1000;
const notchQ = 5;
for (let channel = 0; channel < 2; channel++) {
const output = channel === 0 ? left : right;
// Triangle oscillator phase
let phase = Math.random() * Math.PI * 2;
// Notch filter state variables
let notchState1 = 0;
let notchState2 = 0;
// Stereo variation (slight detune and phase offset)
const stereoDetune = channel === 0 ? 0.998 : 1.002;
const stereoPhaseOffset = channel === 0 ? 0 : 0.1;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const normPhase = i / numSamples;
// Pitch envelope: exponential ramp from pitchMultiplier*freq down to base freq
const pitchEnv = Math.exp(-t / modDuration);
const currentFreq = tunedFreq * stereoDetune * (1 + (pitchMultiplier - 1) * pitchEnv);
// Generate triangle wave
phase += (2 * Math.PI * currentFreq) / sampleRate;
if (phase > 2 * Math.PI) phase -= 2 * Math.PI;
// Triangle wave from phase
const triangle = phase < Math.PI
? -1 + (2 * phase) / Math.PI
: 3 - (2 * phase) / Math.PI;
// Attack envelope (5ms linear ramp)
const attackEnv = t < attackTime ? t / attackTime : 1.0;
// Tonal envelope: exponential decay
const tonalEnv = Math.exp(-t / tonalDecayTime) * attackEnv;
const tonalSignal = triangle * tonalEnv;
// Generate white noise
const noise = Math.random() * 2 - 1;
// Apply notch filter to noise (removes 1000Hz component)
const notchFiltered = this.notchFilter(
noise,
notchFreq,
notchQ,
sampleRate,
notchState1,
notchState2
);
notchState1 = notchFiltered.state1;
notchState2 = notchFiltered.state2;
// Noise envelope: exponential decay (longer than tonal)
const noiseEnv = Math.exp(-t / noiseDecayTime);
const noiseSignal = notchFiltered.output * noiseEnv;
// Mix tonal and noise
const tonalGain = baseVolume * (1 - params.tone * 0.5); // 0.5 to 0.25
const noiseGain = 0.15 + params.tone * 0.2; // 0.15 to 0.35
let sample = tonalSignal * tonalGain + noiseSignal * noiseGain;
// Apply accent
sample *= (0.5 + params.accent * 0.5);
// Soft clipping
sample = this.softClip(sample);
output[i] = sample;
}
}
// Normalize output to consistent level
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.5 / peak; // Normalize to 0.5 peak for more headroom
for (let i = 0; i < numSamples; i++) {
left[i] *= normGain;
right[i] *= normGain;
}
}
return [left, right];
}
private notchFilter(
input: number,
frequency: number,
q: number,
sampleRate: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
// State variable filter configured as notch
const normalizedFreq = Math.min(frequency / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const qRecip = 1 / Math.max(q, 0.5);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - qRecip * state1;
const bandpass = f * highpass + state1;
// Notch = input - bandpass
const notch = input - bandpass;
// Update states with clamping
const newState1 = Math.max(-2, Math.min(2, bandpass));
const newState2 = Math.max(-2, Math.min(2, lowpass));
return {
output: notch,
state1: newState1,
state2: newState2,
};
}
private softClip(x: number): number {
if (x > 1) return 1;
if (x < -1) return -1;
if (x > 0.66) return (3 - (2 - 3 * x) ** 2) / 3;
if (x < -0.66) return -(3 - (2 - 3 * -x) ** 2) / 3;
return x;
}
private freqToParam(freq: number): number {
// Map frequency to 0-1 range (100-400 Hz)
return Math.max(0, Math.min(1, (freq - 100) / 300));
}
}