// Moderne MotorSound-Lib mit Web Audio API (AudioWorklet statt ScriptProcessor) // Geschwindigkeitsabhängiges Motorgeräusch für Taxi-Spiel class MotorSound { constructor(context, generator) { this.context = context; this.generator = generator; this.speed = 0.6; // incoming control value (0.3..1.5) this.isPlaying = false; this.currentFrame = 0; this.workletNode = null; this.gainNode = null; this.data = null; this.sampleRate = context.sampleRate || 44100; this.step = 1; // table index increment per sample, derived from target frequency this.regenerate(); } async init() { // Verwende ScriptProcessor für bessere Kompatibilität console.log('Initialisiere MotorSound mit ScriptProcessor'); // Kleinere Buffergröße für geringere Latenz this.workletNode = this.context.createScriptProcessor(256); this.workletNode.onaudioprocess = this.process.bind(this); // Filter-Kette für motorähnlicheren Klang this.lowShelf = this.context.createBiquadFilter(); this.lowShelf.type = 'lowshelf'; this.lowShelf.frequency.value = 150; this.lowShelf.gain.value = 6; this.lowPass = this.context.createBiquadFilter(); this.lowPass.type = 'lowpass'; this.lowPass.frequency.value = 1200; this.lowPass.Q.value = 0.5; this.gainNode = this.context.createGain(); this.gainNode.gain.setValueAtTime(0.25, this.context.currentTime); // Verbindung: script -> lowshelf -> lowpass -> gain this.workletNode.connect(this.lowShelf); this.lowShelf.connect(this.lowPass); this.lowPass.connect(this.gainNode); console.log('MotorSound ScriptProcessor initialisiert'); } start() { if (!this.gainNode) return; if (this.context.state === 'suspended') { this.context.resume().catch(() => {}); } this.gainNode.connect(this.context.destination); this.isPlaying = true; } stop() { if (!this.gainNode || !this.gainNode.context) return; try { this.gainNode.disconnect(this.context.destination); } catch (_) { // bereits disconnected } this.isPlaying = false; } regenerate() { this.data = this.generator.generate(); console.log('MotorSound Daten regeneriert, Länge:', this.data.length); } setVolume(volume) { if (this.gainNode) this.gainNode.gain.value = volume; } setGenerator(generator) { this.generator = generator; this.regenerate(); } setSpeed(speed) { // speed erwartet ca. 0.3..1.5 (von Spiel geliefert). Normiere auf 0..1 const normalized = Math.max(0, Math.min(1, (speed - 0.3) / 1.2)); // Ziel-Frequenz für Motorgrundton (Hz) const freq = 40 + normalized * 160; // 40..200 Hz // Tabellen-Schritt für gewünschte Frequenz this.step = (freq * this.data.length) / this.sampleRate; } process(event) { if (!this.data || this.data.length === 0) return; const output = event.outputBuffer; const ch0 = output.getChannelData(0); const ch1 = output.numberOfChannels > 1 ? output.getChannelData(1) : ch0; for (let i = 0; i < ch0.length; ++i) { this.currentFrame += this.step; const index = Math.floor(this.currentFrame) % this.data.length; const sample = this.data[index] * 0.9; // etwas dämpfen ch0[i] = sample; ch1[i] = sample; } this.currentFrame %= this.data.length; } } class LinearGenerator { constructor() { this.dataLength = 1024; } pushLinear(data, toValue, toPosition) { const lastPosition = data.length - 1; const lastValue = data.pop(); const positionDiff = toPosition - lastPosition; const step = (toValue - lastValue) / positionDiff; for (let i = 0; i < positionDiff; i++) { data.push(lastValue + step * i); } return data; } generate() { const data = []; let lastValue = 1; data.push(lastValue); for (let i = 0.05; i < 1; i += Math.random() / 8 + 0.01) { const nextPosition = Math.floor(i * this.dataLength); const nextValue = Math.random() * 2 - 1; this.pushLinear(data, nextValue, nextPosition); } this.pushLinear(data, 1, this.dataLength); return data; } } class NoiseGenerator { constructor() { this.dataLength = 4096; this.linearLength = 30; this.smoothness = 3; } generate() { const data = []; let lastValue = 0.5; data.push(lastValue); for (let i = 1; i <= this.dataLength - this.linearLength; i++) { lastValue += (Math.random() - 0.5) / this.smoothness; lastValue = Math.min(1, lastValue); lastValue = Math.max(-1, lastValue); data.push(lastValue); } const step = (0.5 - lastValue) / this.linearLength; for (let j = 0; j < this.linearLength; j++) { data.push(lastValue + step * j); } data.push(0.5); return data; } } class CanvasGenerator { constructor() { this.canvas = document.createElement('canvas'); this.canvas.width = 1024; this.canvas.height = 1; this.ctx = this.canvas.getContext('2d'); } getRandomGradient() { const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, 0); gradient.addColorStop(0, "rgba(0, 0, 0, 255)"); for (let i = 0.05; i < 1; i += Math.random() / 8 + 0.01) { gradient.addColorStop(i, "rgba(0, 0, 0," + Math.random() + ")"); } gradient.addColorStop(1, "rgba(0, 0, 0, 255)"); return gradient; } generate() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = this.getRandomGradient(); this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); const imageData = this.ctx.getImageData(0, 0, this.canvas.width, 1).data; const data = []; for (let i = 3, len = imageData.length; i < len; i += 4) { data.push(imageData[i] / 128 - 1); } return data; } } export { MotorSound, LinearGenerator, NoiseGenerator, CanvasGenerator };