Files
2026-09-24 17:57:54 +02:00

139 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Synthetische Klänge über die Web Audio API – ohne eine einzige Audiodatei.
*
* Dient als Ersatz, wenn Klangdateien fehlen oder der Browser OGG nicht dekodiert,
* und liefert Effekte, für die es kein passendes Sample gibt (Rolle, Mörserpfeifen).
* @module systems/Synth
*/
/**
* Erzeugt eine Sekunde weißes Rauschen (wird für Rausch-Effekte wiederverwendet).
* @param {BaseAudioContext} ctx
* @returns {AudioBuffer}
*/
export function createNoiseBuffer(ctx) {
const buffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i += 1) data[i] = Math.random() * 2 - 1;
return buffer;
}
/**
* Hüllkurve: schneller Anstieg, exponentielles Abklingen. Exponentiell klingt
* natürlicher als linear, darf aber nie genau 0 erreichen (daher 0.0001).
* @param {GainNode} gain
* @param {number} start
* @param {number} peak
* @param {number} attack
* @param {number} duration
*/
function envelope(gain, start, peak, attack, duration) {
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(Math.max(0.0002, peak), start + attack);
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
}
/**
* Einzelner Ton mit optionalem Tonhöhenverlauf.
* @param {AudioContext} ctx
* @param {AudioNode} destination
* @param {{type?: OscillatorType, freq: number, freqEnd?: number, duration: number, volume: number, attack?: number, delay?: number}} p
*/
export function tone(ctx, destination, p) {
const start = ctx.currentTime + (p.delay ?? 0);
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = p.type ?? 'sine';
osc.frequency.setValueAtTime(p.freq, start);
if (p.freqEnd) osc.frequency.exponentialRampToValueAtTime(p.freqEnd, start + p.duration);
envelope(gain, start, p.volume, p.attack ?? 0.005, p.duration);
osc.connect(gain).connect(destination);
osc.start(start);
osc.stop(start + p.duration + 0.05);
}
/**
* Gefiltertes Rauschen (Knall, Zischen, Wusch).
* @param {AudioContext} ctx
* @param {AudioNode} destination
* @param {AudioBuffer} noiseBuffer
* @param {{filter?: BiquadFilterType, freq: number, freqEnd?: number, q?: number, duration: number, volume: number, attack?: number}} p
*/
export function noise(ctx, destination, noiseBuffer, p) {
const start = ctx.currentTime;
const source = ctx.createBufferSource();
source.buffer = noiseBuffer;
const filter = ctx.createBiquadFilter();
filter.type = p.filter ?? 'lowpass';
filter.Q.value = p.q ?? 1;
filter.frequency.setValueAtTime(p.freq, start);
if (p.freqEnd) filter.frequency.exponentialRampToValueAtTime(p.freqEnd, start + p.duration);
const gain = ctx.createGain();
envelope(gain, start, p.volume, p.attack ?? 0.004, p.duration);
source.connect(filter).connect(gain).connect(destination);
source.start(start, Math.random() * 0.5);
source.stop(start + p.duration + 0.05);
}
/** Tonfolge (Fanfaren, Hinweise). */
function arpeggio(ctx, dest, freqs, step, type, volume) {
freqs.forEach((freq, i) => tone(ctx, dest, { type, freq, duration: step * 1.6, volume, delay: i * step }));
}
/**
* Rezepte je Effektname. `v` ist die Lautstärke (0..1) des Effekts.
* @type {Record<string, (ctx: AudioContext, dest: AudioNode, noiseBuffer: AudioBuffer, v: number) => void>}
*/
export const SYNTH_RECIPES = {
'shot-carbine': (c, d, n, v) => {
tone(c, d, { type: 'square', freq: 1400, freqEnd: 300, duration: 0.08, volume: 0.25 * v });
noise(c, d, n, { filter: 'highpass', freq: 2500, duration: 0.06, volume: 0.2 * v });
},
'shot-shotgun': (c, d, n, v) => noise(c, d, n, { freq: 3000, freqEnd: 300, duration: 0.25, volume: 0.6 * v }),
'shot-pistol': (c, d, _n, v) => tone(c, d, { type: 'square', freq: 900, freqEnd: 200, duration: 0.1, volume: 0.3 * v }),
'turret-shot': (c, d, _n, v) => tone(c, d, { type: 'triangle', freq: 1800, freqEnd: 600, duration: 0.06, volume: 0.25 * v }),
'enemy-shot': (c, d, _n, v) => tone(c, d, { type: 'sawtooth', freq: 500, freqEnd: 150, duration: 0.12, volume: 0.15 * v }),
'hit-flesh': (c, d, n, v) => noise(c, d, n, { filter: 'bandpass', freq: 700, q: 2, duration: 0.07, volume: 0.5 * v }),
'hit-metal': (c, d, _n, v) => tone(c, d, { type: 'triangle', freq: 2200, freqEnd: 1600, duration: 0.09, volume: 0.25 * v }),
'death-swarm': (c, d, n, v) => noise(c, d, n, { filter: 'bandpass', freq: 900, freqEnd: 200, q: 3, duration: 0.3, volume: 0.6 * v }),
'death-legion': (c, d, n, v) => noise(c, d, n, { freq: 1800, freqEnd: 200, duration: 0.35, volume: 0.6 * v }),
'explosion-small': (c, d, n, v) => {
noise(c, d, n, { freq: 1200, freqEnd: 120, duration: 0.6, volume: 0.8 * v });
tone(c, d, { freq: 110, freqEnd: 40, duration: 0.35, volume: 0.6 * v });
},
'explosion-big': (c, d, n, v) => {
noise(c, d, n, { freq: 900, freqEnd: 60, duration: 1.4, volume: 1 * v });
tone(c, d, { freq: 80, freqEnd: 30, duration: 0.9, volume: 0.9 * v });
},
'player-hit': (c, d, n, v) => {
tone(c, d, { type: 'square', freq: 220, freqEnd: 90, duration: 0.15, volume: 0.35 * v });
noise(c, d, n, { filter: 'bandpass', freq: 500, duration: 0.1, volume: 0.4 * v });
},
'player-death': (c, d, n, v) => {
noise(c, d, n, { freq: 700, freqEnd: 50, duration: 1.8, volume: 1 * v });
tone(c, d, { type: 'sawtooth', freq: 300, freqEnd: 40, duration: 1.6, volume: 0.3 * v });
},
reload: (c, d, n, v) => noise(c, d, n, { filter: 'bandpass', freq: 2500, q: 4, duration: 0.08, volume: 0.5 * v }),
'reload-done': (c, d, _n, v) => tone(c, d, { type: 'square', freq: 1200, duration: 0.04, volume: 0.2 * v }),
switch: (c, d, _n, v) => tone(c, d, { type: 'triangle', freq: 700, duration: 0.05, volume: 0.3 * v }),
'combo-step': (c, d, _n, v) => tone(c, d, { type: 'sine', freq: 880, duration: 0.05, volume: 0.35 * v }),
'combo-fail': (c, d, _n, v) => tone(c, d, { type: 'square', freq: 180, freqEnd: 120, duration: 0.18, volume: 0.25 * v }),
'orbital-call': (c, d, _n, v) => arpeggio(c, d, [523, 659, 784, 1047], 0.06, 'triangle', 0.3 * v),
pickup: (c, d, _n, v) => arpeggio(c, d, [659, 784, 988, 1319], 0.05, 'sine', 0.35 * v),
brood: (c, d, n, v) => noise(c, d, n, { filter: 'bandpass', freq: 400, freqEnd: 900, q: 5, duration: 0.4, volume: 0.5 * v }),
'ui-click': (c, d, _n, v) => tone(c, d, { type: 'sine', freq: 1000, duration: 0.03, volume: 0.3 * v }),
'ui-confirm': (c, d, _n, v) => arpeggio(c, d, [784, 1047], 0.06, 'triangle', 0.3 * v),
'wave-start': (c, d, _n, v) => arpeggio(c, d, [392, 523, 659, 784], 0.12, 'sawtooth', 0.12 * v),
'wave-cleared': (c, d, _n, v) => arpeggio(c, d, [523, 659, 784, 1047, 1319], 0.1, 'square', 0.1 * v),
alarm: (c, d, _n, v) => {
for (let i = 0; i < 3; i += 1) {
tone(c, d, { type: 'square', freq: 660, duration: 0.14, volume: 0.15 * v, delay: i * 0.3 });
tone(c, d, { type: 'square', freq: 880, duration: 0.14, volume: 0.15 * v, delay: i * 0.3 + 0.15 });
}
},
'game-over': (c, d, _n, v) => arpeggio(c, d, [392, 349, 311, 262], 0.22, 'triangle', 0.3 * v),
dodge: (c, d, n, v) => noise(c, d, n, { filter: 'bandpass', freq: 400, freqEnd: 2400, q: 1.5, duration: 0.25, volume: 0.5 * v, attack: 0.05 }),
// Pfeifen passend zur Mörser-Vorwarnzeit: von hoch nach tief.
mortar: (c, d, _n, v) => tone(c, d, { type: 'sine', freq: 1500, freqEnd: 450, duration: 1.3, volume: 0.2 * v, attack: 0.1 }),
};