more engines
This commit is contained in:
246
src/lib/audio/engines/WavetableEngine.ts
Normal file
246
src/lib/audio/engines/WavetableEngine.ts
Normal file
@ -0,0 +1,246 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
|
||||
interface WavetableParams {
|
||||
bankIndex: number;
|
||||
position: number;
|
||||
baseFreq: number;
|
||||
filterCutoff: number;
|
||||
filterResonance: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
interface Wavetable {
|
||||
name: string;
|
||||
samples: Float32Array;
|
||||
}
|
||||
|
||||
export class WavetableEngine implements SynthEngine<WavetableParams> {
|
||||
private wavetables: Wavetable[] = [];
|
||||
private isLoaded = false;
|
||||
|
||||
constructor() {
|
||||
this.generateBasicWavetables();
|
||||
this.loadWavetablesAsync();
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'Wavetable';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Classic wavetable synthesis';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
private generateBasicWavetables(): void {
|
||||
const size = 2048;
|
||||
const tables = [
|
||||
{ name: 'Sine', gen: (i: number) => Math.sin((i / size) * Math.PI * 2) },
|
||||
{ name: 'Triangle', gen: (i: number) => {
|
||||
const t = i / size;
|
||||
return t < 0.25 ? t * 4 : t < 0.75 ? 2 - t * 4 : t * 4 - 4;
|
||||
}},
|
||||
{ name: 'Saw', gen: (i: number) => (i / size) * 2 - 1 },
|
||||
{ name: 'Square', gen: (i: number) => i < size / 2 ? 1 : -1 },
|
||||
{ name: 'Pulse25', gen: (i: number) => i < size / 4 ? 1 : -1 },
|
||||
];
|
||||
|
||||
this.wavetables = tables.map(({ name, gen }) => ({
|
||||
name,
|
||||
samples: Float32Array.from({ length: size }, (_, i) => gen(i)),
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadWavetablesAsync(): Promise<void> {
|
||||
if (this.isLoaded) return;
|
||||
|
||||
try {
|
||||
const manifestResponse = await fetch('/wavetables/manifest.txt');
|
||||
const manifestText = await manifestResponse.text();
|
||||
const filenames = manifestText.trim().split('\n').filter(line => line.endsWith('.wav'));
|
||||
|
||||
const loadPromises = filenames.slice(0, 50).map(async (filename) => {
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const response = await fetch(`/wavetables/${filename.trim()}`);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
const samples = audioBuffer.getChannelData(0);
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
sum += samples[i];
|
||||
}
|
||||
const dc = sum / samples.length;
|
||||
|
||||
const cleaned = new Float32Array(samples.length);
|
||||
let peak = 0;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
cleaned[i] = samples[i] - dc;
|
||||
peak = Math.max(peak, Math.abs(cleaned[i]));
|
||||
}
|
||||
|
||||
if (peak > 0) {
|
||||
for (let i = 0; i < cleaned.length; i++) {
|
||||
cleaned[i] /= peak;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: filename.replace('.wav', ''),
|
||||
samples: cleaned,
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(loadPromises);
|
||||
const loaded = results.filter((wt): wt is Wavetable => wt !== null);
|
||||
|
||||
if (loaded.length > 0) {
|
||||
this.wavetables = loaded;
|
||||
console.log(`Loaded ${this.wavetables.length} wavetables`);
|
||||
}
|
||||
|
||||
this.isLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load wavetables:', error);
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
generate(params: WavetableParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const left = new Float32Array(numSamples);
|
||||
const right = new Float32Array(numSamples);
|
||||
|
||||
if (this.wavetables.length < 3) {
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
const bankSize = 4;
|
||||
const numBanks = Math.max(1, this.wavetables.length - bankSize + 1);
|
||||
const bankStart = Math.floor(params.bankIndex * numBanks) % numBanks;
|
||||
|
||||
const bank: Wavetable[] = [];
|
||||
for (let i = 0; i < bankSize; i++) {
|
||||
bank.push(this.wavetables[(bankStart + i) % this.wavetables.length]);
|
||||
}
|
||||
|
||||
const tableSize = bank[0].samples.length;
|
||||
const phaseIncrement = params.baseFreq / sampleRate;
|
||||
|
||||
let phase = 0;
|
||||
|
||||
const attackSamples = params.attack * duration * sampleRate;
|
||||
const decaySamples = params.decay * duration * sampleRate;
|
||||
const releaseSamples = params.release * duration * sampleRate;
|
||||
|
||||
let filterLP = 0;
|
||||
let filterBP = 0;
|
||||
|
||||
const tablePos = params.position * (bankSize - 1);
|
||||
const table1Index = Math.floor(tablePos);
|
||||
const table2Index = Math.min(table1Index + 1, bankSize - 1);
|
||||
const tableFade = tablePos - table1Index;
|
||||
|
||||
const cutoffNorm = Math.min(params.filterCutoff * 0.45, 0.45);
|
||||
const f = 2 * Math.sin(Math.PI * cutoffNorm);
|
||||
const q = 1 / Math.max(params.filterResonance * 5 + 0.7, 0.7);
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
let env = 1;
|
||||
if (i < attackSamples) {
|
||||
env = i / attackSamples;
|
||||
} else if (i < attackSamples + decaySamples) {
|
||||
const t = (i - attackSamples) / decaySamples;
|
||||
env = 1 - t * (1 - params.sustain);
|
||||
} else if (i < numSamples - releaseSamples) {
|
||||
env = params.sustain;
|
||||
} else {
|
||||
const t = (i - (numSamples - releaseSamples)) / releaseSamples;
|
||||
env = params.sustain * (1 - t);
|
||||
}
|
||||
|
||||
const index1 = phase * tableSize;
|
||||
const i1 = Math.floor(index1);
|
||||
const i2 = (i1 + 1) % tableSize;
|
||||
const frac = index1 - i1;
|
||||
|
||||
const sample1 = bank[table1Index].samples[i1] * (1 - frac) + bank[table1Index].samples[i2] * frac;
|
||||
const sample2 = bank[table2Index].samples[i1] * (1 - frac) + bank[table2Index].samples[i2] * frac;
|
||||
|
||||
let sample = sample1 * (1 - tableFade) + sample2 * tableFade;
|
||||
|
||||
const hp = sample - filterLP - q * filterBP;
|
||||
filterBP = filterBP + f * hp;
|
||||
filterLP = filterLP + f * filterBP;
|
||||
|
||||
filterBP = Math.max(-2, Math.min(2, filterBP));
|
||||
filterLP = Math.max(-2, Math.min(2, filterLP));
|
||||
|
||||
sample = filterLP * env;
|
||||
|
||||
left[i] = sample;
|
||||
right[i] = sample;
|
||||
|
||||
phase += phaseIncrement;
|
||||
if (phase >= 1) phase -= 1;
|
||||
}
|
||||
|
||||
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 gain = 0.9 / peak;
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
left[i] *= gain;
|
||||
right[i] *= gain;
|
||||
}
|
||||
}
|
||||
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
randomParams(): WavetableParams {
|
||||
const freqs = [110, 146.8, 220, 293.7, 440];
|
||||
return {
|
||||
bankIndex: Math.random(),
|
||||
position: 0.2 + Math.random() * 0.6,
|
||||
baseFreq: freqs[Math.floor(Math.random() * freqs.length)],
|
||||
filterCutoff: 0.5 + Math.random() * 0.4,
|
||||
filterResonance: Math.random() * 0.5,
|
||||
attack: 0.001 + Math.random() * 0.05,
|
||||
decay: 0.05 + Math.random() * 0.2,
|
||||
sustain: 0.5 + Math.random() * 0.4,
|
||||
release: 0.1 + Math.random() * 0.3,
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: WavetableParams): WavetableParams {
|
||||
const mutate = (v: number, amount: number = 0.1) => {
|
||||
return Math.max(0, Math.min(1, v + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
|
||||
return {
|
||||
bankIndex: Math.random() < 0.2 ? Math.random() : params.bankIndex,
|
||||
position: mutate(params.position, 0.2),
|
||||
baseFreq: params.baseFreq,
|
||||
filterCutoff: mutate(params.filterCutoff, 0.2),
|
||||
filterResonance: mutate(params.filterResonance, 0.15),
|
||||
attack: mutate(params.attack, 0.1),
|
||||
decay: mutate(params.decay, 0.15),
|
||||
sustain: mutate(params.sustain, 0.15),
|
||||
release: mutate(params.release, 0.15),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user