class AudioManager { constructor() { this.ctx = null; this.isInitialized = false; this.isPlaying = false; this.masterGain = null; this.compressor = null; this.analyser = null; this.currentVolume = 0.75; this.isMuted = false; // Sleep Timer this.timerId = null; this.timerRemainingSeconds = 0; this.onTimerTick = null; this.onTimerComplete = null; } init() { if (this.isInitialized) return; const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioContextClass(); // Master Dynamics Compressor / Limiter for studio-quality mastering & anti-clipping this.compressor = this.ctx.createDynamicsCompressor(); this.compressor.threshold.setValueAtTime(-12, this.ctx.currentTime); this.compressor.knee.setValueAtTime(8, this.ctx.currentTime); this.compressor.ratio.setValueAtTime(4, this.ctx.currentTime); this.compressor.attack.setValueAtTime(0.003, this.ctx.currentTime); this.compressor.release.setValueAtTime(0.25, this.ctx.currentTime); // Master Gain this.masterGain = this.ctx.createGain(); this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : this.currentVolume, this.ctx.currentTime); // Master Analyser Node for Visualizers this.analyser = this.ctx.createAnalyser(); this.analyser.fftSize = 512; this.analyser.smoothingTimeConstant = 0.82; // Route: Nodes -> Compressor -> MasterGain -> Analyser -> Destination this.compressor.connect(this.masterGain); this.masterGain.connect(this.analyser); this.analyser.connect(this.ctx.destination); this.isInitialized = true; } async resume() { if (!this.isInitialized) this.init(); if (this.ctx.state === 'suspended') { await this.ctx.resume(); } } setMasterVolume(val, smoothTime = 0.05) { const clamped = Math.max(0, Math.min(1, val)); this.currentVolume = clamped; if (this.masterGain && this.ctx) { const target = this.isMuted ? 0 : clamped; const now = this.ctx.currentTime; this.masterGain.gain.cancelScheduledValues(now); this.masterGain.gain.linearRampToValueAtTime(target, now + smoothTime); } } getMasterVolume() { return this.currentVolume; } toggleMute() { return this.setMute(!this.isMuted); } setMute(muted) { this.isMuted = !!muted; if (this.masterGain && this.ctx) { const target = this.isMuted ? 0 : this.currentVolume; const now = this.ctx.currentTime; this.masterGain.gain.cancelScheduledValues(now); this.masterGain.gain.linearRampToValueAtTime(target, now + 0.05); } return this.isMuted; } // Noise Buffer Helper (White, Pink, Brown) createNoiseBuffer(type = 'pink', durationSeconds = 5) { if (!this.ctx) this.init(); const sampleRate = this.ctx.sampleRate; const bufferSize = sampleRate * durationSeconds; const buffer = this.ctx.createBuffer(2, bufferSize, sampleRate); const left = buffer.getChannelData(0); const right = buffer.getChannelData(1); if (type === 'white') { for (let i = 0; i < bufferSize; i++) { left[i] = Math.random() * 2 - 1; right[i] = Math.random() * 2 - 1; } } else if (type === 'pink') { let b0L = 0, b1L = 0, b2L = 0, b3L = 0, b4L = 0, b5L = 0, b6L = 0; let b0R = 0, b1R = 0, b2R = 0, b3R = 0, b4R = 0, b5R = 0, b6R = 0; for (let i = 0; i < bufferSize; i++) { const whiteL = Math.random() * 2 - 1; b0L = 0.99886 * b0L + whiteL * 0.0555179; b1L = 0.99332 * b1L + whiteL * 0.0750759; b2L = 0.96900 * b2L + whiteL * 0.1538520; b3L = 0.86650 * b3L + whiteL * 0.3104856; b4L = 0.55000 * b4L + whiteL * 0.5329522; b5L = -0.7616 * b5L - whiteL * 0.0168980; left[i] = (b0L + b1L + b2L + b3L + b4L + b5L + b6L + whiteL * 0.5362) * 0.11; b6L = whiteL * 0.115926; const whiteR = Math.random() * 2 - 1; b0R = 0.99886 * b0R + whiteR * 0.0555179; b1R = 0.99332 * b1R + whiteR * 0.0750759; b2R = 0.96900 * b2R + whiteR * 0.1538520; b3R = 0.86650 * b3R + whiteR * 0.3104856; b4R = 0.55000 * b4R + whiteR * 0.5329522; b5R = -0.7616 * b5R - whiteR * 0.0168980; right[i] = (b0R + b1R + b2R + b3R + b4R + b5R + b6R + whiteR * 0.5362) * 0.11; b6R = whiteR * 0.115926; } } else if (type === 'brown') { let lastOutL = 0.0; let lastOutR = 0.0; for (let i = 0; i < bufferSize; i++) { const whiteL = Math.random() * 2 - 1; lastOutL = (lastOutL + 0.02 * whiteL) / 1.02; left[i] = lastOutL * 3.5; const whiteR = Math.random() * 2 - 1; lastOutR = (lastOutR + 0.02 * whiteR) / 1.02; right[i] = lastOutR * 3.5; } } return buffer; } // Sleep Timer System startSleepTimer(minutes, onTick, onComplete) { this.stopSleepTimer(); this.timerRemainingSeconds = Math.round(minutes * 60); this.onTimerTick = onTick; this.onTimerComplete = onComplete; if (this.onTimerTick) this.onTimerTick(this.timerRemainingSeconds); this.timerId = setInterval(() => { this.timerRemainingSeconds--; if (this.onTimerTick) this.onTimerTick(this.timerRemainingSeconds); // Begin exponential smooth fadeout during final 30 seconds if (this.timerRemainingSeconds <= 30 && this.timerRemainingSeconds > 0) { const factor = this.timerRemainingSeconds / 30; if (this.masterGain && this.ctx) { const targetVol = this.getMasterVolume() * factor; this.masterGain.gain.setValueAtTime(Math.max(0, targetVol), this.ctx.currentTime); } } if (this.timerRemainingSeconds <= 0) { this.stopSleepTimer(); if (this.onTimerComplete) this.onTimerComplete(); } }, 1000); } stopSleepTimer() { if (this.timerId) { clearInterval(this.timerId); this.timerId = null; this.timerRemainingSeconds = 0; } } } window.AudioManager = AudioManager; /** * Hull Drone & Environmental Sub-Bass Synthesizer * Generates organic, continuous low-frequency starship structural vibration & room tone. */ class HullDroneSynth { constructor(audioManager) { this.am = audioManager; this.nodes = []; this.gainNode = null; this.filterNode = null; this.subOsc1 = null; this.subOsc2 = null; this.noiseSource = null; this.isMuted = false; // Default configuration parameters this.params = { volume: 0.7, baseFreq: 50, // Fundamental frequency (e.g. 50Hz for TNG bridge) filterCutoff: 110, // Lowpass filter cutoff resonance: 2.5, // Filter Q / resonance peak noiseMix: 0.45, // Brown noise texture mix harmonicSpread: 1.02 // Slight frequency detune between sub-oscillators for phasing }; } start(rampDuration = 0) { this.stop(); const ctx = this.am.ctx; if (!ctx) return; // Channel Gain Node this.gainNode = ctx.createGain(); const targetVol = this.isMuted ? 0 : this.params.volume; if (rampDuration > 0) { this.gainNode.gain.setValueAtTime(0.0001, ctx.currentTime); this.gainNode.gain.linearRampToValueAtTime(targetVol, ctx.currentTime + rampDuration); } else { this.gainNode.gain.setValueAtTime(targetVol, ctx.currentTime); } // Steep Lowpass Filter (24dB/oct via 2 cascading biquads) this.filterNode = ctx.createBiquadFilter(); this.filterNode.type = 'lowpass'; this.filterNode.frequency.setValueAtTime(this.params.filterCutoff, ctx.currentTime); this.filterNode.Q.setValueAtTime(this.params.resonance, ctx.currentTime); const filterStage2 = ctx.createBiquadFilter(); filterStage2.type = 'lowpass'; filterStage2.frequency.setValueAtTime(this.params.filterCutoff * 1.5, ctx.currentTime); filterStage2.Q.setValueAtTime(1.0, ctx.currentTime); // Sub-bass Oscillator 1 (Sine) this.subOsc1 = ctx.createOscillator(); this.subOsc1.type = 'sine'; this.subOsc1.frequency.setValueAtTime(this.params.baseFreq, ctx.currentTime); const osc1Gain = ctx.createGain(); osc1Gain.gain.setValueAtTime(0.5, ctx.currentTime); this.subOsc1.connect(osc1Gain); osc1Gain.connect(this.filterNode); // Sub-bass Oscillator 2 (Triangle/Sine detuned for slow, natural phase beating) this.subOsc2 = ctx.createOscillator(); this.subOsc2.type = 'triangle'; this.subOsc2.frequency.setValueAtTime(this.params.baseFreq * this.params.harmonicSpread, ctx.currentTime); const osc2Gain = ctx.createGain(); osc2Gain.gain.setValueAtTime(0.3, ctx.currentTime); this.subOsc2.connect(osc2Gain); osc2Gain.connect(this.filterNode); // Brown Noise Structural Rumble Layer const brownBuffer = this.am.createNoiseBuffer('brown', 6); this.noiseSource = ctx.createBufferSource(); this.noiseSource.buffer = brownBuffer; this.noiseSource.loop = true; const noiseGain = ctx.createGain(); noiseGain.gain.setValueAtTime(this.params.noiseMix * 0.7, ctx.currentTime); this.noiseSource.connect(noiseGain); noiseGain.connect(this.filterNode); // Slow LFO for organic drifting movement const lfo = ctx.createOscillator(); lfo.type = 'sine'; lfo.frequency.setValueAtTime(0.1, ctx.currentTime); // 10 second cycle const lfoGain = ctx.createGain(); lfoGain.gain.setValueAtTime(12, ctx.currentTime); // Modulate cutoff by ±12Hz lfo.connect(lfoGain); lfoGain.connect(this.filterNode.frequency); // Connect Graph this.filterNode.connect(filterStage2); filterStage2.connect(this.gainNode); this.gainNode.connect(this.am.compressor); // Start Sources this.subOsc1.start(); this.subOsc2.start(); this.noiseSource.start(); lfo.start(); this.nodes = [this.subOsc1, this.subOsc2, this.noiseSource, lfo, osc1Gain, osc2Gain, noiseGain, lfoGain, this.filterNode, filterStage2, this.gainNode]; } stop(fadeDuration = 0) { if (fadeDuration > 0 && this.nodes.length > 0 && this.gainNode && this.am.ctx) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.setValueAtTime(Math.max(0.0001, this.gainNode.gain.value), now); this.gainNode.gain.linearRampToValueAtTime(0.0001, now + fadeDuration); if (this.stoppingTimeout) clearTimeout(this.stoppingTimeout); this.stoppingTimeout = setTimeout(() => { this.stop(0); }, fadeDuration * 1000); return; } if (this.stoppingTimeout) { clearTimeout(this.stoppingTimeout); this.stoppingTimeout = null; } if (this.nodes.length > 0) { try { if (this.subOsc1) this.subOsc1.stop(); if (this.subOsc2) this.subOsc2.stop(); if (this.noiseSource) this.noiseSource.stop(); } catch (e) { // Ignore if already stopped } this.nodes.forEach(node => { try { node.disconnect(); } catch (e) {} }); this.nodes = []; this.subOsc1 = null; this.subOsc2 = null; this.noiseSource = null; this.gainNode = null; } } setVolume(val) { this.params.volume = Math.max(0, Math.min(1, val)); if (this.gainNode && this.am.ctx && !this.isMuted) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05); } } setBaseFreq(freq) { this.params.baseFreq = freq; if (this.subOsc1 && this.subOsc2 && this.am.ctx) { const now = this.am.ctx.currentTime; this.subOsc1.frequency.linearRampToValueAtTime(freq, now + 0.1); this.subOsc2.frequency.linearRampToValueAtTime(freq * this.params.harmonicSpread, now + 0.1); } } setFilterCutoff(cutoff) { this.params.filterCutoff = cutoff; if (this.filterNode && this.am.ctx) { const now = this.am.ctx.currentTime; this.filterNode.frequency.linearRampToValueAtTime(cutoff, now + 0.1); } } applyPreset(config) { if (config.volume !== undefined) this.params.volume = config.volume; if (config.baseFreq !== undefined) this.setBaseFreq(config.baseFreq); if (config.filterCutoff !== undefined) this.setFilterCutoff(config.filterCutoff); if (config.resonance !== undefined) this.params.resonance = config.resonance; if (config.noiseMix !== undefined) this.params.noiseMix = config.noiseMix; if (config.harmonicSpread !== undefined) this.params.harmonicSpread = config.harmonicSpread; this.setVolume(this.params.volume); } } window.HullDroneSynth = HullDroneSynth; /** * Warp Core & Reactor Pulse Synthesizer * Generates the iconic pulsating magnetic intermix thrum of Star Trek warp cores. */ class WarpCoreSynth { constructor(audioManager) { this.am = audioManager; this.nodes = []; this.gainNode = null; this.isMuted = false; // Pulse parameters this.params = { volume: 0.8, bpm: 48, // Pulse rate (TNG is ~46-52 BPM, Voyager is ~68-75 BPM) carrierFreq: 58, // Fundamental carrier pitch (Hz) modFreqRatio: 2.0, // FM modulation frequency multiplier modIndex: 40, // FM modulation depth filterCutoff: 180, // Lowpass filter cutoff pulseShape: 'tng', // 'tng', 'voyager', 'tos', 'defiant', 'nx' resonance: 3.0, swirlMix: 0.35 // Stereo phase swirl }; this.pulseInterval = null; this.pulsePhase = 0; this.onPulse = null; // Callback for UI visualizer pulse animation! } start(rampDuration = 0) { this.stop(); const ctx = this.am.ctx; if (!ctx) return; this.gainNode = ctx.createGain(); const targetVol = this.isMuted ? 0 : this.params.volume; if (rampDuration > 0) { this.gainNode.gain.setValueAtTime(0.0001, ctx.currentTime); this.gainNode.gain.linearRampToValueAtTime(targetVol, ctx.currentTime + rampDuration); } else { this.gainNode.gain.setValueAtTime(targetVol, ctx.currentTime); } // Filter Node this.filterNode = ctx.createBiquadFilter(); this.filterNode.type = 'lowpass'; this.filterNode.frequency.setValueAtTime(this.params.filterCutoff, ctx.currentTime); this.filterNode.Q.setValueAtTime(this.params.resonance, ctx.currentTime); // Stereo Panner for magnetic swirl this.panner = ctx.createStereoPanner ? ctx.createStereoPanner() : null; // Carrier & Modulator Oscillators (FM Engine) this.carrier = ctx.createOscillator(); this.carrier.type = this.params.pulseShape === 'tos' ? 'sawtooth' : (this.params.pulseShape === 'nx' ? 'triangle' : 'sine'); if (rampDuration > 0) { this.carrier.frequency.setValueAtTime(Math.max(20, this.params.carrierFreq * 0.45), ctx.currentTime); this.carrier.frequency.exponentialRampToValueAtTime(this.params.carrierFreq, ctx.currentTime + rampDuration); } else { this.carrier.frequency.setValueAtTime(this.params.carrierFreq, ctx.currentTime); } // Sub-harmonic oscillator for massive bottom end this.subOsc = ctx.createOscillator(); this.subOsc.type = 'sine'; if (rampDuration > 0) { this.subOsc.frequency.setValueAtTime(Math.max(12, this.params.carrierFreq * 0.22), ctx.currentTime); this.subOsc.frequency.exponentialRampToValueAtTime(this.params.carrierFreq * 0.5, ctx.currentTime + rampDuration); } else { this.subOsc.frequency.setValueAtTime(this.params.carrierFreq * 0.5, ctx.currentTime); } const subGain = ctx.createGain(); subGain.gain.setValueAtTime(0.6, ctx.currentTime); this.subOsc.connect(subGain); subGain.connect(this.filterNode); // Pulse Envelope Modulator Gain Node this.pulseGain = ctx.createGain(); this.pulseGain.gain.setValueAtTime(0.2, ctx.currentTime); // Connect Carrier -> PulseGain -> Filter -> Panner -> ChannelGain -> Master this.carrier.connect(this.pulseGain); this.pulseGain.connect(this.filterNode); if (this.panner) { this.filterNode.connect(this.panner); this.panner.connect(this.gainNode); } else { this.filterNode.connect(this.gainNode); } this.gainNode.connect(this.am.compressor); this.carrier.start(); this.subOsc.start(); this.nodes = [this.carrier, this.subOsc, subGain, this.pulseGain, this.filterNode, this.gainNode]; if (this.panner) this.nodes.push(this.panner); // Start precision pulse scheduler this.startPulseLoop(); } startPulseLoop() { if (this.pulseInterval) clearInterval(this.pulseInterval); const intervalMs = (60 / this.params.bpm) * 1000; this.scheduleNextPulse(); this.pulseInterval = setInterval(() => { this.scheduleNextPulse(); }, intervalMs); } scheduleNextPulse() { const ctx = this.am.ctx; if (!ctx || !this.pulseGain || !this.filterNode) return; const now = ctx.currentTime; const pulseDuration = (60 / this.params.bpm); this.pulsePhase = (this.pulsePhase + 1) % 4; // Trigger visualizer callback if (this.onPulse) { this.onPulse(this.pulsePhase, pulseDuration); } // Dynamic envelope shaping based on ship era if (this.params.pulseShape === 'tng') { // Iconic 4-stage Galaxy-class magnetic warp pulse // Soft attack, deep swelling peak, secondary reverberant harmonic bloom, smooth decay const peakTime = now + pulseDuration * 0.28; const secondPeak = now + pulseDuration * 0.58; this.pulseGain.gain.cancelScheduledValues(now); this.pulseGain.gain.setValueAtTime(0.18, now); this.pulseGain.gain.linearRampToValueAtTime(0.95, peakTime); this.pulseGain.gain.exponentialRampToValueAtTime(0.45, now + pulseDuration * 0.42); this.pulseGain.gain.linearRampToValueAtTime(0.65, secondPeak); this.pulseGain.gain.exponentialRampToValueAtTime(0.18, now + pulseDuration * 0.95); // Modulate filter cutoff in sync with the pulse this.filterNode.frequency.cancelScheduledValues(now); this.filterNode.frequency.setValueAtTime(this.params.filterCutoff * 0.7, now); this.filterNode.frequency.exponentialRampToValueAtTime(this.params.filterCutoff * 1.6, peakTime); this.filterNode.frequency.exponentialRampToValueAtTime(this.params.filterCutoff * 0.7, now + pulseDuration * 0.95); } else if (this.params.pulseShape === 'voyager') { // Faster, sharper, higher-resonance Class 9 warp core const peakTime = now + pulseDuration * 0.2; this.pulseGain.gain.cancelScheduledValues(now); this.pulseGain.gain.setValueAtTime(0.25, now); this.pulseGain.gain.linearRampToValueAtTime(1.0, peakTime); this.pulseGain.gain.exponentialRampToValueAtTime(0.25, now + pulseDuration * 0.85); this.filterNode.frequency.cancelScheduledValues(now); this.filterNode.frequency.linearRampToValueAtTime(this.params.filterCutoff * 1.8, peakTime); this.filterNode.frequency.linearRampToValueAtTime(this.params.filterCutoff * 0.8, now + pulseDuration * 0.85); } else if (this.params.pulseShape === 'tos') { // TOS Electromechanical oscillating engine thrum const halfTime = now + pulseDuration * 0.5; this.pulseGain.gain.cancelScheduledValues(now); this.pulseGain.gain.setValueAtTime(0.4, now); this.pulseGain.gain.linearRampToValueAtTime(0.9, halfTime); this.pulseGain.gain.linearRampToValueAtTime(0.4, now + pulseDuration); } else if (this.params.pulseShape === 'defiant') { // Defiant: tight, aggressive pulse with rapid decay const peakTime = now + pulseDuration * 0.15; this.pulseGain.gain.cancelScheduledValues(now); this.pulseGain.gain.setValueAtTime(0.3, now); this.pulseGain.gain.linearRampToValueAtTime(1.0, peakTime); this.pulseGain.gain.exponentialRampToValueAtTime(0.3, now + pulseDuration * 0.75); } else { // NX / Industrial reactor chug const peakTime = now + pulseDuration * 0.35; this.pulseGain.gain.cancelScheduledValues(now); this.pulseGain.gain.setValueAtTime(0.2, now); this.pulseGain.gain.linearRampToValueAtTime(0.85, peakTime); this.pulseGain.gain.linearRampToValueAtTime(0.2, now + pulseDuration); } // Subtle stereo panning drift if (this.panner && this.params.swirlMix > 0) { const panTarget = Math.sin(this.pulsePhase * Math.PI * 0.5) * this.params.swirlMix; this.panner.pan.linearRampToValueAtTime(panTarget, now + pulseDuration * 0.5); } } setBpm(bpm) { this.params.bpm = Math.max(20, Math.min(160, bpm)); if (this.nodes.length > 0) { this.startPulseLoop(); } } setVolume(val) { this.params.volume = Math.max(0, Math.min(1, val)); if (this.gainNode && this.am.ctx && !this.isMuted) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05); } } setCarrierFreq(freq) { this.params.carrierFreq = freq; if (this.carrier && this.subOsc && this.am.ctx) { const now = this.am.ctx.currentTime; this.carrier.frequency.linearRampToValueAtTime(freq, now + 0.1); this.subOsc.frequency.linearRampToValueAtTime(freq * 0.5, now + 0.1); } } stop(fadeDuration = 0) { if (fadeDuration > 0 && this.nodes.length > 0 && this.gainNode && this.am.ctx) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.setValueAtTime(Math.max(0.0001, this.gainNode.gain.value), now); this.gainNode.gain.linearRampToValueAtTime(0.0001, now + fadeDuration); if (this.carrier) { try { this.carrier.frequency.cancelScheduledValues(now); this.carrier.frequency.setValueAtTime(this.carrier.frequency.value, now); this.carrier.frequency.linearRampToValueAtTime(Math.max(20, this.params.carrierFreq * 0.35), now + fadeDuration); } catch (e) {} } if (this.stoppingTimeout) clearTimeout(this.stoppingTimeout); this.stoppingTimeout = setTimeout(() => { this.stop(0); }, fadeDuration * 1000); return; } if (this.stoppingTimeout) { clearTimeout(this.stoppingTimeout); this.stoppingTimeout = null; } if (this.pulseInterval) { clearInterval(this.pulseInterval); this.pulseInterval = null; } if (this.nodes.length > 0) { try { if (this.carrier) this.carrier.stop(); if (this.subOsc) this.subOsc.stop(); } catch (e) {} this.nodes.forEach(node => { try { node.disconnect(); } catch (e) {} }); this.nodes = []; this.carrier = null; this.subOsc = null; this.gainNode = null; } } applyPreset(config) { if (config.volume !== undefined) this.params.volume = config.volume; if (config.bpm !== undefined) this.setBpm(config.bpm); if (config.carrierFreq !== undefined) this.setCarrierFreq(config.carrierFreq); if (config.filterCutoff !== undefined) this.params.filterCutoff = config.filterCutoff; if (config.pulseShape !== undefined) this.params.pulseShape = config.pulseShape; if (config.resonance !== undefined) this.params.resonance = config.resonance; if (config.swirlMix !== undefined) this.params.swirlMix = config.swirlMix; this.setVolume(this.params.volume); } } window.WarpCoreSynth = WarpCoreSynth; /** * Environmental Life Support & Airflow Synthesizer * Generates continuous ventilation airflow, atmospheric hiss, and room acoustic damping. */ class LifeSupportSynth { constructor(audioManager) { this.am = audioManager; this.nodes = []; this.gainNode = null; this.isMuted = false; this.params = { volume: 0.5, noiseType: 'pink', // 'pink' (warm TNG), 'white' (crisp Voyager), 'brown' (heavy NX-01) highpassFreq: 180, // Cuts extreme sub rumble to isolate air movement lowpassFreq: 1800, // Gentle top-end rolloff airflowModSpeed: 0.15, // Subtle breathing movement of the environmental airflow airflowModDepth: 0.12 // Depth of airflow intensity modulation }; } start(rampDuration = 0) { this.stop(); const ctx = this.am.ctx; if (!ctx) return; this.gainNode = ctx.createGain(); const targetVol = this.isMuted ? 0 : this.params.volume; if (rampDuration > 0) { this.gainNode.gain.setValueAtTime(0.0001, ctx.currentTime); this.gainNode.gain.linearRampToValueAtTime(targetVol, ctx.currentTime + rampDuration); } else { this.gainNode.gain.setValueAtTime(targetVol, ctx.currentTime); } // Highpass filter (cuts muddy lows) const hpFilter = ctx.createBiquadFilter(); hpFilter.type = 'highpass'; hpFilter.frequency.setValueAtTime(this.params.highpassFreq, ctx.currentTime); // Lowpass filter (shapes the crispness vs warmth of the air) this.lpFilter = ctx.createBiquadFilter(); this.lpFilter.type = 'lowpass'; this.lpFilter.frequency.setValueAtTime(this.params.lowpassFreq, ctx.currentTime); this.lpFilter.Q.setValueAtTime(0.7, ctx.currentTime); // Noise buffer source const noiseBuffer = this.am.createNoiseBuffer(this.params.noiseType, 6); this.noiseSource = ctx.createBufferSource(); this.noiseSource.buffer = noiseBuffer; this.noiseSource.loop = true; // Slow airflow modulation LFO for organic breath const airflowLfo = ctx.createOscillator(); airflowLfo.type = 'sine'; airflowLfo.frequency.setValueAtTime(this.params.airflowModSpeed, ctx.currentTime); const lfoGain = ctx.createGain(); lfoGain.gain.setValueAtTime(this.params.airflowModDepth, ctx.currentTime); const modGain = ctx.createGain(); modGain.gain.setValueAtTime(0.8, ctx.currentTime); airflowLfo.connect(lfoGain); lfoGain.connect(modGain.gain); // Stereo widener using delay const splitter = ctx.createChannelSplitter(2); const merger = ctx.createChannelMerger(2); const delayRight = ctx.createDelay(); delayRight.delayTime.setValueAtTime(0.018, ctx.currentTime); // 18ms Haas effect widening // Graph: Noise -> ModGain -> HP -> LP -> Splitter -> (Left direct, Right delay) -> Merger -> Gain -> Compressor this.noiseSource.connect(modGain); modGain.connect(hpFilter); hpFilter.connect(this.lpFilter); this.lpFilter.connect(splitter); splitter.connect(merger, 0, 0); // Left channel splitter.connect(delayRight, 1); delayRight.connect(merger, 0, 1); // Right delayed channel merger.connect(this.gainNode); this.gainNode.connect(this.am.compressor); this.noiseSource.start(); airflowLfo.start(); this.nodes = [ this.noiseSource, airflowLfo, lfoGain, modGain, hpFilter, this.lpFilter, splitter, delayRight, merger, this.gainNode ]; } stop(fadeDuration = 0) { if (fadeDuration > 0 && this.nodes.length > 0 && this.gainNode && this.am.ctx) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.setValueAtTime(Math.max(0.0001, this.gainNode.gain.value), now); this.gainNode.gain.linearRampToValueAtTime(0.0001, now + fadeDuration); if (this.stoppingTimeout) clearTimeout(this.stoppingTimeout); this.stoppingTimeout = setTimeout(() => { this.stop(0); }, fadeDuration * 1000); return; } if (this.stoppingTimeout) { clearTimeout(this.stoppingTimeout); this.stoppingTimeout = null; } if (this.nodes.length > 0) { try { if (this.noiseSource) this.noiseSource.stop(); } catch (e) {} this.nodes.forEach(node => { try { node.disconnect(); } catch (e) {} }); this.nodes = []; this.noiseSource = null; this.gainNode = null; } } setVolume(val) { this.params.volume = Math.max(0, Math.min(1, val)); if (this.gainNode && this.am.ctx && !this.isMuted) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05); } } setFilterCutoff(freq) { this.params.lowpassFreq = freq; if (this.lpFilter && this.am.ctx) { const now = this.am.ctx.currentTime; this.lpFilter.frequency.linearRampToValueAtTime(freq, now + 0.1); } } applyPreset(config) { if (config.volume !== undefined) this.params.volume = config.volume; if (config.noiseType !== undefined) this.params.noiseType = config.noiseType; if (config.highpassFreq !== undefined) this.params.highpassFreq = config.highpassFreq; if (config.lowpassFreq !== undefined) this.setFilterCutoff(config.lowpassFreq); if (config.airflowModSpeed !== undefined) this.params.airflowModSpeed = config.airflowModSpeed; if (config.airflowModDepth !== undefined) this.params.airflowModDepth = config.airflowModDepth; this.setVolume(this.params.volume); } } window.LifeSupportSynth = LifeSupportSynth; /** * Procedural Starship Telemetry, LCARS Chirps, Beeps & Console Synthesizer * 100% synthesized programmatically via Web Audio API oscillators, FM synthesis, and envelopes. * Zero stored audio samples. */ class TelemetrySynth { constructor(audioManager) { this.am = audioManager; this.gainNode = null; this.isMuted = false; this.schedulerTimer = null; this.params = { volume: 0.4, density: 0.5, // How often background telemetry chirps occur (0 = off, 1 = busy bridge) era: 'tng', // 'tng', 'voyager', 'tos', 'ds9', 'nx' reverbMix: 0.25 }; // Musical pitch frequencies for authentic LCARS musical intervals (major/minor pentatonic & perfect 4ths/5ths) this.lcarsPitches = [ 880, 987.77, 1046.50, 1174.66, 1318.51, 1396.91, 1567.98, 1760, 1975.53, 2093.00, 2349.32, 2637.02 ]; // TOS Bridge oscillator warble frequencies this.tosFrequencies = [ 440, 554.37, 659.25, 830.61, 880, 1108.73, 1318.51, 1661.22, 2217.46 ]; } start() { this.stop(); const ctx = this.am.ctx; if (!ctx) return; this.gainNode = ctx.createGain(); this.gainNode.gain.setValueAtTime(this.isMuted ? 0 : this.params.volume, ctx.currentTime); this.gainNode.connect(this.am.compressor); this.startAutoTelemetryScheduler(); } stop() { if (this.schedulerTimer) { clearTimeout(this.schedulerTimer); this.schedulerTimer = null; } } setVolume(val) { this.params.volume = Math.max(0, Math.min(1, val)); if (this.gainNode && this.am.ctx && !this.isMuted) { const now = this.am.ctx.currentTime; this.gainNode.gain.cancelScheduledValues(now); this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05); } } setDensity(val) { this.params.density = Math.max(0, Math.min(1, val)); } startAutoTelemetryScheduler() { if (this.schedulerTimer) clearTimeout(this.schedulerTimer); if (this.params.density <= 0.01) return; // Calculate delay inversely proportional to density (2s to 12s) const baseDelay = 12000 * (1.05 - this.params.density); const jitter = Math.random() * 4000; const nextInterval = Math.max(800, baseDelay + jitter); this.schedulerTimer = setTimeout(() => { this.playRandomTelemetrySound(); this.startAutoTelemetryScheduler(); }, nextInterval); } playRandomTelemetrySound() { if (this.isMuted || this.params.volume <= 0.01 || !this.am.ctx) return; switch (this.params.era) { case 'tos': Math.random() > 0.4 ? this.synthesizeTOSWarble() : this.synthesizeTOSRelayClick(); break; case 'ds9': Math.random() > 0.5 ? this.synthesizeCardassianSensor() : this.synthesizeLCARSSingleChirp(); break; case 'voyager': { const r = Math.random(); if (r < 0.12) this.synthesizeCommBadge(); else if (r < 0.55) this.synthesizeLCARSDoubleChirp(); else this.synthesizeSensorSweep(); break; } case 'nx': Math.random() > 0.5 ? this.synthesizeNXRelay() : this.synthesizeNXIndicatorBeep(); break; case 'tng': default: { const r = Math.random(); if (r < 0.10) this.synthesizeCommBadge(); else if (r < 0.45) this.synthesizeLCARSSingleChirp(); else if (r < 0.72) this.synthesizeLCARSDoubleChirp(); else if (r < 0.88) this.synthesizeLCARSSequence(); else this.synthesizeSensorSweep(); break; } } // OBSERVATION intentionally treats telemetry as an abstract activity pulse, // not as a claim that a specific fictional beep means a specific thing. window.dispatchEvent(new CustomEvent('scifi-telemetry-activity', { detail: { era: this.params.era, density: this.params.density, firedAt: performance.now() } })); } /** * Star Trek Comm Badge Confirmation Chirp */ synthesizeCommBadge() { if (window.expandedAudio) { window.expandedAudio.synthesizeCommBadge(); return; } const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; [784, 1396.91].forEach((freq, idx) => { const t = now + idx * 0.038; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(0.35, t + 0.005); env.gain.exponentialRampToValueAtTime(0.001, t + (idx === 0 ? 0.045 : 0.09)); osc.connect(env); env.connect(this.gainNode); osc.start(t); osc.stop(t + (idx === 0 ? 0.05 : 0.095)); }); } /** * TNG/Voyager Single LCARS Touch Tone (Soft sine with gentle attack and rapid exponential decay) */ synthesizeLCARSSingleChirp(pitch = null) { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const freq = pitch || this.lcarsPitches[Math.floor(Math.random() * this.lcarsPitches.length)]; const duration = 0.09; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now); // Subtle downward micro-pitch glide (12Hz) for that warm capacitive touch feel osc.frequency.exponentialRampToValueAtTime(freq * 0.98, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.35, now + 0.008); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); // Filter to eliminate any click const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(3200, now); osc.connect(env); env.connect(filter); filter.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * TNG LCARS Double Chirp (Iconic standard confirmation tone) */ synthesizeLCARSDoubleChirp() { const ctx = this.am.ctx; if (!ctx) return; const idx = Math.floor(Math.random() * (this.lcarsPitches.length - 2)); const p1 = this.lcarsPitches[idx]; const p2 = this.lcarsPitches[idx + 2]; // Minor third or fourth higher this.synthesizeLCARSSingleChirp(p1); setTimeout(() => { this.synthesizeLCARSSingleChirp(p2); }, 65); } /** * TNG LCARS Multi-Tone Data Acknowledgment Sequence */ synthesizeLCARSSequence() { const ctx = this.am.ctx; if (!ctx) return; const notes = [ this.lcarsPitches[Math.floor(Math.random() * 4) + 4], this.lcarsPitches[Math.floor(Math.random() * 4) + 6], this.lcarsPitches[Math.floor(Math.random() * 4) + 2] ]; notes.forEach((freq, i) => { setTimeout(() => { this.synthesizeLCARSSingleChirp(freq); }, i * 75); }); } /** * High-tech Sensor Sweep Tone (Voyager/TNG Long-Range Sensor telemetry) */ synthesizeSensorSweep() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.38; const startFreq = 1200 + Math.random() * 800; const endFreq = startFreq * (Math.random() > 0.5 ? 1.6 : 0.65); const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(startFreq, now); osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.18, now + 0.05); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * TOS Original Series Bridge Electronic Computer Warble * Two detuned square/triangle oscillators modulated by high-speed vibrato LFO */ synthesizeTOSWarble() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.45; const baseFreq = this.tosFrequencies[Math.floor(Math.random() * this.tosFrequencies.length)]; const osc1 = ctx.createOscillator(); osc1.type = 'triangle'; osc1.frequency.setValueAtTime(baseFreq, now); const osc2 = ctx.createOscillator(); osc2.type = 'sawtooth'; osc2.frequency.setValueAtTime(baseFreq * 1.5, now); // Fast Vibrato LFO const lfo = ctx.createOscillator(); lfo.type = 'sine'; lfo.frequency.setValueAtTime(14 + Math.random() * 8, now); // 14-22 Hz warble const lfoGain = ctx.createGain(); lfoGain.gain.setValueAtTime(35, now); lfo.connect(lfoGain); lfoGain.connect(osc1.frequency); lfoGain.connect(osc2.frequency); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.22, now + 0.04); env.gain.setValueAtTime(0.22, now + duration * 0.7); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); // Bandpass filter to create that vintage analog 1960s telephone/relay resonance const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(baseFreq * 1.2, now); filter.Q.setValueAtTime(3.5, now); osc1.connect(env); osc2.connect(env); env.connect(filter); filter.connect(this.gainNode); osc1.start(now); osc2.start(now); lfo.start(now); osc1.stop(now + duration); osc2.stop(now + duration); lfo.stop(now + duration); } /** * TOS Mechanical Relay Solenoid Click */ synthesizeTOSRelayClick() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.025; const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(1400, now); osc.frequency.exponentialRampToValueAtTime(300, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.3, now); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * DS9 / Cardassian Cavernous Sensor Tone (Resonant metallic ring) */ synthesizeCardassianSensor() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.55; const freq = 420 + Math.random() * 200; const osc1 = ctx.createOscillator(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(freq, now); const osc2 = ctx.createOscillator(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(freq * 1.414, now); // Tritone metallic dissonance const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.2, now + 0.015); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc1.connect(env); osc2.connect(env); env.connect(this.gainNode); osc1.start(now); osc2.start(now); osc1.stop(now + duration); osc2.stop(now + duration); } /** * NX-01 Industrial Hydraulic Relay Click */ synthesizeNXRelay() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.04; const osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(750, now); osc.frequency.exponentialRampToValueAtTime(120, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.25, now); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * NX-01 Indicator Beep (Early 22nd century industrial tone) */ synthesizeNXIndicatorBeep() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.08; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(950, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.2, now + 0.005); env.gain.setValueAtTime(0.2, now + duration * 0.8); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Iconic TNG 2-Tone Door Chime ("Come in") */ synthesizeDoorChime() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const f1 = 880; // A5 const f2 = 1174.66; // D6 (Up a fourth) const osc1 = ctx.createOscillator(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(f1, now); const env1 = ctx.createGain(); env1.gain.setValueAtTime(0.001, now); env1.gain.linearRampToValueAtTime(0.35, now + 0.015); env1.gain.exponentialRampToValueAtTime(0.001, now + 0.45); osc1.connect(env1); env1.connect(this.gainNode); osc1.start(now); osc1.stop(now + 0.45); // Second tone starts at 0.16s const osc2 = ctx.createOscillator(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(f2, now + 0.16); const env2 = ctx.createGain(); env2.gain.setValueAtTime(0.001, now + 0.16); env2.gain.linearRampToValueAtTime(0.4, now + 0.175); env2.gain.exponentialRampToValueAtTime(0.001, now + 0.7); osc2.connect(env2); env2.connect(this.gainNode); osc2.start(now + 0.16); osc2.stop(now + 0.7); } applyPreset(config) { if (config.volume !== undefined) this.params.volume = config.volume; if (config.density !== undefined) this.setDensity(config.density); if (config.era !== undefined) this.params.era = config.era; this.setVolume(this.params.volume); this.startAutoTelemetryScheduler(); } } window.TelemetrySynth = TelemetrySynth; /** * Procedural Starship Alert & Event Synthesizer * 100% synthesized programmatically in Web Audio API. * Includes TNG Red Alert (3-tone), TOS Red Alert (hooter buzzer), Movie-era descending klaxon, * Yellow Alert chime, and dynamic Warp Drive Throttle swell. */ class AlertSynth { constructor(audioManager) { this.am = audioManager; this.gainNode = null; this.activeAlert = null; // 'red', 'yellow', null this.alertTimer = null; this.alertType = 'tng'; // 'tng', 'tos', 'movie' this.params = { volume: 0.6 }; } init() { if (this.gainNode || !this.am.ctx) return; const ctx = this.am.ctx; this.gainNode = ctx.createGain(); this.gainNode.gain.setValueAtTime(this.params.volume, ctx.currentTime); this.gainNode.connect(this.am.compressor); } setVolume(val) { this.params.volume = Math.max(0, Math.min(1, val)); if (this.gainNode && this.am.ctx) { const now = this.am.ctx.currentTime; this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05); } } triggerRedAlert(type = 'tng') { this.init(); this.stopAlert(); this.activeAlert = 'red'; this.alertType = type; const playLoop = () => { if (this.activeAlert !== 'red') return; let loopDuration = 1.35; if (this.alertType === 'tng') { this.synthesizeTNGRedAlertCycle(); loopDuration = 1.35; } else if (this.alertType === 'tos') { this.synthesizeTOSRedAlertCycle(); loopDuration = 1.1; } else { this.synthesizeMovieRedAlertCycle(); loopDuration = 1.4; } this.alertTimer = setTimeout(playLoop, loopDuration * 1000); }; playLoop(); } triggerYellowAlert() { this.init(); this.stopAlert(); this.activeAlert = 'yellow'; const playLoop = () => { if (this.activeAlert !== 'yellow') return; this.synthesizeYellowAlertCycle(); this.alertTimer = setTimeout(playLoop, 2200); }; playLoop(); } stopAlert() { this.activeAlert = null; if (this.alertTimer) { clearTimeout(this.alertTimer); this.alertTimer = null; } } /** * TNG Red Alert Klaxon (3-tone rising & cascading electronic horn with resonant envelope) */ synthesizeTNGRedAlertCycle() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Frequencies for the iconic TNG 3-tone klaxon chord: F5 (698.46Hz), Ab5 (830.61Hz), C6 (1046.50Hz) const freqs = [698.46, 830.61, 1046.50]; freqs.forEach((freq, idx) => { const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(freq * 0.94, now); // Fast upward swoop on trigger osc.frequency.exponentialRampToValueAtTime(freq, now + 0.12); // Lowpass filter to give that brassy starship horn acoustic resonance const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(1600, now); filter.Q.setValueAtTime(4.0, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.25 / freqs.length, now + 0.08); env.gain.setValueAtTime(0.25 / freqs.length, now + 0.45); env.gain.exponentialRampToValueAtTime(0.0001, now + 0.85); osc.connect(filter); filter.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + 0.86); }); } /** * TOS Red Alert Buzzer / Hooter Siren (Pulsing 2-tone frequency modulation) */ synthesizeTOSRedAlertCycle() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.85; const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(540, now); osc.frequency.linearRampToValueAtTime(920, now + duration * 0.5); osc.frequency.linearRampToValueAtTime(540, now + duration); const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(800, now); filter.Q.setValueAtTime(2.2, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.01, now); env.gain.linearRampToValueAtTime(0.28, now + 0.05); env.gain.setValueAtTime(0.28, now + duration * 0.85); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(filter); filter.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Star Trek Movie Era Refit Descending Red Alert Klaxon */ synthesizeMovieRedAlertCycle() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.05; const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(1350, now); osc.frequency.exponentialRampToValueAtTime(420, now + duration * 0.9); const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(2200, now); filter.Q.setValueAtTime(3.5, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.01, now); env.gain.linearRampToValueAtTime(0.3, now + 0.06); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(filter); filter.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Yellow Alert Pulsing Warning Chime */ synthesizeYellowAlertCycle() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const f1 = 660; // E5 const f2 = 880; // A5 [0, 0.22].forEach((offset, idx) => { const freq = idx === 0 ? f1 : f2; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now + offset); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now + offset); env.gain.linearRampToValueAtTime(0.32, now + offset + 0.015); env.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.5); osc.connect(env); env.connect(this.gainNode); osc.start(now + offset); osc.stop(now + offset + 0.52); }); } /** * Procedural Warp Drive Acceleration Swell ("Engage!") * Synthesizes rising plasma induction whine + deep bass detonation */ synthesizeWarpJump() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 2.8; // 1. Rising High Induction Whine const whineOsc = ctx.createOscillator(); whineOsc.type = 'sawtooth'; whineOsc.frequency.setValueAtTime(80, now); whineOsc.frequency.exponentialRampToValueAtTime(3800, now + 1.8); whineOsc.frequency.exponentialRampToValueAtTime(14000, now + 2.5); const whineFilter = ctx.createBiquadFilter(); whineFilter.type = 'bandpass'; whineFilter.frequency.setValueAtTime(200, now); whineFilter.frequency.exponentialRampToValueAtTime(4500, now + 1.8); whineFilter.Q.setValueAtTime(5.0, now); const whineEnv = ctx.createGain(); whineEnv.gain.setValueAtTime(0.01, now); whineEnv.gain.linearRampToValueAtTime(0.35, now + 1.6); whineEnv.gain.exponentialRampToValueAtTime(0.001, now + 2.7); whineOsc.connect(whineFilter); whineFilter.connect(whineEnv); whineEnv.connect(this.gainNode); // 2. Sub-bass Matter-Antimatter Boom const subOsc = ctx.createOscillator(); subOsc.type = 'sine'; subOsc.frequency.setValueAtTime(140, now + 1.4); subOsc.frequency.exponentialRampToValueAtTime(32, now + 2.6); const subEnv = ctx.createGain(); subEnv.gain.setValueAtTime(0.001, now + 1.4); subEnv.gain.linearRampToValueAtTime(0.7, now + 1.7); subEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); subOsc.connect(subEnv); subEnv.connect(this.gainNode); whineOsc.start(now); whineOsc.stop(now + 2.7); subOsc.start(now + 1.4); subOsc.stop(now + duration); } } window.AlertSynth = AlertSynth; /** * Procedural Doctor Who & TARDIS Sound Synthesizer * 100% synthesized programmatically via Web Audio API. * Includes Dematerialization Wheeze-Groan, Cloister Bell, Sonic Screwdriver, and TARDIS Console Foley. */ class WhoniverseAudioSynth { constructor(audioManager) { this.am = audioManager; this.gainNode = null; this.activeCloister = false; this.cloisterTimer = null; this.typeTelemetryTimer = null; } init() { if (this.gainNode || !this.am.ctx) return; const ctx = this.am.ctx; this.gainNode = ctx.createGain(); this.gainNode.gain.setValueAtTime(0.7, ctx.currentTime); this.gainNode.connect(this.am.compressor); } /** * Procedural TARDIS Materialization / Dematerialization ("Wheeze-Groan") * Modeled after Brian Hodgson's 1963 BBC Radiophonic technique: * Dragging keys on piano bass strings -> reverse playback -> slow tape speed -> feedback loop. */ synthesizeDematCycle(cycles = 4) { this.init(); const ctx = this.am.ctx; if (!ctx) return; for (let c = 0; c < cycles; c++) { const cycleStart = ctx.currentTime + c * 1.85; this.synthesizeSingleDematSwell(cycleStart, c, cycles); } } synthesizeSingleDematSwell(startTime, cycleIndex, totalCycles) { const ctx = this.am.ctx; const duration = 1.75; // Intensity fades slightly on later cycles const intensity = 1.0 - (cycleIndex / totalCycles) * 0.35; // 1. Friction Scrape Carrier (Sawtooth through resonant highpass/bandpass with frequency glide) const frictionOsc = ctx.createOscillator(); frictionOsc.type = 'sawtooth'; // Frequency glides up then groans down frictionOsc.frequency.setValueAtTime(120, startTime); frictionOsc.frequency.exponentialRampToValueAtTime(840, startTime + 0.65); frictionOsc.frequency.exponentialRampToValueAtTime(95, startTime + duration); // Filter modeling the piano soundboard metallic scraping resonance const frictionFilter = ctx.createBiquadFilter(); frictionFilter.type = 'bandpass'; frictionFilter.frequency.setValueAtTime(320, startTime); frictionFilter.frequency.exponentialRampToValueAtTime(1450, startTime + 0.65); frictionFilter.frequency.exponentialRampToValueAtTime(220, startTime + duration); frictionFilter.Q.setValueAtTime(4.5, startTime); const frictionGain = ctx.createGain(); frictionGain.gain.setValueAtTime(0.001, startTime); frictionGain.gain.linearRampToValueAtTime(0.4 * intensity, startTime + 0.45); frictionGain.gain.exponentialRampToValueAtTime(0.001, startTime + duration); // 2. Sub-Vortex Resonant Groan (FM synthesis for the deep cosmic groaning undertone) const groanCarrier = ctx.createOscillator(); groanCarrier.type = 'triangle'; groanCarrier.frequency.setValueAtTime(55, startTime); groanCarrier.frequency.linearRampToValueAtTime(138, startTime + 0.55); groanCarrier.frequency.exponentialRampToValueAtTime(48, startTime + duration); const groanMod = ctx.createOscillator(); groanMod.type = 'sine'; groanMod.frequency.setValueAtTime(28, startTime); // Phasing FM modulator groanMod.frequency.linearRampToValueAtTime(65, startTime + 0.6); const groanModGain = ctx.createGain(); groanModGain.gain.setValueAtTime(45, startTime); groanMod.connect(groanModGain); groanModGain.connect(groanCarrier.frequency); const groanGain = ctx.createGain(); groanGain.gain.setValueAtTime(0.001, startTime); groanGain.gain.linearRampToValueAtTime(0.6 * intensity, startTime + 0.5); groanGain.gain.exponentialRampToValueAtTime(0.001, startTime + duration); // 3. Phasing Flutter / Swell (Tape-flange simulation via slow LFO) const lfo = ctx.createOscillator(); lfo.type = 'sine'; lfo.frequency.setValueAtTime(5.5, startTime); // 5.5 Hz flanging flutter const lfoDepth = ctx.createGain(); lfoDepth.gain.setValueAtTime(0.25, startTime); lfo.connect(lfoDepth); lfoDepth.connect(frictionGain.gain); // Connect Graph frictionOsc.connect(frictionFilter); frictionFilter.connect(frictionGain); frictionGain.connect(this.gainNode); groanCarrier.connect(groanGain); groanGain.connect(this.gainNode); // Trigger Nodes frictionOsc.start(startTime); frictionOsc.stop(startTime + duration); groanCarrier.start(startTime); groanCarrier.stop(startTime + duration); groanMod.start(startTime); groanMod.stop(startTime + duration); lfo.start(startTime); lfo.stop(startTime + duration); } /** * Procedural Cloister Bell (Deep, ominous bronze cathedral bell) */ triggerCloisterBell() { this.init(); this.stopCloisterBell(); this.activeCloister = true; const ringLoop = () => { if (!this.activeCloister) return; this.synthesizeCloisterStrike(); this.cloisterTimer = setTimeout(ringLoop, 3200); // Canonical cloister bell repetition rate }; ringLoop(); } stopCloisterBell() { this.activeCloister = false; if (this.cloisterTimer) { clearTimeout(this.cloisterTimer); this.cloisterTimer = null; } } synthesizeCloisterStrike() { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 4.2; // Authentic bell inharmonic partial ratios: Fundamental, Minor 3rd, 5th, Octave, Major 7th const bellPartials = [ { freqRatio: 1.0, gain: 0.65, decay: 4.2 }, // Fundamental ~108 Hz { freqRatio: 1.19, gain: 0.45, decay: 3.6 }, // Minor third { freqRatio: 1.51, gain: 0.40, decay: 3.1 }, // Fifth { freqRatio: 2.01, gain: 0.30, decay: 2.4 }, // Octave { freqRatio: 2.74, gain: 0.22, decay: 1.8 }, // Upper strike tone { freqRatio: 3.42, gain: 0.15, decay: 1.2 } // High strike transient ]; const basePitch = 108.0; // Deep bronze bell pitch bellPartials.forEach(p => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(basePitch * p.freqRatio, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(p.gain * 0.35, now + 0.012); // Sharp hammer impact env.gain.exponentialRampToValueAtTime(0.0001, now + p.decay); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + p.decay + 0.05); }); } /** * Procedural Sonic Screwdriver (High-frequency modulated sweep & vibrato warble) */ synthesizeSonicScrewdriver(durationSeconds = 1.2) { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = durationSeconds; // Dual square/saw oscillators const osc1 = ctx.createOscillator(); osc1.type = 'square'; osc1.frequency.setValueAtTime(2350, now); osc1.frequency.linearRampToValueAtTime(2650, now + duration * 0.5); osc1.frequency.linearRampToValueAtTime(2350, now + duration); const osc2 = ctx.createOscillator(); osc2.type = 'sawtooth'; osc2.frequency.setValueAtTime(2362, now); // 12Hz natural phase beat // Rapid Vibrato LFO const vibrato = ctx.createOscillator(); vibrato.type = 'sine'; vibrato.frequency.setValueAtTime(32, now); // 32 Hz warble rate const vibGain = ctx.createGain(); vibGain.gain.setValueAtTime(140, now); vibrato.connect(vibGain); vibGain.connect(osc1.frequency); vibGain.connect(osc2.frequency); // Bandpass filter for metallic resonance const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(2500, now); filter.Q.setValueAtTime(4.0, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.28, now + 0.03); env.gain.setValueAtTime(0.28, now + duration * 0.85); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc1.connect(filter); osc2.connect(filter); filter.connect(env); env.connect(this.gainNode); osc1.start(now); osc2.start(now); vibrato.start(now); osc1.stop(now + duration); osc2.stop(now + duration); vibrato.stop(now + duration); } /** * Fast-Return Spring Lever (Heavy spring recoil clack + resonant ring) */ synthesizeFastReturn() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.35; const osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(620, now); osc.frequency.exponentialRampToValueAtTime(95, now + 0.08); const env = ctx.createGain(); env.gain.setValueAtTime(0.45, now); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * TARDIS Demat Switch / Relay Solenoid */ synthesizeDematSwitch() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.06; const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(850, now); osc.frequency.exponentialRampToValueAtTime(140, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.35, now); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Telepathic Circuit Chime (Glassy, mystical resonance) */ synthesizeTelepathicChime() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.4; const notes = [1046.50, 1318.51, 1567.98, 2093.00]; // C Major arpeggio shimmer notes.forEach((freq, idx) => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now + idx * 0.08); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now + idx * 0.08); env.gain.linearRampToValueAtTime(0.18, now + idx * 0.08 + 0.02); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now + idx * 0.08); osc.stop(now + duration + 0.05); }); } /** * TARDIS Lever-Throw Clunk (Tactile mechanical switch) * Hard square transient click coupled to a dull highpass noise thump */ synthesizeLeverClunk() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Hard square transient click const clickOsc = ctx.createOscillator(); clickOsc.type = 'square'; clickOsc.frequency.setValueAtTime(80, now); clickOsc.frequency.exponentialRampToValueAtTime(30, now + 0.035); const clickEnv = ctx.createGain(); clickEnv.gain.setValueAtTime(0.42, now); clickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.04); clickOsc.connect(clickEnv); clickEnv.connect(this.gainNode); clickOsc.start(now); clickOsc.stop(now + 0.042); // Dull highpass noise thump const noiseBuf = this.am.createNoiseBuffer('pink', 0.15); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const filter = ctx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.setValueAtTime(320, now); const noiseEnv = ctx.createGain(); noiseEnv.gain.setValueAtTime(0.3, now); noiseEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.06); noise.connect(filter); filter.connect(noiseEnv); noiseEnv.connect(this.gainNode); noise.start(now); noise.stop(now + 0.065); } /** * TARDIS Police Box Exterior Door Open / Close (Wood creak + mortise latch) * Modulated bandpass friction noise layered over a sharp metallic dual-click transient */ synthesizeTardisDoor() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.75; // 1. Modulated bandpass friction noise (wood creak) const creakBuf = this.am.createNoiseBuffer('brown', duration); const creak = ctx.createBufferSource(); creak.buffer = creakBuf; const creakFilter = ctx.createBiquadFilter(); creakFilter.type = 'bandpass'; creakFilter.frequency.setValueAtTime(260, now); creakFilter.frequency.linearRampToValueAtTime(540, now + 0.35); creakFilter.frequency.exponentialRampToValueAtTime(310, now + 0.65); creakFilter.Q.setValueAtTime(6.0, now); const creakEnv = ctx.createGain(); creakEnv.gain.setValueAtTime(0.001, now); creakEnv.gain.linearRampToValueAtTime(0.38, now + 0.15); creakEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.68); creak.connect(creakFilter); creakFilter.connect(creakEnv); creakEnv.connect(this.gainNode); creak.start(now); creak.stop(now + 0.7); // 2. Iron mortise latch dual-click transient [0.54, 0.62].forEach((offset, idx) => { const click = ctx.createOscillator(); click.type = 'triangle'; click.frequency.setValueAtTime(idx === 0 ? 1100 : 750, now + offset); click.frequency.exponentialRampToValueAtTime(180, now + offset + 0.03); const cEnv = ctx.createGain(); cEnv.gain.setValueAtTime(0.28, now + offset); cEnv.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.035); click.connect(cEnv); cEnv.connect(this.gainNode); click.start(now + offset); click.stop(now + offset + 0.04); }); } /** * Scanner / Monitor Screen Activation (Whoniverse, audio_gaps.md #24) * Rapid upward sine sweep (200 Hz -> 1600 Hz over 120 ms) terminating in a * CRT flyback whine. The 15.6 kHz spec tail is compromised to ~10.6 kHz: * near/above Nyquist on low-rate contexts and ear-fatiguing at audible levels. */ synthesizeScannerActivate() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // 1. Cathode-ray startup pitch sweep const sweep = ctx.createOscillator(); sweep.type = 'sine'; sweep.frequency.setValueAtTime(200, now); sweep.frequency.exponentialRampToValueAtTime(1600, now + 0.12); const sweepEnv = ctx.createGain(); sweepEnv.gain.setValueAtTime(0.001, now); sweepEnv.gain.linearRampToValueAtTime(0.32, now + 0.03); sweepEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.16); sweep.connect(sweepEnv); sweepEnv.connect(this.gainNode); sweep.start(now); sweep.stop(now + 0.17); // 2. CRT flyback whine partial (10.6 kHz compromise, low amplitude) const whine = ctx.createOscillator(); whine.type = 'sine'; whine.frequency.setValueAtTime(10600, now + 0.1); const wobble = ctx.createOscillator(); wobble.type = 'sine'; wobble.frequency.setValueAtTime(1.0, now + 0.1); const wobbleDepth = ctx.createGain(); wobbleDepth.gain.setValueAtTime(8, now + 0.1); wobble.connect(wobbleDepth); wobbleDepth.connect(whine.frequency); const whineEnv = ctx.createGain(); whineEnv.gain.setValueAtTime(0.001, now + 0.1); whineEnv.gain.linearRampToValueAtTime(0.03, now + 0.16); whineEnv.gain.exponentialRampToValueAtTime(0.0001, now + 0.85); whine.connect(whineEnv); whineEnv.connect(this.gainNode); whine.start(now + 0.1); whine.stop(now + 0.87); wobble.start(now + 0.1); wobble.stop(now + 0.87); } /** * Console "Type" Input Clatter Cluster (Whoniverse, audio_gaps.md #25) * Rhythmic mechanical clatter of toggle switches, spring-loaded buttons and * tumbler relays: clustered wooden/plastic clicks and solenoid snaps rather * than tonal beeps. Produces a single 0.3-0.6 s cluster of 3-7 transients. */ synthesizeTypeClatter() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const transientCount = 3 + Math.floor(Math.random() * 5); // 3-7 strikes let cursor = 0; const strike = (kind, offset, peak) => { const t = now + offset; if (kind === 'toggle') { // Wooden relay toggle: short bandpass pink-noise burst with downward snap const buf = this.am.createNoiseBuffer('pink', 0.1); const noise = ctx.createBufferSource(); noise.buffer = buf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(700 + Math.random() * 700, t); bp.frequency.exponentialRampToValueAtTime(240, t + 0.03); bp.Q.setValueAtTime(8 + Math.random() * 4, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(peak, t + 0.005); env.gain.exponentialRampToValueAtTime(0.001, t + 0.045); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(t); noise.stop(t + 0.05); } else if (kind === 'button') { // Spring-loaded plastic button: fast square click collapsing in pitch const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(900, t); osc.frequency.exponentialRampToValueAtTime(200, t + 0.012); const env = ctx.createGain(); env.gain.setValueAtTime(peak * 0.8, t); env.gain.exponentialRampToValueAtTime(0.001, t + 0.022); osc.connect(env); env.connect(this.gainNode); osc.start(t); osc.stop(t + 0.025); } else { // Heavy solenoid snap: dull low square thump const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(120, t); osc.frequency.exponentialRampToValueAtTime(55, t + 0.025); const env = ctx.createGain(); env.gain.setValueAtTime(peak, t); env.gain.exponentialRampToValueAtTime(0.001, t + 0.035); osc.connect(env); env.connect(this.gainNode); osc.start(t); osc.stop(t + 0.04); } }; for (let i = 0; i < transientCount; i++) { const kinds = ['toggle', 'toggle', 'button', 'button', 'solenoid']; const kind = kinds[Math.floor(Math.random() * kinds.length)]; cursor += 0.015 + Math.random() * 0.075; strike(kind, cursor, 0.07 + Math.random() * 0.07); } } /** * Ambient Loop: TARDIS Console "Type" Input Telemetry (#25) * Recursive random-window clatter while any TARDIS console room is engaged. */ startConsoleTypeTelemetry(minIntervalMs = 2800, maxIntervalMs = 6500) { this.stopConsoleTypeTelemetry(); const scheduleNext = () => { const delay = minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); this.typeTelemetryTimer = setTimeout(() => { this.synthesizeTypeClatter(); scheduleNext(); }, delay); }; scheduleNext(); } stopConsoleTypeTelemetry() { if (this.typeTelemetryTimer) { clearTimeout(this.typeTelemetryTimer); this.typeTelemetryTimer = null; } } /** * Stops all automated Whoniverse ambient loops. Cloister bell is an * interactive toggle and keeps its own explicit call sites. */ stopAmbientLoops() { this.stopConsoleTypeTelemetry(); } } window.WhoniverseAudioSynth = WhoniverseAudioSynth; /** * Procedural Audio Synthesizers for Expanded Sci-Fi Universes * Generates Epstein drives, Bio-ship neural pulses, DRADIS sonar, Singularity drives, * Retro analog tone glides, and Ludicrous speed reality shifts via Web Audio API. */ class ExpandedSciFiAudioSynth { constructor(audioManager) { this.am = audioManager; this.gainNode = null; this.medicalTimer = null; this.sparkTimer = null; this.loopTimers = {}; } init() { if (this.gainNode || !this.am.ctx) return; const ctx = this.am.ctx; this.gainNode = ctx.createGain(); this.gainNode.gain.setValueAtTime(0.7, ctx.currentTime); this.gainNode.connect(this.am.compressor); } /** * Epstein Drive Fusion Torch Burn (The Expanse / Industrial Space) * Tremendous raw fusion thrust with high-pressure magnetic plasma acceleration */ synthesizeEpsteinBurn() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 3.5; // 1. High-frequency plasma induction whine const whineOsc = ctx.createOscillator(); whineOsc.type = 'sawtooth'; whineOsc.frequency.setValueAtTime(140, now); whineOsc.frequency.exponentialRampToValueAtTime(1800, now + 1.2); whineOsc.frequency.exponentialRampToValueAtTime(3200, now + 2.5); const whineFilter = ctx.createBiquadFilter(); whineFilter.type = 'bandpass'; whineFilter.frequency.setValueAtTime(280, now); whineFilter.frequency.exponentialRampToValueAtTime(2600, now + 2.0); whineFilter.Q.setValueAtTime(4.0, now); const whineGain = ctx.createGain(); whineGain.gain.setValueAtTime(0.001, now); whineGain.gain.linearRampToValueAtTime(0.35, now + 1.0); whineGain.gain.exponentialRampToValueAtTime(0.001, now + duration); whineOsc.connect(whineFilter); whineFilter.connect(whineGain); whineGain.connect(this.gainNode); // 2. Colossal fusion blast roar (filtered noise) const roarBuffer = this.am.createNoiseBuffer('brown', 4); const roarSource = ctx.createBufferSource(); roarSource.buffer = roarBuffer; const roarFilter = ctx.createBiquadFilter(); roarFilter.type = 'lowpass'; roarFilter.frequency.setValueAtTime(180, now); roarFilter.frequency.linearRampToValueAtTime(550, now + 1.2); roarFilter.frequency.exponentialRampToValueAtTime(120, now + duration); const roarGain = ctx.createGain(); roarGain.gain.setValueAtTime(0.001, now); roarGain.gain.linearRampToValueAtTime(0.7, now + 1.2); roarGain.gain.exponentialRampToValueAtTime(0.001, now + duration); roarSource.connect(roarFilter); roarFilter.connect(roarGain); roarGain.connect(this.gainNode); whineOsc.start(now); whineOsc.stop(now + duration); roarSource.start(now); roarSource.stop(now + duration); } /** * Bio-Ship Starburst / Neural Pulse (Farscape Moya & Bioships) * Organic vocalized dimensional fold and vascular wave */ synthesizeStarburst() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 2.8; const osc1 = ctx.createOscillator(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(85, now); osc1.frequency.exponentialRampToValueAtTime(940, now + 1.4); osc1.frequency.exponentialRampToValueAtTime(45, now + duration); const osc2 = ctx.createOscillator(); osc2.type = 'triangle'; osc2.frequency.setValueAtTime(125, now); osc2.frequency.exponentialRampToValueAtTime(1420, now + 1.4); osc2.frequency.exponentialRampToValueAtTime(65, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.45, now + 1.3); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc1.connect(env); osc2.connect(env); env.connect(this.gainNode); osc1.start(now); osc2.start(now); osc1.stop(now + duration); osc2.stop(now + duration); } /** * Battlestar Galactica DRADIS Sonar Ping (Military Space) * The iconic tactical combat contact echo */ synthesizeDradisPing() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 1.4; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(1860, now); osc.frequency.exponentialRampToValueAtTime(1540, now + 0.08); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.35, now + 0.01); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * FTL Jump Thunderclap (BSG / Military Space) * Sudden vacuum displacement shockwave */ synthesizeFtlJump() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 2.2; const noiseBuffer = this.am.createNoiseBuffer('brown', 2.5); const noise = ctx.createBufferSource(); noise.buffer = noiseBuffer; const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(800, now); filter.frequency.exponentialRampToValueAtTime(45, now + 1.8); const env = ctx.createGain(); env.gain.setValueAtTime(0.8, now); env.gain.exponentialRampToValueAtTime(0.001, now + duration); noise.connect(filter); filter.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration); } /** * Retro Astrogator Tone Glide (Jupiter 2 / Retro Future) * 1960s Theremin / electronic oscillator glissando */ synthesizeRetroAstrogator() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 1.6; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(440, now); osc.frequency.linearRampToValueAtTime(1180, now + 0.6); osc.frequency.linearRampToValueAtTime(320, now + 1.1); osc.frequency.linearRampToValueAtTime(660, now + duration); const vibrato = ctx.createOscillator(); vibrato.type = 'sine'; vibrato.frequency.setValueAtTime(8, now); const vibGain = ctx.createGain(); vibGain.gain.setValueAtTime(25, now); vibrato.connect(vibGain); vibGain.connect(osc.frequency); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.3, now + 0.1); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); vibrato.start(now); osc.stop(now + duration); vibrato.stop(now + duration); } /** * HAL 9000 Logic Confirmation Chime (Discovery One) */ synthesizeHalChime() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const f1 = 784; // G5 const f2 = 523; // C5 [0, 0.14].forEach((offset, idx) => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(idx === 0 ? f1 : f2, now + offset); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now + offset); env.gain.linearRampToValueAtTime(0.25, now + offset + 0.01); env.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.5); osc.connect(env); env.connect(this.gainNode); osc.start(now + offset); osc.stop(now + offset + 0.52); }); } /** * Event Horizon Gravity Singularity Pulse (Deep Space) * Deep sub-bass dimensional warping thrum */ synthesizeSingularityEngage() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 3.2; const sub = ctx.createOscillator(); sub.type = 'sine'; sub.frequency.setValueAtTime(95, now); sub.frequency.exponentialRampToValueAtTime(28, now + 2.2); const mod = ctx.createOscillator(); mod.type = 'triangle'; mod.frequency.setValueAtTime(14, now); mod.frequency.linearRampToValueAtTime(45, now + 1.8); const modGain = ctx.createGain(); modGain.gain.setValueAtTime(60, now); mod.connect(modGain); modGain.connect(sub.frequency); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.65, now + 1.5); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); sub.connect(env); env.connect(this.gainNode); sub.start(now); mod.start(now); sub.stop(now + duration); mod.stop(now + duration); } /** * Outlaw Afterburner Thruster Surge (Cowboy Bebop / Milano) */ synthesizeAfterburner() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 2.4; const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(80, now); osc.frequency.exponentialRampToValueAtTime(850, now + 0.8); osc.frequency.linearRampToValueAtTime(620, now + duration); const filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(350, now); filter.frequency.exponentialRampToValueAtTime(1400, now + 0.8); filter.Q.setValueAtTime(3.0, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.5, now + 0.6); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(filter); filter.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Space Station Docking Clamp Latch & Airlock Purge */ synthesizeDockingClamp() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; // Heavy mechanical solenoid impact const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(380, now); osc.frequency.exponentialRampToValueAtTime(65, now + 0.12); const env = ctx.createGain(); env.gain.setValueAtTime(0.5, now); env.gain.exponentialRampToValueAtTime(0.001, now + 0.25); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + 0.25); // Followed by pneumatic seal hiss setTimeout(() => { if (!this.am.ctx) return; const t = this.am.ctx.currentTime; const hissBuf = this.am.createNoiseBuffer('white', 1.2); const hiss = this.am.ctx.createBufferSource(); hiss.buffer = hissBuf; const hFilter = this.am.ctx.createBiquadFilter(); hFilter.type = 'bandpass'; hFilter.frequency.setValueAtTime(2200, t); hFilter.Q.setValueAtTime(2.5, t); const hEnv = this.am.ctx.createGain(); hEnv.gain.setValueAtTime(0.001, t); hEnv.gain.linearRampToValueAtTime(0.25, t + 0.05); hEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.9); hiss.connect(hFilter); hFilter.connect(hEnv); hEnv.connect(this.gainNode); hiss.start(t); hiss.stop(t + 0.9); }, 180); } /** * Space Station Air Handler Thud (large HVAC unit cycling on) * Distinct from the docking clamp latch above: a dull low-frequency thump * (no bright metallic impact) followed by a slow-building airflow whoosh * rather than a short pneumatic hiss. A clamp is a single hard mechanical * event; an air handler is a big soft one that keeps breathing after it. */ synthesizeAirHandlerThud() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; // Dull low-frequency thump -- triangle, not square, and no bright impact // transient, so it reads as a heavy fan housing rather than a latch. const thump = ctx.createOscillator(); thump.type = 'triangle'; thump.frequency.setValueAtTime(95, now); thump.frequency.exponentialRampToValueAtTime(38, now + 0.22); const thumpEnv = ctx.createGain(); thumpEnv.gain.setValueAtTime(0.001, now); thumpEnv.gain.linearRampToValueAtTime(0.42, now + 0.02); thumpEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.4); // Sub-octave body for HVAC housing weight const sub = ctx.createOscillator(); sub.type = 'sine'; sub.frequency.setValueAtTime(46, now); const subEnv = ctx.createGain(); subEnv.gain.setValueAtTime(0.001, now); subEnv.gain.linearRampToValueAtTime(0.25, now + 0.03); subEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.45); thump.connect(thumpEnv); thumpEnv.connect(this.gainNode); sub.connect(subEnv); subEnv.connect(this.gainNode); thump.start(now); thump.stop(now + 0.4); sub.start(now); sub.stop(now + 0.45); // Slow-building airflow whoosh -- lowpass rather than the clamp's // bandpass, and a much slower attack, so it reads as a fan spinning up // rather than a sharp seal-release hiss. setTimeout(() => { if (!this.am.ctx) return; const t = this.am.ctx.currentTime; const airBuf = this.am.createNoiseBuffer('pink', 1.8); const air = this.am.ctx.createBufferSource(); air.buffer = airBuf; const airFilter = this.am.ctx.createBiquadFilter(); airFilter.type = 'lowpass'; airFilter.frequency.setValueAtTime(420, t); airFilter.Q.setValueAtTime(0.7, t); const airEnv = this.am.ctx.createGain(); airEnv.gain.setValueAtTime(0.001, t); airEnv.gain.linearRampToValueAtTime(0.22, t + 0.35); airEnv.gain.exponentialRampToValueAtTime(0.001, t + 1.6); air.connect(airFilter); airFilter.connect(airEnv); airEnv.connect(this.gainNode); air.start(t); air.stop(t + 1.6); }, 140); } /** * Ludicrous Speed Accelerator (Spaceball One / Comedy) */ synthesizeLudicrousSpeed() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 3.2; const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(60, now); osc.frequency.exponentialRampToValueAtTime(5400, now + 2.2); const env = ctx.createGain(); env.gain.setValueAtTime(0.01, now); env.gain.linearRampToValueAtTime(0.4, now + 1.8); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Infinite Improbability Reality-Warp Flip (Heart of Gold) */ synthesizeImprobabilityFlip() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 1.8; const osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(1400, now); osc.frequency.exponentialRampToValueAtTime(180, now + 0.7); osc.frequency.exponentialRampToValueAtTime(2200, now + 1.3); osc.frequency.exponentialRampToValueAtTime(440, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.35, now + 0.1); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Geiger Counter Click Burst (Nostromo / Mining) */ synthesizeGeigerBurst() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const clicks = 8 + Math.floor(Math.random() * 8); for (let i = 0; i < clicks; i++) { const clickTime = now + (i * 0.04) + (Math.random() * 0.03); const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(2800 + Math.random() * 800, clickTime); const env = ctx.createGain(); env.gain.setValueAtTime(0.18, clickTime); env.gain.exponentialRampToValueAtTime(0.001, clickTime + 0.015); osc.connect(env); env.connect(this.gainNode); osc.start(clickTime); osc.stop(clickTime + 0.016); } } /** * Cheerful Door Sigh (Heart of Gold / Sirius Cybernetics Corp) */ synthesizeCheerfulDoor() { this.init(); const ctx = this.am.ctx; if (!ctx) return; const now = ctx.currentTime; const duration = 0.9; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(620, now); osc.frequency.linearRampToValueAtTime(840, now + 0.35); osc.frequency.linearRampToValueAtTime(520, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.25, now + 0.15); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration); } /** * Star Trek Comm Badge Confirmation Chirp * Iconic bright two-tone pulse in rapid sequence (784 Hz to 1397 Hz) */ synthesizeCommBadge() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const tone1Freq = 784; // G5 const tone2Freq = 1396.91; // F6 // Pulse 1 const osc1 = ctx.createOscillator(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(tone1Freq, now); const env1 = ctx.createGain(); env1.gain.setValueAtTime(0.001, now); env1.gain.linearRampToValueAtTime(0.38, now + 0.005); env1.gain.exponentialRampToValueAtTime(0.001, now + 0.045); osc1.connect(env1); env1.connect(this.gainNode); osc1.start(now); osc1.stop(now + 0.048); // Pulse 2 const t2 = now + 0.038; const osc2 = ctx.createOscillator(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(tone2Freq, t2); const env2 = ctx.createGain(); env2.gain.setValueAtTime(0.001, t2); env2.gain.linearRampToValueAtTime(0.42, t2 + 0.006); env2.gain.exponentialRampToValueAtTime(0.001, t2 + 0.095); osc2.connect(env2); env2.connect(this.gainNode); osc2.start(t2); osc2.stop(t2 + 0.1); } /** * Starfleet Pneumatic Door Swish (Four Era Variants) * Bandpass-filtered white noise burst shaped per era */ synthesizeDoorSwish(era = 'tng') { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; let centerFreq = 950; let qVal = 1.8; let duration = 0.45; let noiseType = 'white'; switch (era) { case 'tos': centerFreq = 720; qVal = 1.4; duration = 0.38; break; case 'voyager': centerFreq = 1250; qVal = 2.4; duration = 0.40; break; case 'nx': centerFreq = 650; qVal = 2.0; duration = 0.55; noiseType = 'pink'; break; case 'tng': default: centerFreq = 950; qVal = 1.8; duration = 0.45; break; } const noiseBuf = this.am.createNoiseBuffer(noiseType, duration + 0.1); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(centerFreq * 0.8, now); bp.frequency.linearRampToValueAtTime(centerFreq * 1.15, now + duration * 0.4); bp.frequency.exponentialRampToValueAtTime(centerFreq * 0.7, now + duration); bp.Q.setValueAtTime(qVal, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.45, now + 0.03); env.gain.exponentialRampToValueAtTime(0.001, now + duration); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.02); } /** * Starfleet Bosun's Pipe Whistle (TOS Command Announce) * Two-tone sine glide (1800 Hz -> 2400 Hz -> 1850 Hz) with gentle tremolo */ synthesizeBosunWhistle() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.85; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(1800, now); osc.frequency.linearRampToValueAtTime(2400, now + 0.22); osc.frequency.setValueAtTime(2400, now + 0.48); osc.frequency.linearRampToValueAtTime(1850, now + 0.75); const tremolo = ctx.createOscillator(); tremolo.type = 'sine'; tremolo.frequency.setValueAtTime(6.5, now); const tremGain = ctx.createGain(); tremGain.gain.setValueAtTime(0.12, now); tremolo.connect(tremGain); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.28, now + 0.05); env.gain.setValueAtTime(0.28, now + 0.65); env.gain.exponentialRampToValueAtTime(0.001, now + duration); tremGain.connect(env.gain); osc.connect(env); env.connect(this.gainNode); osc.start(now); tremolo.start(now); osc.stop(now + duration); tremolo.stop(now + duration); } /** * Starfleet Sickbay Medical Monitor ECG Ping * Calm, periodic rhythmic vital signs pulse (1080 Hz sine, 40 ms decay) */ synthesizeMedicalMonitor() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.045; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(1080, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.32, now + 0.003); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration + 0.005); } /** * Sevastopol Electrical Conduit Spark Transient * High-voltage electrical discharge arcs across damaged station bulkheads */ synthesizeSevastopolSpark() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const bursts = 2 + Math.floor(Math.random() * 2); for (let i = 0; i < bursts; i++) { const offset = i * (0.015 + Math.random() * 0.02); const t = now + offset; const dur = 0.008 + Math.random() * 0.008; const noiseBuf = this.am.createNoiseBuffer('white', 0.05); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.setValueAtTime(3600 + Math.random() * 800, t); hp.Q.setValueAtTime(4.5, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.45, t); env.gain.exponentialRampToValueAtTime(0.001, t + dur); noise.connect(hp); hp.connect(env); env.connect(this.gainNode); noise.start(t); noise.stop(t + dur + 0.005); } } /** * Moonbase Alpha Commlock Calling Tone (Space: 1999) * Dual square wave pulse sequence (1100 Hz / 1500 Hz) */ synthesizeCommlockTone() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Beep 1: 1100 Hz, 60 ms const osc1 = ctx.createOscillator(); osc1.type = 'square'; osc1.frequency.setValueAtTime(1100, now); const env1 = ctx.createGain(); env1.gain.setValueAtTime(0.001, now); env1.gain.linearRampToValueAtTime(0.24, now + 0.004); env1.gain.setValueAtTime(0.24, now + 0.055); env1.gain.exponentialRampToValueAtTime(0.001, now + 0.062); osc1.connect(env1); env1.connect(this.gainNode); osc1.start(now); osc1.stop(now + 0.065); // Beep 2: 1500 Hz, 75 ms starting at +0.082s const t2 = now + 0.082; const osc2 = ctx.createOscillator(); osc2.type = 'square'; osc2.frequency.setValueAtTime(1500, t2); const env2 = ctx.createGain(); env2.gain.setValueAtTime(0.001, t2); env2.gain.linearRampToValueAtTime(0.24, t2 + 0.004); env2.gain.setValueAtTime(0.24, t2 + 0.07); env2.gain.exponentialRampToValueAtTime(0.001, t2 + 0.078); osc2.connect(env2); env2.connect(this.gainNode); osc2.start(t2); osc2.stop(t2 + 0.082); } /** * Gateway Station Medical Vital Telemetry Pip (Aliens) * Clinical sterile 950 Hz pure sine pip (35 ms decay) */ synthesizeStationMedicalPing() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.038; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(950, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.3, now + 0.003); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration + 0.005); } /** * Solar Radiation / Heat Shield Roar (Icarus II / Sunshine) * Terrifying lowpass brown noise roar with amplitude swell and crackle impulses */ synthesizeSolarRoar() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 3.6; // Lowpass brown noise body const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.2); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(120, now); lp.frequency.linearRampToValueAtTime(180, now + 1.5); lp.frequency.exponentialRampToValueAtTime(90, now + duration); lp.Q.setValueAtTime(2.2, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.72, now + 1.4); env.gain.setValueAtTime(0.70, now + 2.2); env.gain.exponentialRampToValueAtTime(0.001, now + duration); noise.connect(lp); lp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.05); // Highpass solar crackle layer const crackleBuf = this.am.createNoiseBuffer('pink', duration); const crackle = ctx.createBufferSource(); crackle.buffer = crackleBuf; const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.setValueAtTime(3200, now); hp.Q.setValueAtTime(4.0, now); const crackleEnv = ctx.createGain(); crackleEnv.gain.setValueAtTime(0.001, now); crackleEnv.gain.linearRampToValueAtTime(0.08, now + 1.2); crackleEnv.gain.exponentialRampToValueAtTime(0.001, now + duration); crackle.connect(hp); hp.connect(crackleEnv); crackleEnv.connect(this.gainNode); crackle.start(now); crackle.stop(now + duration); } /** * Cryogenic Pod Depressurization Sigh (Avalon / Ark One) * Gas pressure relief with exponential filter decay followed by 205 Hz seal hum */ synthesizeCryoDepressurize() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.35; // Pneumatic gas depressurization sigh const noiseBuf = this.am.createNoiseBuffer('white', 1.4); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(1800, now); lp.frequency.exponentialRampToValueAtTime(280, now + 1.1); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.38, now + 0.06); env.gain.exponentialRampToValueAtTime(0.001, now + 1.15); noise.connect(lp); lp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + 1.2); // Quiet motorized seal hum const hum = ctx.createOscillator(); hum.type = 'sine'; hum.frequency.setValueAtTime(205, now + 0.35); const humEnv = ctx.createGain(); humEnv.gain.setValueAtTime(0.001, now + 0.35); humEnv.gain.linearRampToValueAtTime(0.18, now + 0.55); humEnv.gain.exponentialRampToValueAtTime(0.001, now + duration); hum.connect(humEnv); humEnv.connect(this.gainNode); hum.start(now + 0.35); hum.stop(now + duration + 0.02); } /** * Mid-Century Orion Nuclear Pulse Thump (USS Ascension) * Heavy 45 Hz lowpass square impulse with sub-bass body resonance */ synthesizeOrionPulseThump() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.22; const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(45, now); osc.frequency.exponentialRampToValueAtTime(24, now + 0.09); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(140, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.65, now); env.gain.exponentialRampToValueAtTime(0.001, now + 0.085); osc.connect(lp); lp.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + 0.09); const sub = ctx.createOscillator(); sub.type = 'sine'; sub.frequency.setValueAtTime(36, now); const subEnv = ctx.createGain(); subEnv.gain.setValueAtTime(0.001, now); subEnv.gain.linearRampToValueAtTime(0.55, now + 0.01); subEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); sub.connect(subEnv); subEnv.connect(this.gainNode); sub.start(now); sub.stop(now + duration); } /** * Nutri-Matic Dedicated Tea Dispenser Gurgle (Heart of Gold) * Bandpass bubbling noise hops (500–1000 Hz) terminating in a short steam hiss */ synthesizeTeaDispenser() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Bubbling hops const bubbleFreqs = [520, 840, 610, 960, 480, 730]; bubbleFreqs.forEach((freq, i) => { const t = now + i * 0.11; const bDur = 0.09; const noiseBuf = this.am.createNoiseBuffer('pink', 0.12); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(freq, t); bp.Q.setValueAtTime(6.0, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(0.35, t + 0.015); env.gain.exponentialRampToValueAtTime(0.001, t + bDur); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(t); noise.stop(t + bDur + 0.01); }); // Steam hiss const steamTime = now + 0.65; const steamBuf = this.am.createNoiseBuffer('white', 0.45); const steam = ctx.createBufferSource(); steam.buffer = steamBuf; const steamFilter = ctx.createBiquadFilter(); steamFilter.type = 'bandpass'; steamFilter.frequency.setValueAtTime(2400, steamTime); steamFilter.Q.setValueAtTime(3.0, steamTime); const steamEnv = ctx.createGain(); steamEnv.gain.setValueAtTime(0.001, steamTime); steamEnv.gain.linearRampToValueAtTime(0.28, steamTime + 0.04); steamEnv.gain.exponentialRampToValueAtTime(0.001, steamTime + 0.42); steam.connect(steamFilter); steamFilter.connect(steamEnv); steamEnv.connect(this.gainNode); steam.start(steamTime); steam.stop(steamTime + 0.45); } /** * British Electric Kettle Steam Whistle (HMS Camden Lock / Hyperdrive) * Narrow bandpass sine sweeping 1820 Hz to 2380 Hz with steady tremolo */ synthesizeKettleWhistle() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.6; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(1820, now); osc.frequency.exponentialRampToValueAtTime(2380, now + duration * 0.7); osc.frequency.linearRampToValueAtTime(2320, now + duration); const tremolo = ctx.createOscillator(); tremolo.type = 'sine'; tremolo.frequency.setValueAtTime(5.2, now); const tremGain = ctx.createGain(); tremGain.gain.setValueAtTime(0.15, now); tremolo.connect(tremGain); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.26, now + 0.25); env.gain.setValueAtTime(0.26, now + 1.2); env.gain.exponentialRampToValueAtTime(0.001, now + duration); tremGain.connect(env.gain); osc.connect(env); env.connect(this.gainNode); osc.start(now); tremolo.start(now); osc.stop(now + duration); tremolo.stop(now + duration); } /** * Ludicrous Speed Plaid Alarm (Spaceball One) * Two-tone alternating square wave siren (440 Hz / 880 Hz) */ synthesizePlaidAlarm() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const steps = 6; const stepDur = 0.12; for (let i = 0; i < steps; i++) { const t = now + i * stepDur; const freq = (i % 2 === 0) ? 440 : 880; const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(freq, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(0.28, t + 0.006); env.gain.setValueAtTime(0.28, t + stepDur - 0.01); env.gain.exponentialRampToValueAtTime(0.001, t + stepDur); osc.connect(env); env.connect(this.gainNode); osc.start(t); osc.stop(t + stepDur + 0.005); } } /** * Dedicated Cassette Transport Clunk & Whirr (Milano / Bebop) * Dual square transient click (120 Hz / 360 Hz) followed by 2200 Hz capstan tone with flutter */ synthesizeCassetteTransport() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Dual square mechanical button clunk [120, 360].forEach((freq) => { const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(freq, now); osc.frequency.exponentialRampToValueAtTime(40, now + 0.028); const env = ctx.createGain(); env.gain.setValueAtTime(0.35, now); env.gain.exponentialRampToValueAtTime(0.001, now + 0.03); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + 0.032); }); // 12V capstan motor tone + tape flutter const motorTime = now + 0.04; const motorDur = 0.95; const motor = ctx.createOscillator(); motor.type = 'sine'; motor.frequency.setValueAtTime(2200, motorTime); const flutter = ctx.createOscillator(); flutter.type = 'sine'; flutter.frequency.setValueAtTime(8.2, motorTime); const flutGain = ctx.createGain(); flutGain.gain.setValueAtTime(35, motorTime); flutter.connect(flutGain); flutGain.connect(motor.frequency); const motorEnv = ctx.createGain(); motorEnv.gain.setValueAtTime(0.001, motorTime); motorEnv.gain.linearRampToValueAtTime(0.12, motorTime + 0.08); motorEnv.gain.exponentialRampToValueAtTime(0.001, motorTime + motorDur); motor.connect(motorEnv); motorEnv.connect(this.gainNode); motor.start(motorTime); flutter.start(motorTime); motor.stop(motorTime + motorDur); flutter.stop(motorTime + motorDur); // Tape noise bed const tapeBuf = this.am.createNoiseBuffer('pink', motorDur); const tape = ctx.createBufferSource(); tape.buffer = tapeBuf; const tapeFilter = ctx.createBiquadFilter(); tapeFilter.type = 'bandpass'; tapeFilter.frequency.setValueAtTime(3200, motorTime); tapeFilter.Q.setValueAtTime(2.0, motorTime); const tapeEnv = ctx.createGain(); tapeEnv.gain.setValueAtTime(0.001, motorTime); tapeEnv.gain.linearRampToValueAtTime(0.08, motorTime + 0.05); tapeEnv.gain.exponentialRampToValueAtTime(0.001, motorTime + motorDur); tape.connect(tapeFilter); tapeFilter.connect(tapeEnv); tapeEnv.connect(this.gainNode); tape.start(motorTime); tape.stop(motorTime + motorDur); } /** * Grappler Arm Servo Motor Whine (Outlaw Star / Bebop) * Swept triangle wave (220 Hz -> 680 Hz) through resonant bandpass filter */ synthesizeGrapplerServo() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.44; const osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(220, now); osc.frequency.exponentialRampToValueAtTime(680, now + duration * 0.7); osc.frequency.linearRampToValueAtTime(540, now + duration); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(320, now); bp.frequency.exponentialRampToValueAtTime(820, now + duration * 0.7); bp.Q.setValueAtTime(3.2, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.38, now + 0.04); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(bp); bp.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration + 0.01); } /** * True RF Radio Static Burst (The Betty / The Marauder) * Bandpass-filtered pink/white noise burst (300–3200 Hz) with hard-knee square gate */ synthesizeRadioStatic() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 0.18; const noiseBuf = this.am.createNoiseBuffer('pink', 0.25); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(1400, now); bp.Q.setValueAtTime(1.1, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.42, now); env.gain.setValueAtTime(0.40, now + duration - 0.02); env.gain.exponentialRampToValueAtTime(0.001, now + duration); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.01); } /** * Magnetic Boot ("Mag-Boot") Latch (Ceres / Tycho / The Expanse) * Low-frequency impact thump (80 Hz click) followed by 120 Hz inductive clamp buzz */ synthesizeMagBootLatch() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Stage 1: Impact thump const thump = ctx.createOscillator(); thump.type = 'triangle'; thump.frequency.setValueAtTime(80, now); thump.frequency.exponentialRampToValueAtTime(32, now + 0.03); const thumpEnv = ctx.createGain(); thumpEnv.gain.setValueAtTime(0.5, now); thumpEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.035); thump.connect(thumpEnv); thumpEnv.connect(this.gainNode); thump.start(now); thump.stop(now + 0.038); // Stage 2: 120 Hz inductive clamping buzz const buzzTime = now + 0.025; const buzz = ctx.createOscillator(); buzz.type = 'square'; buzz.frequency.setValueAtTime(120, buzzTime); const buzzFilter = ctx.createBiquadFilter(); buzzFilter.type = 'lowpass'; buzzFilter.frequency.setValueAtTime(420, buzzTime); const buzzEnv = ctx.createGain(); buzzEnv.gain.setValueAtTime(0.001, buzzTime); buzzEnv.gain.linearRampToValueAtTime(0.32, buzzTime + 0.01); buzzEnv.gain.exponentialRampToValueAtTime(0.001, buzzTime + 0.16); buzz.connect(buzzFilter); buzzFilter.connect(buzzEnv); buzzEnv.connect(this.gainNode); buzz.start(buzzTime); buzz.stop(buzzTime + 0.17); } /** * Background Loop: Periodic Medical Vital Signs Monitor (ECG) */ startMedicalMonitor(intervalSeconds = 1.1) { this.stopMedicalMonitor(); const tick = () => { this.synthesizeMedicalMonitor(); this.medicalTimer = setTimeout(tick, intervalSeconds * 1000); }; tick(); } stopMedicalMonitor() { if (this.medicalTimer) { clearTimeout(this.medicalTimer); this.medicalTimer = null; } } /** * Background Loop: Random Electrical Sparks on Damaged Stations */ startStationSparks(minIntervalMs = 4000, maxIntervalMs = 12000) { this.stopStationSparks(); const scheduleNext = () => { const delay = minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); this.sparkTimer = setTimeout(() => { this.synthesizeSevastopolSpark(); scheduleNext(); }, delay); }; scheduleNext(); } stopStationSparks() { if (this.sparkTimer) { clearTimeout(this.sparkTimer); this.sparkTimer = null; } } /** * Ambient Loop Registry (Phase 2) * Generic recursive-timeout scheduler keyed by name. A start call always * kills any previous instance of the same key first. If min === max the * cadence is fixed; otherwise it is random within [min, max]. */ _scheduleLoop(key, minIntervalMs, maxIntervalMs, fn) { this._stopLoop(key); const scheduleNext = () => { const delay = maxIntervalMs === minIntervalMs ? minIntervalMs : minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); this.loopTimers[key] = setTimeout(() => { fn(); scheduleNext(); }, delay); }; scheduleNext(); } _stopLoop(key) { if (this.loopTimers[key]) { clearTimeout(this.loopTimers[key]); delete this.loopTimers[key]; } } /** * Cardassian Bulkhead Door Grind (Star Trek DS9, audio_gaps.md #21) * Oppressive pneumatic bulkhead grinding open: resonant bandpass sweep * across brown noise (120 Hz -> 650 Hz) layered with a scraping metallic * saw undertone. */ synthesizeCardassianDoor() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.4; const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.2); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(120, now); bp.frequency.exponentialRampToValueAtTime(650, now + 1.1); bp.Q.setValueAtTime(3.5, now); // Scraping metallic saw undertone through the same sweeping bandpass const saw = ctx.createOscillator(); saw.type = 'sawtooth'; saw.frequency.setValueAtTime(110, now); saw.frequency.exponentialRampToValueAtTime(70, now + duration); const sawGain = ctx.createGain(); sawGain.gain.setValueAtTime(0.001, now); sawGain.gain.linearRampToValueAtTime(0.09, now + 0.6); sawGain.gain.exponentialRampToValueAtTime(0.001, now + duration); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.5, now + 0.4); env.gain.exponentialRampToValueAtTime(0.001, now + duration); noise.connect(bp); saw.connect(sawGain); sawGain.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.05); saw.start(now); saw.stop(now + duration + 0.05); } /** * Turbolift Pass Whoosh (Star Trek TNG/VOY, audio_gaps.md #22) * Bandpass-filtered white noise pitch sweep (180 -> 450 Hz and back) with * resonant boost and soft stereo panning drift past the listener. */ synthesizeTurboliftWhoosh() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.6; const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(180, now); bp.frequency.exponentialRampToValueAtTime(450, now + 0.55); bp.frequency.exponentialRampToValueAtTime(180, now + 1.4); bp.Q.setValueAtTime(2.2, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.42, now + 0.5); env.gain.linearRampToValueAtTime(0.3, now + 0.8); env.gain.exponentialRampToValueAtTime(0.001, now + duration); let panOut; if (ctx.createStereoPanner) { const panner = ctx.createStereoPanner(); panner.pan.setValueAtTime(-0.45, now); panner.pan.linearRampToValueAtTime(0.45, now + duration); panOut = panner; } if (panOut) { env.connect(panOut); panOut.connect(this.gainNode); } else { env.connect(this.gainNode); } noise.connect(bp); bp.connect(env); noise.start(now); noise.stop(now + duration + 0.02); } /** * Replicator Materialization Shimmer (Star Trek TNG/VOY, audio_gaps.md #23) * High-frequency white-noise shimmer (2.5-8 kHz) amplitude-modulated by a * fast 30 Hz sine LFO with a soft ramp decay. */ synthesizeReplicatorShimmer() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 2.0; const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const masterEnv = ctx.createGain(); masterEnv.gain.setValueAtTime(0.001, now); masterEnv.gain.linearRampToValueAtTime(0.3, now + 0.5); masterEnv.gain.exponentialRampToValueAtTime(0.001, now + duration); masterEnv.connect(this.gainNode); // Two parallel shimmer bands (2.5-4 kHz and 5-8 kHz) const bands = [ { center: 3200, q: 1.3, depth: 0.22 }, { center: 6400, q: 1.2, depth: 0.16 } ]; bands.forEach((band) => { const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(band.center, now); bp.Q.setValueAtTime(band.q, now); const amEnv = ctx.createGain(); amEnv.gain.setValueAtTime(band.depth, now); // 30 Hz shimmer AM const lfo = ctx.createOscillator(); lfo.type = 'sine'; lfo.frequency.setValueAtTime(30, now); const lfoDepth = ctx.createGain(); lfoDepth.gain.setValueAtTime(band.depth * 0.9, now); lfo.connect(lfoDepth); lfoDepth.connect(amEnv.gain); noise.connect(bp); bp.connect(amEnv); amEnv.connect(masterEnv); lfo.start(now); lfo.stop(now + duration + 0.05); }); noise.start(now); noise.stop(now + duration + 0.02); } /** * Vorlon Crystal "Singing" Resonance (Bioships, audio_gaps.md #26) * Ethereal telepathic crystal harmonics: dual detuned sines (528/532 Hz) * through a narrow bandpass with a slow 0.2 Hz undulating tremolo. * One 8.5 s swell phrasing. */ synthesizeVorlonSingingSwell() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 8.5; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(530, now); bp.Q.setValueAtTime(22, now); const tremolo = ctx.createGain(); tremolo.gain.setValueAtTime(0.5, now); const lfo = ctx.createOscillator(); lfo.type = 'sine'; lfo.frequency.setValueAtTime(0.2, now); const lfoDepth = ctx.createGain(); lfoDepth.gain.setValueAtTime(0.24, now); lfo.connect(lfoDepth); lfoDepth.connect(tremolo.gain); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.16, now + 3.0); env.gain.linearRampToValueAtTime(0.13, now + 5.5); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); env.connect(this.gainNode); [528, 532].forEach((freq, idx) => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now); if (idx === 1) osc.detune.setValueAtTime(3, now); osc.connect(bp); osc.start(now); osc.stop(now + duration + 0.05); }); bp.connect(tremolo); tremolo.connect(env); lfo.start(now); lfo.stop(now + duration + 0.05); } /** * Ambient Loop: Vorlon singing resonance while the Vorlon cruiser is engaged */ startVorlonSong(minIntervalMs = 11000, maxIntervalMs = 18000) { this._scheduleLoop('vorlonSong', minIntervalMs, maxIntervalMs, () => { this.synthesizeVorlonSingingSwell(); }); } /** * Neural-Bond Swell (Bioships, audio_gaps.md #27) * Symbiotic neural link between pilot and bioship swelling with emotion: * deep 55 Hz triangle sweeping into a resonant vowel-formant filter * (350-850 Hz) over 2.5 seconds. */ synthesizeNeuralBondSwell() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 3.4; // Sub-bass carrier const osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(55, now); osc.frequency.linearRampToValueAtTime(52, now + duration); // Formant sweep adding vowel-like harmonics const formant = ctx.createBiquadFilter(); formant.type = 'bandpass'; formant.frequency.setValueAtTime(350, now); formant.frequency.linearRampToValueAtTime(850, now + 2.5); formant.Q.setValueAtTime(4.5, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.34, now + 0.9); env.gain.linearRampToValueAtTime(0.42, now + 1.9); env.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(formant); formant.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration + 0.05); } /** * Hull Self-Repair / Regeneration Texture (Bioships, audio_gaps.md #28) * Microscopic organic tissue knit and chitin regrowth: granular * amplitude-modulated pink noise (8-24 Hz modulation) with wet low * regenerative pops. One ~1.8 s regeneration burst. */ synthesizeHullRegenBurst() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.8; const noiseBuf = this.am.createNoiseBuffer('pink', duration + 0.1); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(650, now); bp.frequency.linearRampToValueAtTime(1600, now + duration); bp.Q.setValueAtTime(2.0, now); const modEnv = ctx.createGain(); modEnv.gain.setValueAtTime(0.16, now); // Rapid scabbing modulation (~13 Hz) over the wet tissue bed const lfo = ctx.createOscillator(); lfo.type = 'square'; lfo.frequency.setValueAtTime(9 + Math.random() * 8, now); const lfoDepth = ctx.createGain(); lfoDepth.gain.setValueAtTime(0.12, now); lfo.connect(lfoDepth); lfoDepth.connect(modEnv.gain); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.2, now + 0.25); env.gain.exponentialRampToValueAtTime(0.001, now + duration); noise.connect(bp); bp.connect(modEnv); modEnv.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.05); lfo.start(now); lfo.stop(now + duration + 0.05); // Wet bioplasmic pops (chitin knit) const popCount = 2 + Math.floor(Math.random() * 3); for (let i = 0; i < popCount; i++) { const popTime = now + 0.25 + Math.random() * (duration - 0.6); const pop = ctx.createOscillator(); pop.type = 'sine'; pop.frequency.setValueAtTime(110 + Math.random() * 60, popTime); pop.frequency.exponentialRampToValueAtTime(60, popTime + 0.06); const popEnv = ctx.createGain(); popEnv.gain.setValueAtTime(0.001, popTime); popEnv.gain.linearRampToValueAtTime(0.14, popTime + 0.008); popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.07); pop.connect(popEnv); popEnv.connect(this.gainNode); pop.start(popTime); pop.stop(popTime + 0.08); } } /** * Ambient Loop: Periodic hull regeneration texture on Wraith hives */ startHullRegen(minIntervalMs = 6000, maxIntervalMs = 13000) { this._scheduleLoop('hullRegen', minIntervalMs, maxIntervalMs, () => { this.synthesizeHullRegenBurst(); }); } /** * Belter Jury-Rigged Telemetry Jitter (Space Stations, audio_gaps.md #29) * Rattle of loose relays, worn copper contactors and erratic voltage drops: * noisy square relay pulses with micro-dropouts, contact-bounce double * clicks and harsh metallic ticks on a jittered clock. One ~1.1 s cluster. */ synthesizeBelterTelemetryJitter() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const eventCount = 3 + Math.floor(Math.random() * 5); // 3-7 events let cursor = now + 0.05 + Math.random() * 0.2; const relayPulse = (t) => { const osc = ctx.createOscillator(); osc.type = 'square'; const baseFreq = 60 + Math.random() * 60; osc.frequency.setValueAtTime(baseFreq, t); osc.frequency.linearRampToValueAtTime(baseFreq * 0.8, t + 0.06); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(420, t); const env = ctx.createGain(); // Erratic voltage: randomized micro-dropouts in the pulse body env.gain.setValueAtTime(0.17, t); env.gain.setValueAtTime(0.001, t + 0.012 + Math.random() * 0.015); env.gain.setValueAtTime(0.14, t + 0.026 + Math.random() * 0.02); env.gain.setValueAtTime(0.001, t + 0.05 + Math.random() * 0.025); env.gain.setValueAtTime(0.1, t + 0.07); env.gain.exponentialRampToValueAtTime(0.001, t + 0.1); osc.connect(lp); lp.connect(env); env.connect(this.gainNode); osc.start(t); osc.stop(t + 0.11); }; const contactBounce = (t) => { // Worn contactor: double/triple click 1-3 ms apart const clicks = 2 + (Math.random() < 0.35 ? 1 : 0); for (let i = 0; i < clicks; i++) { const ct = t + i * (0.001 + Math.random() * 0.002); const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(500 + Math.random() * 500, ct); osc.frequency.exponentialRampToValueAtTime(120, ct + 0.008); const env = ctx.createGain(); env.gain.setValueAtTime(0.14, ct); env.gain.exponentialRampToValueAtTime(0.001, ct + 0.012); osc.connect(env); env.connect(this.gainNode); osc.start(ct); osc.stop(ct + 0.015); } }; const metallicTick = (t) => { const buf = this.am.createNoiseBuffer('white', 0.05); const noise = ctx.createBufferSource(); noise.buffer = buf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(400 + Math.random() * 1400, t); bp.Q.setValueAtTime(6, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.09, t); env.gain.exponentialRampToValueAtTime(0.001, t + 0.02); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(t); noise.stop(t + 0.025); }; for (let i = 0; i < eventCount; i++) { const kind = Math.random(); if (kind < 0.4) relayPulse(cursor); else if (kind < 0.75) contactBounce(cursor); else metallicTick(cursor); cursor += 0.08 + Math.random() * 0.22; // jittered clock drift } } /** * Ambient Loop: Intermittent jury-rigged telemetry on Belter stations */ startBelterTelemetry(minIntervalMs = 1800, maxIntervalMs = 4500) { this._scheduleLoop('belterTelemetry', minIntervalMs, maxIntervalMs, () => { this.synthesizeBelterTelemetryJitter(); }); } /** * True Hull Strain Moan (Deep Space, audio_gaps.md #30) * Massive structural groan of stressed bulkheads flexing under gravitational * shear: resonant bandpass slowly sweeping 40-220 Hz (high Q) over shaped * brown noise with an asymmetric attack/decay. ~5.5 s. */ synthesizeHullStrainMoan() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 5.5; const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.3); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(48, now); bp.frequency.exponentialRampToValueAtTime(210, now + 3.4); bp.Q.setValueAtTime(10, now); // Asymmetric envelope: slow menacing rise, long creaking decay const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.3, now + 0.9); env.gain.linearRampToValueAtTime(0.22, now + 1.7); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.05); } /** * Ambient Loop: Sporadic structural strain moans on haunted deep-space hulls */ startHullStrainMoans(minIntervalMs = 16000, maxIntervalMs = 40000) { this._scheduleLoop('hullStrainMoans', minIntervalMs, maxIntervalMs, () => { this.synthesizeHullStrainMoan(); }); } /** * Icarus I Distress Beacon (Deep Space, audio_gaps.md #31) * Eerie hypnotic modal arpeggio echoing from the ghost ship: a 4-note * modal sine sequence through a long feedback delay with soft lowpass * dampening. ~7 s including delay tail. */ synthesizeIcarusBeacon() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const notes = [220.0, 261.63, 329.63, 440.0]; // A minor modal beacon const delay = ctx.createDelay(1.2); delay.delayTime.setValueAtTime(0.45, now); const feedback = ctx.createGain(); feedback.gain.setValueAtTime(0.35, now); const dampen = ctx.createBiquadFilter(); dampen.type = 'lowpass'; dampen.frequency.setValueAtTime(2200, now); const wet = ctx.createGain(); wet.gain.setValueAtTime(0.5, now); delay.connect(feedback); feedback.connect(dampen); dampen.connect(delay); delay.connect(wet); wet.connect(this.gainNode); notes.forEach((freq, idx) => { const t = now + idx * 0.95; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(0.17, t + 0.05); env.gain.exponentialRampToValueAtTime(0.001, t + 0.85); osc.connect(env); env.connect(this.gainNode); env.connect(delay); osc.start(t); osc.stop(t + 0.9); }); } /** * Ambient Loop: Periodic distress beacon arpeggio while Icarus II is engaged */ startIcarusBeacon(minIntervalMs = 22000, maxIntervalMs = 40000) { this._scheduleLoop('icarusBeacon', minIntervalMs, maxIntervalMs, () => { this.synthesizeIcarusBeacon(); }); } /** * Extradimensional Psychic Static Whisper (Deep Space, audio_gaps.md #32) * Menacing psychoacoustic burst: multi-formant filtered white noise with * randomized micro-envelopes evoking whispered vowels (800-2400 Hz). */ synthesizeVoidWhisper() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 2.2; const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.1); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.12, now + 0.4); env.gain.linearRampToValueAtTime(0.09, now + 1.3); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); env.connect(this.gainNode); // Three parallel whispered-vowel formants with independent wobbles const formants = [ { center: 700, q: 7, wobbleHz: 0.8, wobbleAmt: 160 }, { center: 1250, q: 8, wobbleHz: 1.1, wobbleAmt: 280 }, { center: 2200, q: 10, wobbleHz: 1.6, wobbleAmt: 420 } ]; formants.forEach((formant) => { const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(formant.center, now); bp.Q.setValueAtTime(formant.q, now); const wobble = ctx.createOscillator(); wobble.type = 'sine'; wobble.frequency.setValueAtTime(formant.wobbleHz, now); const wobbleDepth = ctx.createGain(); wobbleDepth.gain.setValueAtTime(formant.wobbleAmt, now); wobble.connect(wobbleDepth); wobbleDepth.connect(bp.frequency); const breath = ctx.createGain(); breath.gain.setValueAtTime(0.3, now); // Randomized micro-envelopes: syllable-like gulps of the formant const gulpCount = 3 + Math.floor(Math.random() * 3); for (let i = 0; i < gulpCount; i++) { const gt = now + 0.15 + Math.random() * (duration - 0.55); breath.gain.setValueAtTime(0.12, gt); breath.gain.linearRampToValueAtTime(0.3 + Math.random() * 0.18, gt + 0.04); breath.gain.linearRampToValueAtTime(0.05, gt + 0.12 + Math.random() * 0.1); } noise.connect(bp); bp.connect(breath); breath.connect(env); wobble.start(now); wobble.stop(now + duration + 0.05); }); noise.start(now); noise.stop(now + duration + 0.02); } /** * Ambient Loop: Psychic whisper bursts on haunted deep-space hulls */ startVoidWhispers(minIntervalMs = 15000, maxIntervalMs = 32000) { this._scheduleLoop('voidWhispers', minIntervalMs, maxIntervalMs, () => { this.synthesizeVoidWhisper(); }); } /** * Beryllium Sphere Resonant Thrum (Comedy, audio_gaps.md #33) * Deep glassy crystalline reactor hum: dual pure sines (65 + 195 Hz third * harmonic) with slow beating detune and a subtle comb-filter ring. * One ~10 s swell phrasing. */ synthesizeBerylliumThrumSwell() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 10.0; const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.16, now + 4.0); env.gain.linearRampToValueAtTime(0.14, now + 6.5); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); env.connect(this.gainNode); // Comb ring: short 5.1 ms reflection const comb = ctx.createDelay(0.05); comb.delayTime.setValueAtTime(0.0051, now); const combFb = ctx.createGain(); combFb.gain.setValueAtTime(0.45, now); comb.connect(combFb); combFb.connect(comb); const combWet = ctx.createGain(); combWet.gain.setValueAtTime(0.6, now); comb.connect(combWet); combWet.connect(env); const partials = [ { freq: 65, amp: 0.55 }, { freq: 65.3, amp: 0.3 }, // slow 0.3 Hz beating partner { freq: 195, amp: 0.3 } ]; partials.forEach((partial) => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(partial.freq, now); const g = ctx.createGain(); g.gain.setValueAtTime(partial.amp, now); osc.connect(g); g.connect(env); g.connect(comb); osc.start(now); osc.stop(now + duration + 0.05); }); } /** * Ambient Loop: Beryllium sphere thrum while the NSEA Protector is engaged */ startBerylliumThrum(minIntervalMs = 9000, maxIntervalMs = 16000) { this._scheduleLoop('berylliumThrum', minIntervalMs, maxIntervalMs, () => { this.synthesizeBerylliumThrumSwell(); }); } /** * Omega-13 Temporal Capacitor Whine (Comedy, audio_gaps.md #34) * Tremendous temporal capacitor power buildup: deep 40 Hz sub-bass swelling * exponentially to 3200 Hz over 3 seconds, culminating in a wide * white-noise discharge pop. */ synthesizeOmega13Whine() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 3.1; const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(40, now); osc.frequency.exponentialRampToValueAtTime(3200, now + 3.0); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.exponentialRampToValueAtTime(0.5, now + 2.7); env.gain.setValueAtTime(0.5, now + 2.95); env.gain.exponentialRampToValueAtTime(0.001, now + 3.2); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + duration + 0.1); // Dimensional discharge pop (wide white-noise transient) const popTime = now + 3.0; const popBuf = this.am.createNoiseBuffer('white', 0.3); const pop = ctx.createBufferSource(); pop.buffer = popBuf; const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.setValueAtTime(2000, popTime); const popEnv = ctx.createGain(); popEnv.gain.setValueAtTime(0.001, popTime); popEnv.gain.linearRampToValueAtTime(0.42, popTime + 0.008); popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.16); pop.connect(hp); hp.connect(popEnv); popEnv.connect(this.gainNode); pop.start(popTime); pop.stop(popTime + 0.18); } /** * Repulsorlift Engine Drone (Outlaw, audio_gaps.md #35) * Quintessential anti-gravity vehicle wash: dual detuned sines (68/72 Hz) * through an asymmetric overdrive waveshaper, lowpass-filtered at 280 Hz. * One ~10 s wash phrasing. */ synthesizeRepulsorliftDrone() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 10.0; let shaperOut = null; if (ctx.createWaveShaper) { const shaper = ctx.createWaveShaper(); shaper.oversample = '2x'; const curve = new Float32Array(1024); for (let i = 0; i < 1024; i++) { const x = (i / 512) - 1; // -1..1 curve[i] = Math.tanh(2.4 * x); // asymmetric-ish saturation } shaper.curve = curve; shaperOut = shaper; } const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(280, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.2, now + 2.6); env.gain.linearRampToValueAtTime(0.17, now + 7.2); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); if (shaperOut) shaperOut.connect(lp); lp.connect(env); env.connect(this.gainNode); [68, 72].forEach((freq) => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now); osc.connect(shaperOut || lp); osc.start(now); osc.stop(now + duration + 0.05); }); } /** * Ambient Loop: Repulsorlift wash while the Marauder is engaged */ startRepulsorliftDrone(intervalMs = 13000) { this._scheduleLoop('repulsorliftDrone', intervalMs, intervalMs, () => { this.synthesizeRepulsorliftDrone(); }); } /** * Converted Marine Trawler Engine Chug (Outlaw, audio_gaps.md #36) * Rhythmic heavy diesel-like piston strokes: low-frequency square pulses * (4-8 Hz cadence) through an 80 Hz resonant lowpass with mechanical * piston wheeze. One cluster of 2-4 strokes. */ synthesizeBebopChugStroke() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const strokes = 2 + Math.floor(Math.random() * 3); // 2-4 strokes const stroke = (t, peak) => { const osc = ctx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(45 + Math.random() * 25, t); osc.frequency.linearRampToValueAtTime(38, t + 0.3); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(80, t); lp.Q.setValueAtTime(3, t); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(peak, t + 0.02); env.gain.exponentialRampToValueAtTime(0.001, t + 0.42); osc.connect(lp); lp.connect(env); env.connect(this.gainNode); osc.start(t); osc.stop(t + 0.45); // Mechanical piston wheeze const wheezeBuf = this.am.createNoiseBuffer('pink', 0.3); const wheeze = ctx.createBufferSource(); wheeze.buffer = wheezeBuf; const wheezeBp = ctx.createBiquadFilter(); wheezeBp.type = 'bandpass'; wheezeBp.frequency.setValueAtTime(380, t); wheezeBp.Q.setValueAtTime(1.6, t); const wheezeEnv = ctx.createGain(); wheezeEnv.gain.setValueAtTime(0.001, t + 0.02); wheezeEnv.gain.linearRampToValueAtTime(0.05, t + 0.08); wheezeEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.3); wheeze.connect(wheezeBp); wheezeBp.connect(wheezeEnv); wheezeEnv.connect(this.gainNode); wheeze.start(t); wheeze.stop(t + 0.32); }; for (let i = 0; i < strokes; i++) { stroke(now + i * (0.34 + Math.random() * 0.2), 0.3 - i * 0.04); } } /** * Ambient Loop: Rhythmic diesel chug while the Bebop is engaged */ startBebopChug(minIntervalMs = 2200, maxIntervalMs = 4200) { this._scheduleLoop('bebopChug', minIntervalMs, maxIntervalMs, () => { this.synthesizeBebopChugStroke(); }); } /** * Centrifugal Habitat Carousel Motor Groan (Military, audio_gaps.md #37) * Deep rotational strain of a kilometer-long rotating drum: sub-audible * 16 Hz rotational pair with bearing harmonics, modulated by a single * ~1 RPM (0.0167 Hz) revolution LFO. One 58 s rotation pass. */ synthesizeCarouselGroanCycle() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 58.0; const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.55, now + 8.0); env.gain.linearRampToValueAtTime(0.5, now + 50.0); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); env.connect(this.gainNode); // One rotation per ~60 s: slow strain swell and release const rotation = ctx.createOscillator(); rotation.type = 'sine'; rotation.frequency.setValueAtTime(1 / 60, now); const rotationDepth = ctx.createGain(); rotationDepth.gain.setValueAtTime(0.38, now); rotation.connect(rotationDepth); rotationDepth.connect(env.gain); const partials = [ { freq: 16, amp: 0.55 }, { freq: 16.04, amp: 0.32 }, // rotational beating pair { freq: 48, amp: 0.22 }, // structural harmonic { freq: 96, amp: 0.07 } // bearing hum harmonic ]; partials.forEach((partial) => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(partial.freq, now); const g = ctx.createGain(); g.gain.setValueAtTime(partial.amp, now); osc.connect(g); g.connect(env); osc.start(now); osc.stop(now + duration + 0.1); }); rotation.start(now); rotation.stop(now + duration + 0.1); } /** * Ambient Loop: Carousel rotation groan while the Agamemnon is engaged * (one 58 s pass every 60 s; the 2 s gap reads as a rotation splice) */ startCarouselGroan(intervalMs = 60000) { this._scheduleLoop('carouselGroan', intervalMs, intervalMs, () => { this.synthesizeCarouselGroanCycle(); }); } /** * Slipstream Transition Surge (Military, audio_gaps.md #38) * Ship tearing through an exotic slipstream: dual sweeping bandpass surges * (200 -> 2400 Hz) with escalating resonance Q and a comb-filter shimmer * tail. ~4.2 s. */ synthesizeSlipstreamSurge() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Comb shimmer tail bus const shimmer = ctx.createDelay(0.5); shimmer.delayTime.setValueAtTime(0.12, now); const shimmerFb = ctx.createGain(); shimmerFb.gain.setValueAtTime(0.3, now); const shimmerLp = ctx.createBiquadFilter(); shimmerLp.type = 'lowpass'; shimmerLp.frequency.setValueAtTime(4000, now); shimmer.connect(shimmerFb); shimmerFb.connect(shimmerLp); shimmerLp.connect(shimmer); const shimmerWet = ctx.createGain(); shimmerWet.gain.setValueAtTime(0.5, now); shimmer.connect(shimmerWet); shimmerWet.connect(this.gainNode); const surge = (offset, peak) => { const t = now + offset; const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(200, t); osc.frequency.exponentialRampToValueAtTime(2400, t + 2.6); const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(200, t); bp.frequency.exponentialRampToValueAtTime(2400, t + 2.6); bp.Q.setValueAtTime(2, t); bp.Q.linearRampToValueAtTime(10, t + 2.6); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(peak, t + 1.4); env.gain.setValueAtTime(peak, t + 2.7); env.gain.exponentialRampToValueAtTime(0.001, t + 3.8); osc.connect(bp); bp.connect(env); env.connect(this.gainNode); env.connect(shimmer); osc.start(t); osc.stop(t + 4.0); }; surge(0, 0.22); surge(0.3, 0.16); } /** * Viper Pilot Oxygen Regulator Demand Valve (Military, audio_gaps.md #39) * Pulsing rebreather of a Viper pilot mid-combat: a sharp 15 ms mechanical * diaphragm click preceding pulsed highpass-filtered white noise breath * (1.2-4.5 kHz, ~1.1 s). One breath cycle. */ synthesizeOxygenRegulatorCycle() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; // Sharp mechanical diaphragm click const click = ctx.createOscillator(); click.type = 'triangle'; click.frequency.setValueAtTime(1800, now); click.frequency.exponentialRampToValueAtTime(600, now + 0.015); const clickEnv = ctx.createGain(); clickEnv.gain.setValueAtTime(0.3, now); clickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.02); click.connect(clickEnv); clickEnv.connect(this.gainNode); click.start(now); click.stop(now + 0.025); // Regulated airflow breath const noiseBuf = this.am.createNoiseBuffer('white', 1.3); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.setValueAtTime(1200, now + 0.02); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(4500, now + 0.02); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now + 0.02); env.gain.linearRampToValueAtTime(0.13, now + 0.45); env.gain.linearRampToValueAtTime(0.09, now + 0.62); env.gain.exponentialRampToValueAtTime(0.001, now + 1.15); noise.connect(hp); hp.connect(lp); lp.connect(env); env.connect(this.gainNode); noise.start(now + 0.02); noise.stop(now + 1.2); } /** * Ambient Loop: Rhythmic oxygen demand breathing while a Viper cockpit * is engaged */ startOxygenRegulator(minIntervalMs = 3400, maxIntervalMs = 5200) { this._scheduleLoop('oxygenRegulator', minIntervalMs, maxIntervalMs, () => { this.synthesizeOxygenRegulatorCycle(); }); } /** * Condensation Pipe Drip & Expansion Tick (Industrial, audio_gaps.md #40) * Lonely water droplet pinging inside a kilometres-long cargo hauler: * high-Q sine pings (1400-2600 Hz) with fast exponential decay on a * randomized clock. One drip. */ synthesizePipeDrip() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const decay = 0.01 + Math.random() * 0.015; const osc = ctx.createOscillator(); osc.type = 'sine'; const freq = 1400 + Math.random() * 1200; osc.frequency.setValueAtTime(freq, now); osc.frequency.exponentialRampToValueAtTime(freq * 0.92, now + decay); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.1 + Math.random() * 0.2, now + 0.001); env.gain.exponentialRampToValueAtTime(0.0001, now + decay + 0.01); osc.connect(env); env.connect(this.gainNode); osc.start(now); osc.stop(now + decay + 0.03); } /** * Ambient Loop: Sporadic condensation drips while the Nostromo is engaged */ startPipeDrips(minIntervalMs = 500, maxIntervalMs = 3500) { this._scheduleLoop('pipeDrips', minIntervalMs, maxIntervalMs, () => { this.synthesizePipeDrip(); }); } /** * High-Pressure Steam / Boiler Venting (Industrial, audio_gaps.md #41) * Continuous hot-vent hiss of a steam-era mining vessel: bandpass-filtered * white noise (800-2800 Hz) with a slow amplitude swell and random * micro-flutter. One ~9 s vent pass. */ synthesizeSteamVentSwell() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 9.0; // Looping noise bed (short buffer, sustained vent) const noiseBuf = this.am.createNoiseBuffer('white', 4.0); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; noise.loop = true; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(1500, now); bp.Q.setValueAtTime(0.7, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.17, now + 3.0); env.gain.linearRampToValueAtTime(0.15, now + 5.5); env.gain.exponentialRampToValueAtTime(0.0001, now + duration); // Boiler pressure micro-flutter (~6-9 Hz jitter) const flutter = ctx.createOscillator(); flutter.type = 'sine'; flutter.frequency.setValueAtTime(6 + Math.random() * 3, now); const flutterDepth = ctx.createGain(); flutterDepth.gain.setValueAtTime(0.04, now); flutter.connect(flutterDepth); flutterDepth.connect(env.gain); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + duration + 0.1); flutter.start(now); flutter.stop(now + duration + 0.1); } /** * Ambient Loop: Boiler vent passes while Serenity-class and Starbug * presets are engaged */ startSteamVent(minIntervalMs = 6000, maxIntervalMs = 10000) { this._scheduleLoop('steamVent', minIntervalMs, maxIntervalMs, () => { this.synthesizeSteamVentSwell(); }); } /** * Crash-Couch Hydraulic Gimbal Strain (Industrial, audio_gaps.md #42) * Pilot's couch swinging into launch position: FM triangle carrier * (140 Hz / 35 Hz modulator) through lowpass damping that tracks the * acceleration strain. ~1.9 s. */ synthesizeCrashCouchGimbal() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const duration = 1.9; const layer = (offset, carrierFreq, modFreq) => { const t = now + offset; const carrier = ctx.createOscillator(); carrier.type = 'triangle'; carrier.frequency.setValueAtTime(carrierFreq, t); const mod = ctx.createOscillator(); mod.type = 'sine'; mod.frequency.setValueAtTime(modFreq, t); const modDepth = ctx.createGain(); modDepth.gain.setValueAtTime(carrierFreq * 4, t); mod.connect(modDepth); modDepth.connect(carrier.frequency); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(300, t); lp.frequency.linearRampToValueAtTime(900, t + 0.5); lp.frequency.exponentialRampToValueAtTime(350, t + duration - offset); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, t); env.gain.linearRampToValueAtTime(0.24, t + 0.55); env.gain.linearRampToValueAtTime(0.2, t + 1.0); env.gain.exponentialRampToValueAtTime(0.0001, t + duration - offset); carrier.connect(lp); lp.connect(env); env.connect(this.gainNode); carrier.start(t); carrier.stop(t + (duration - offset) + 0.05); mod.start(t); mod.stop(t + (duration - offset) + 0.05); }; layer(0, 140, 35); layer(0.42, 152, 38); } /** * HAL 9000 Breathing Loop (Retro Future, audio_gaps.md #43) * Eerie slow respiration of a quiet, observant ship computer: rhythmic * bandpass-filtered pink noise (450-1100 Hz) with a gentle 3.5 s * inhalation/exhalation envelope. One breath. */ synthesizeHalBreathCycle() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const inhale = 1.3; const exhale = 2.2; const noiseBuf = this.am.createNoiseBuffer('pink', 4.0); const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; noise.loop = true; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(700, now); bp.Q.setValueAtTime(1.2, now); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.085, now + inhale); env.gain.exponentialRampToValueAtTime(0.001, now + inhale + exhale); noise.connect(bp); bp.connect(env); env.connect(this.gainNode); noise.start(now); noise.stop(now + inhale + exhale + 0.05); } /** * Ambient Loop: Continuous respiration while the Discovery One is engaged */ startHalBreathing(intervalMs = 3500) { this._scheduleLoop('halBreathing', intervalMs, intervalMs, () => { this.synthesizeHalBreathCycle(); }); } /** * Death Blossom Energy Surge Ramp (Retro Future, audio_gaps.md #44) * Fearsome gunstar overdrive: cascaded sawtooth exponential ramp * (200 Hz -> 8000 Hz over 2.5 s) with rising overdrive, cutting off * abruptly at full bloom. */ synthesizeDeathBlossomSurge() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const rampDur = 2.5; const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.setValueAtTime(400, now); hp.frequency.exponentialRampToValueAtTime(4000, now + rampDur); const hp2 = ctx.createBiquadFilter(); hp2.type = 'highpass'; hp2.frequency.setValueAtTime(200, now); hp2.frequency.exponentialRampToValueAtTime(2000, now + rampDur); const env = ctx.createGain(); env.gain.setValueAtTime(0.001, now); env.gain.linearRampToValueAtTime(0.32, now + rampDur); env.gain.setValueAtTime(0.32, now + rampDur + 0.03); env.gain.exponentialRampToValueAtTime(0.001, now + rampDur + 0.4); hp.connect(hp2); hp2.connect(env); env.connect(this.gainNode); // Cascaded overdriven saws with staggered ignition [0, 0.15, 0.3].forEach((offset, idx) => { const t = now + offset; const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(200, t); osc.frequency.exponentialRampToValueAtTime(8000, t + rampDur - offset); const g = ctx.createGain(); g.gain.setValueAtTime(0.33 - idx * 0.08, t); osc.connect(g); g.connect(hp); osc.start(t); osc.stop(t + (rampDur - offset) + 0.45); }); // Abrupt bloom transient const burstTime = now + rampDur; const burstBuf = this.am.createNoiseBuffer('white', 0.25); const burst = ctx.createBufferSource(); burst.buffer = burstBuf; const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(3500, burstTime); bp.Q.setValueAtTime(1.5, burstTime); const burstEnv = ctx.createGain(); burstEnv.gain.setValueAtTime(0.001, burstTime); burstEnv.gain.linearRampToValueAtTime(0.3, burstTime + 0.01); burstEnv.gain.exponentialRampToValueAtTime(0.001, burstTime + 0.22); burst.connect(bp); bp.connect(burstEnv); burstEnv.connect(this.gainNode); burst.start(burstTime); burst.stop(burstTime + 0.25); } /** * Victorian Steam Piston Chug (Retro Future, audio_gaps.md #45) * The steam-age skeleton of the Cygnus: lowpass-filtered noise bursts * synchronized with heavy 40 Hz triangle thumps and a metallic slapback. * One cluster of 2-3 stroke cycles. */ synthesizeCygnusPistonChug() { this.init(); const ctx = this.am.ctx; if (!ctx || !this.gainNode) return; const now = ctx.currentTime; const strokes = 2 + Math.floor(Math.random() * 2); // 2-3 strokes const slap = ctx.createDelay(0.6); slap.delayTime.setValueAtTime(0.35, now); const slapFb = ctx.createGain(); slapFb.gain.setValueAtTime(0.3, now); const slapWet = ctx.createGain(); slapWet.gain.setValueAtTime(0.4, now); slap.connect(slapFb); slapFb.connect(slap); slap.connect(slapWet); slapWet.connect(this.gainNode); const stroke = (t, peak) => { // Heavy piston body thump const thump = ctx.createOscillator(); thump.type = 'triangle'; thump.frequency.setValueAtTime(40, t); thump.frequency.linearRampToValueAtTime(32, t + 0.4); const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(120, t); const thumpEnv = ctx.createGain(); thumpEnv.gain.setValueAtTime(0.001, t); thumpEnv.gain.linearRampToValueAtTime(peak, t + 0.02); thumpEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.45); thump.connect(lp); lp.connect(thumpEnv); thumpEnv.connect(this.gainNode); thumpEnv.connect(slap); thump.start(t); thump.stop(t + 0.5); // Synchronized steam exhaust puff const puffBuf = this.am.createNoiseBuffer('white', 0.16); const puff = ctx.createBufferSource(); puff.buffer = puffBuf; const puffLp = ctx.createBiquadFilter(); puffLp.type = 'lowpass'; puffLp.frequency.setValueAtTime(500, t); const puffEnv = ctx.createGain(); puffEnv.gain.setValueAtTime(0.001, t + 0.01); puffEnv.gain.linearRampToValueAtTime(0.13, t + 0.03); puffEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.15); puff.connect(puffLp); puffLp.connect(puffEnv); puffEnv.connect(this.gainNode); puff.start(t); puff.stop(t + 0.17); }; for (let i = 0; i < strokes; i++) { stroke(now + i * (0.5 + Math.random() * 0.25), 0.26 - i * 0.03); } } /** * Ambient Loop: Steam piston chug while the Cygnus is engaged */ startCygnusChug(minIntervalMs = 1800, maxIntervalMs = 3200) { this._scheduleLoop('cygnusChug', minIntervalMs, maxIntervalMs, () => { this.synthesizeCygnusPistonChug(); }); } stopAllLoops() { this.stopMedicalMonitor(); this.stopStationSparks(); Object.keys(this.loopTimers).forEach((key) => { this._stopLoop(key); }); } } window.ExpandedSciFiAudioSynth = ExpandedSciFiAudioSynth; /** * Layered & Staged Engine Startup and Shutdown Synthesizer * Synthesizes chronological multi-phase acoustic scripts for 15 distinct starship archetypes. */ class EngineTransitionSynth { constructor(audioManager) { this.am = audioManager; this.gainNode = null; this.activeNodes = []; this.activeTimeouts = []; } init() { if (!this.am.ctx) return; if (!this.gainNode) { this.gainNode = this.am.ctx.createGain(); this.gainNode.gain.setValueAtTime(0.85, this.am.ctx.currentTime); this.gainNode.connect(this.am.compressor); } } stopTransitions() { this.activeTimeouts.forEach(t => clearTimeout(t)); this.activeTimeouts = []; this.activeNodes.forEach(node => { try { if (node.stop) node.stop(); } catch (e) {} try { node.disconnect(); } catch (e) {} }); this.activeNodes = []; } _tone({ type = 'sine', startFreq, endFreq, startTime, duration, startVol = 0.001, peakVol = 0.4, endVol = 0.0001, filterType = null, filterFreq = 1000, filterQ = 1.0 }) { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return null; const osc = ctx.createOscillator(); osc.type = type; osc.frequency.setValueAtTime(Math.max(10, startFreq), startTime); if (endFreq && endFreq !== startFreq) { osc.frequency.exponentialRampToValueAtTime(Math.max(10, endFreq), startTime + duration); } const env = ctx.createGain(); env.gain.setValueAtTime(Math.max(0.0001, startVol), startTime); const attack = Math.min(0.08, duration * 0.25); env.gain.linearRampToValueAtTime(peakVol, startTime + attack); env.gain.exponentialRampToValueAtTime(Math.max(0.0001, endVol), startTime + duration); let lastNode = osc; if (filterType) { const filter = ctx.createBiquadFilter(); filter.type = filterType; filter.frequency.setValueAtTime(filterFreq, startTime); filter.Q.setValueAtTime(filterQ, startTime); lastNode.connect(filter); lastNode = filter; this.activeNodes.push(filter); } lastNode.connect(env); env.connect(this.gainNode); osc.start(startTime); osc.stop(startTime + duration); this.activeNodes.push(osc, env); return osc; } _noise({ noiseType = 'pink', filterType = 'bandpass', startFreq = 800, endFreq = 800, Q = 1.5, startTime, duration, peakVol = 0.35 }) { const ctx = this.am.ctx; if (!ctx || !this.gainNode) return null; const buf = this.am.createNoiseBuffer(noiseType, Math.max(3, Math.ceil(duration + 1))); const src = ctx.createBufferSource(); src.buffer = buf; const filter = ctx.createBiquadFilter(); filter.type = filterType; filter.frequency.setValueAtTime(Math.max(20, startFreq), startTime); if (endFreq && endFreq !== startFreq) { filter.frequency.exponentialRampToValueAtTime(Math.max(20, endFreq), startTime + duration); } filter.Q.setValueAtTime(Q, startTime); const env = ctx.createGain(); env.gain.setValueAtTime(0.0001, startTime); const attack = Math.min(0.12, duration * 0.3); env.gain.linearRampToValueAtTime(peakVol, startTime + attack); env.gain.exponentialRampToValueAtTime(0.0001, startTime + duration); src.connect(filter); filter.connect(env); env.connect(this.gainNode); src.start(startTime); src.stop(startTime + duration); this.activeNodes.push(src, filter, env); return src; } _click(startTime, freq = 140, duration = 0.035, vol = 0.4, type = 'square') { return this._tone({ type, startFreq: freq, endFreq: 25, startTime, duration, peakVol: vol, endVol: 0.0001 }); } _sub(startTime, startFreq = 70, endFreq = 30, duration = 0.8, vol = 0.6) { return this._tone({ type: 'sine', startFreq, endFreq, startTime, duration, peakVol: vol, endVol: 0.0001 }); } _chime(startTime, pitches = [880, 1174, 1567], step = 0.05, duration = 0.35, vol = 0.25) { pitches.forEach((freq, idx) => { const t = startTime + idx * step; this._tone({ type: 'sine', startFreq: freq, endFreq: freq * 1.01, startTime: t, duration, peakVol: vol, endVol: 0.0001 }); }); } playStartup(profileKey = 'galaxy', duration = 2.5) { this.init(); if (!this.am.ctx) return; this.stopTransitions(); const now = this.am.ctx.currentTime; const s = duration / 2.5; switch (profileKey) { case 'intrepid': this._startupIntrepid(now, s); break; case 'defiant': this._startupDefiant(now, s); break; case 'cardassian': this._startupCardassian(now, s); break; case 'tos': this._startupTOS(now, s); break; case 'nx': this._startupNX(now, s); break; case 'tardis': this._startupTardis(now, s); break; case 'industrial': this._startupIndustrial(now, s); break; case 'bioship': this._startupBioship(now, s); break; case 'retrofuture': this._startupRetrofuture(now, s); break; case 'military': this._startupMilitary(now, s); break; case 'deepspace': this._startupDeepSpace(now, s); break; case 'outlaw': this._startupOutlaw(now, s); break; case 'station': this._startupStation(now, s); break; case 'comedy': this._startupComedy(now, s); break; case 'galaxy': default: this._startupGalaxy(now, s); break; } } playShutdown(profileKey = 'galaxy', duration = 2.5) { this.init(); if (!this.am.ctx) return; this.stopTransitions(); const now = this.am.ctx.currentTime; const s = duration / 2.5; switch (profileKey) { case 'intrepid': this._shutdownIntrepid(now, s); break; case 'defiant': this._shutdownDefiant(now, s); break; case 'cardassian': this._shutdownCardassian(now, s); break; case 'tos': this._shutdownTOS(now, s); break; case 'nx': this._shutdownNX(now, s); break; case 'tardis': this._shutdownTardis(now, s); break; case 'industrial': this._shutdownIndustrial(now, s); break; case 'bioship': this._shutdownBioship(now, s); break; case 'retrofuture': this._shutdownRetrofuture(now, s); break; case 'military': this._shutdownMilitary(now, s); break; case 'deepspace': this._shutdownDeepSpace(now, s); break; case 'outlaw': this._shutdownOutlaw(now, s); break; case 'station': this._shutdownStation(now, s); break; case 'comedy': this._shutdownComedy(now, s); break; case 'galaxy': default: this._shutdownGalaxy(now, s); break; } } // 1. TNG GALAXY CLASS _startupGalaxy(now, s) { this._click(now, 150, 0.04, 0.45); this._click(now + 0.14 * s, 110, 0.03, 0.35); this._tone({ type: 'sine', startFreq: 60, endFreq: 140, startTime: now + 0.05 * s, duration: 0.65 * s, peakVol: 0.35 }); this._tone({ type: 'sawtooth', startFreq: 120, endFreq: 480, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 750, filterQ: 3.5 }); this._sub(now + 0.85 * s, 65, 80, 0.95 * s, 0.55); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 200, endFreq: 600, startTime: now + 0.9 * s, duration: 0.9 * s, peakVol: 0.25 }); this._chime(now + 1.8 * s, [784, 1046, 1318], 0.06 * s, 0.5 * s, 0.3); this._sub(now + 1.8 * s, 80, 58, 0.65 * s, 0.45); } _shutdownGalaxy(now, s) { this._click(now, 160, 0.05, 0.5); this._sub(now, 85, 40, 0.65 * s, 0.6); this._tone({ type: 'sawtooth', startFreq: 460, endFreq: 75, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 600, filterQ: 2.0 }); this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 650, endFreq: 180, startTime: now + 0.75 * s, duration: 1.0 * s, peakVol: 0.28 }); this._tone({ type: 'sine', startFreq: 75, endFreq: 24, startTime: now + 1.7 * s, duration: 0.75 * s, peakVol: 0.35 }); } // 2. VOYAGER INTREPID CLASS _startupIntrepid(now, s) { this._click(now, 450, 0.025, 0.35); this._click(now + 0.08 * s, 680, 0.025, 0.35); this._tone({ type: 'triangle', startFreq: 240, endFreq: 580, startTime: now + 0.1 * s, duration: 0.6 * s, peakVol: 0.3 }); this._tone({ type: 'sawtooth', startFreq: 320, endFreq: 1650, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.35, filterType: 'bandpass', filterFreq: 1400, filterQ: 4.5 }); this._sub(now + 0.8 * s, 70, 95, 0.9 * s, 0.45); this._noise({ noiseType: 'white', filterType: 'bandpass', startFreq: 1200, endFreq: 2400, startTime: now + 0.85 * s, duration: 0.9 * s, peakVol: 0.22 }); this._chime(now + 1.8 * s, [1174, 1567, 2093], 0.05 * s, 0.45 * s, 0.28); this._tone({ type: 'sine', startFreq: 95, endFreq: 70, startTime: now + 1.85 * s, duration: 0.65 * s, peakVol: 0.4 }); } _shutdownIntrepid(now, s) { this._click(now, 520, 0.03, 0.4); this._sub(now, 95, 50, 0.5 * s, 0.5); this._tone({ type: 'sawtooth', startFreq: 1600, endFreq: 140, startTime: now + 0.5 * s, duration: 1.2 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 1200 }); this._noise({ noiseType: 'white', filterType: 'highpass', startFreq: 1400, endFreq: 400, startTime: now + 0.6 * s, duration: 0.8 * s, peakVol: 0.25 }); this._tone({ type: 'sine', startFreq: 140, endFreq: 30, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 }); } // 3. DEFIANT ESCORT _startupDefiant(now, s) { this._click(now, 90, 0.06, 0.6, 'square'); this._tone({ type: 'sawtooth', startFreq: 50, endFreq: 120, startTime: now + 0.05 * s, duration: 0.6 * s, peakVol: 0.4, filterType: 'lowpass', filterFreq: 300 }); this._sub(now + 0.65 * s, 35, 95, 1.1 * s, 0.7); this._tone({ type: 'sawtooth', startFreq: 180, endFreq: 740, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.32, filterType: 'bandpass', filterFreq: 550, filterQ: 3.0 }); this._chime(now + 1.8 * s, [660, 880], 0.08 * s, 0.4 * s, 0.35); this._sub(now + 1.8 * s, 95, 68, 0.7 * s, 0.5); } _shutdownDefiant(now, s) { this._click(now, 110, 0.05, 0.6); this._sub(now, 90, 35, 0.6 * s, 0.65); this._tone({ type: 'sawtooth', startFreq: 720, endFreq: 60, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.3, filterType: 'lowpass', filterFreq: 450 }); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 500, endFreq: 120, startTime: now + 0.7 * s, duration: 1.0 * s, peakVol: 0.3 }); this._tone({ type: 'sine', startFreq: 60, endFreq: 22, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.35 }); } // 4. CARDASSIAN / DS9 _startupCardassian(now, s) { this._click(now, 85, 0.08, 0.55); this._tone({ type: 'triangle', startFreq: 165, endFreq: 110, startTime: now, duration: 0.7 * s, peakVol: 0.4 }); this._sub(now + 0.7 * s, 32, 68, 1.1 * s, 0.65); this._tone({ type: 'sawtooth', startFreq: 80, endFreq: 260, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.3, filterType: 'lowpass', filterFreq: 320 }); this._noise({ noiseType: 'brown', filterType: 'lowpass', startFreq: 180, endFreq: 350, startTime: now + 0.8 * s, duration: 1.0 * s, peakVol: 0.3 }); this._chime(now + 1.8 * s, [330, 440], 0.1 * s, 0.5 * s, 0.35); } _shutdownCardassian(now, s) { this._click(now, 95, 0.06, 0.5); this._sub(now, 68, 30, 0.6 * s, 0.6); this._tone({ type: 'sawtooth', startFreq: 250, endFreq: 40, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 220 }); this._noise({ noiseType: 'brown', filterType: 'bandpass', startFreq: 240, endFreq: 80, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.25 }); this._tone({ type: 'sine', startFreq: 40, endFreq: 20, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.3 }); } // 5. TOS 1960s _startupTOS(now, s) { this._click(now, 220, 0.03, 0.45); this._tone({ type: 'sine', startFreq: 80, endFreq: 180, startTime: now + 0.04 * s, duration: 0.6 * s, peakVol: 0.35 }); this._tone({ type: 'sine', startFreq: 220, endFreq: 980, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.32 }); this._tone({ type: 'triangle', startFreq: 225, endFreq: 990, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.2 }); this._chime(now + 1.8 * s, [880, 1108], 0.08 * s, 0.35 * s, 0.3); } _shutdownTOS(now, s) { this._click(now, 260, 0.035, 0.45); this._tone({ type: 'sine', startFreq: 950, endFreq: 90, startTime: now + 0.5 * s, duration: 1.2 * s, peakVol: 0.3 }); this._tone({ type: 'sine', startFreq: 2400, endFreq: 400, startTime: now + 0.6 * s, duration: 0.8 * s, peakVol: 0.15 }); this._tone({ type: 'sine', startFreq: 90, endFreq: 25, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 }); } // 6. NX-01 PROTOTYPE _startupNX(now, s) { [0, 0.12, 0.24, 0.36, 0.48].forEach(dt => this._click(now + dt * s, 110, 0.04, 0.45)); this._tone({ type: 'sawtooth', startFreq: 95, endFreq: 420, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.32, filterType: 'lowpass', filterFreq: 600 }); this._sub(now + 0.8 * s, 45, 75, 1.0 * s, 0.6); this._noise({ noiseType: 'brown', filterType: 'lowpass', startFreq: 220, endFreq: 480, startTime: now + 0.85 * s, duration: 0.95 * s, peakVol: 0.3 }); this._click(now + 1.8 * s, 180, 0.03, 0.4); } _shutdownNX(now, s) { this._click(now, 95, 0.06, 0.55); this._sub(now, 75, 35, 0.6 * s, 0.6); [0.6, 0.85, 1.15, 1.5].forEach(dt => this._click(now + dt * s, 85, 0.04, 0.35)); this._noise({ noiseType: 'pink', filterType: 'highpass', startFreq: 900, endFreq: 300, startTime: now + 0.7 * s, duration: 1.0 * s, peakVol: 0.25 }); this._tone({ type: 'sine', startFreq: 40, endFreq: 18, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.3 }); } // 7. TARDIS DIMENSIONAL _startupTardis(now, s) { this._click(now, 80, 0.06, 0.55); this._tone({ type: 'sawtooth', startFreq: 140, endFreq: 680, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.32, filterType: 'bandpass', filterFreq: 550, filterQ: 3.0 }); this._tone({ type: 'sine', startFreq: 70, endFreq: 220, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.35 }); this._chime(now + 1.8 * s, [1046, 1318, 1567], 0.06 * s, 0.45 * s, 0.3); } _shutdownTardis(now, s) { this._click(now, 90, 0.05, 0.5); this._tone({ type: 'sawtooth', startFreq: 620, endFreq: 75, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.28, filterType: 'bandpass', filterFreq: 350, filterQ: 2.0 }); this._chime(now + 0.8 * s, [440], 0.1, 0.7 * s, 0.3); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 350, endFreq: 80, startTime: now + 1.6 * s, duration: 0.9 * s, peakVol: 0.2 }); } // 8. INDUSTRIAL FREIGHTER _startupIndustrial(now, s) { this._click(now, 75, 0.08, 0.6); this._noise({ noiseType: 'white', filterType: 'highpass', startFreq: 1800, endFreq: 800, startTime: now + 0.05 * s, duration: 0.4 * s, peakVol: 0.35 }); this._tone({ type: 'sawtooth', startFreq: 55, endFreq: 290, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.35, filterType: 'lowpass', filterFreq: 400 }); this._sub(now + 0.7 * s, 35, 65, 1.1 * s, 0.7); this._noise({ noiseType: 'brown', filterType: 'bandpass', startFreq: 400, endFreq: 900, startTime: now + 0.8 * s, duration: 1.0 * s, peakVol: 0.3 }); this._sub(now + 1.8 * s, 65, 40, 0.7 * s, 0.6); } _shutdownIndustrial(now, s) { this._click(now, 85, 0.07, 0.65); this._sub(now, 65, 28, 0.6 * s, 0.6); this._noise({ noiseType: 'white', filterType: 'lowpass', startFreq: 2400, endFreq: 400, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.38 }); this._tone({ type: 'sawtooth', startFreq: 280, endFreq: 40, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 300 }); [1.7, 1.95, 2.2].forEach(dt => this._click(now + dt * s, 320, 0.02, 0.25)); } // 9. BIOSHIP _startupBioship(now, s) { this._chime(now, [784, 1174, 1567], 0.04 * s, 0.3 * s, 0.25); this._sub(now + 0.6 * s, 42, 28, 0.4 * s, 0.65); this._sub(now + 0.95 * s, 44, 28, 0.4 * s, 0.7); this._tone({ type: 'sine', startFreq: 260, endFreq: 540, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.32 }); this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 250, endFreq: 600, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.3 }); } _shutdownBioship(now, s) { this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 550, endFreq: 180, startTime: now, duration: 1.1 * s, peakVol: 0.35 }); this._sub(now + 0.5 * s, 38, 25, 0.4 * s, 0.55); this._sub(now + 1.1 * s, 34, 22, 0.4 * s, 0.45); this._tone({ type: 'sine', startFreq: 480, endFreq: 180, startTime: now + 0.6 * s, duration: 1.1 * s, peakVol: 0.22 }); this._tone({ type: 'sine', startFreq: 35, endFreq: 18, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 }); } // 10. RETROFUTURE _startupRetrofuture(now, s) { [0, 0.07, 0.14, 0.21].forEach(dt => this._click(now + dt * s, 380, 0.02, 0.3)); this._tone({ type: 'triangle', startFreq: 95, endFreq: 580, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.35, filterType: 'lowpass', filterFreq: 650 }); this._tone({ type: 'sine', startFreq: 100, endFreq: 590, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.25 }); this._chime(now + 1.8 * s, [920], 0.1, 0.5 * s, 0.35); } _shutdownRetrofuture(now, s) { this._click(now, 140, 0.05, 0.55); this._tone({ type: 'triangle', startFreq: 550, endFreq: 65, startTime: now + 0.5 * s, duration: 1.2 * s, peakVol: 0.28 }); this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 450, endFreq: 150, startTime: now + 0.6 * s, duration: 1.1 * s, peakVol: 0.22 }); this._tone({ type: 'sine', startFreq: 3200, endFreq: 120, startTime: now + 1.7 * s, duration: 0.7 * s, peakVol: 0.2 }); } // 11. MILITARY _startupMilitary(now, s) { this._click(now, 120, 0.06, 0.6); this._tone({ type: 'square', startFreq: 60, endFreq: 120, startTime: now, duration: 0.6 * s, peakVol: 0.35, filterType: 'lowpass', filterFreq: 250 }); this._tone({ type: 'sawtooth', startFreq: 140, endFreq: 880, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.35, filterType: 'bandpass', filterFreq: 750, filterQ: 3.0 }); this._sub(now + 0.7 * s, 40, 85, 1.1 * s, 0.65); this._chime(now + 1.8 * s, [750, 750], 0.08 * s, 0.35 * s, 0.35); } _shutdownMilitary(now, s) { this._click(now, 150, 0.04, 0.5); this._chime(now + 0.05 * s, [880], 0.1, 0.3 * s, 0.35); this._tone({ type: 'sawtooth', startFreq: 850, endFreq: 70, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 500 }); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 600, endFreq: 140, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.28 }); this._sub(now + 1.7 * s, 70, 25, 0.8 * s, 0.4); } // 12. DEEP SPACE _startupDeepSpace(now, s) { this._noise({ noiseType: 'white', filterType: 'bandpass', startFreq: 2400, endFreq: 1200, startTime: now, duration: 0.65 * s, peakVol: 0.32 }); this._sub(now + 0.7 * s, 28, 68, 1.1 * s, 0.7); this._tone({ type: 'sine', startFreq: 110, endFreq: 280, startTime: now + 0.75 * s, duration: 1.1 * s, peakVol: 0.3 }); this._chime(now + 1.8 * s, [440, 554], 0.1 * s, 0.5 * s, 0.25); } _shutdownDeepSpace(now, s) { this._sub(now, 68, 25, 0.7 * s, 0.65); this._tone({ type: 'sine', startFreq: 260, endFreq: 50, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25 }); this._noise({ noiseType: 'brown', filterType: 'lowpass', startFreq: 200, endFreq: 40, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.3 }); this._sub(now + 1.7 * s, 35, 15, 0.8 * s, 0.25); } // 13. OUTLAW _startupOutlaw(now, s) { [0, 0.1, 0.22, 0.32, 0.44].forEach(dt => this._click(now + dt * s, 130 + Math.random() * 40, 0.035, 0.4)); this._tone({ type: 'sawtooth', startFreq: 70, endFreq: 130, startTime: now + 0.1 * s, duration: 0.5 * s, peakVol: 0.3, filterType: 'lowpass', filterFreq: 280 }); this._tone({ type: 'sawtooth', startFreq: 180, endFreq: 1150, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.35, filterType: 'bandpass', filterFreq: 900, filterQ: 4.0 }); this._sub(now + 0.7 * s, 45, 80, 1.1 * s, 0.65); this._noise({ noiseType: 'white', filterType: 'highpass', startFreq: 2800, endFreq: 1500, startTime: now + 1.8 * s, duration: 0.3 * s, peakVol: 0.35 }); } _shutdownOutlaw(now, s) { this._click(now, 70, 0.08, 0.7, 'sawtooth'); this._sub(now, 90, 40, 0.4 * s, 0.7); [0.5, 0.8, 1.15, 1.55].forEach(dt => this._click(now + dt * s, 90, 0.04, 0.35)); this._tone({ type: 'sawtooth', startFreq: 750, endFreq: 55, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 350 }); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 350, endFreq: 80, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 }); } // 14. STATION _startupStation(now, s) { this._chime(now, [587, 880], 0.08 * s, 0.35 * s, 0.3); this._click(now + 0.15 * s, 110, 0.05, 0.45); this._sub(now + 0.6 * s, 30, 65, 1.2 * s, 0.7); this._tone({ type: 'sawtooth', startFreq: 70, endFreq: 240, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 280 }); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 200, endFreq: 500, startTime: now + 0.8 * s, duration: 1.0 * s, peakVol: 0.25 }); this._click(now + 1.8 * s, 85, 0.06, 0.5); } _shutdownStation(now, s) { this._click(now, 105, 0.05, 0.5); this._sub(now + 0.5 * s, 65, 25, 1.4 * s, 0.55); this._tone({ type: 'sawtooth', startFreq: 220, endFreq: 45, startTime: now + 0.55 * s, duration: 1.3 * s, peakVol: 0.22, filterType: 'lowpass', filterFreq: 200 }); this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 280, endFreq: 90, startTime: now + 1.6 * s, duration: 0.9 * s, peakVol: 0.2 }); } // 15. COMEDY _startupComedy(now, s) { this._chime(now, [330, 440, 554, 659], 0.07 * s, 0.35 * s, 0.32); this._tone({ type: 'sawtooth', startFreq: 110, endFreq: 1400, startTime: now + 0.55 * s, duration: 1.25 * s, peakVol: 0.3, filterType: 'bandpass', filterFreq: 800, filterQ: 3.5 }); this._sub(now + 0.7 * s, 40, 75, 1.1 * s, 0.5); this._chime(now + 1.8 * s, [1200], 0.1, 0.5 * s, 0.4); } _shutdownComedy(now, s) { this._tone({ type: 'sine', startFreq: 920, endFreq: 140, startTime: now, duration: 0.85 * s, peakVol: 0.32 }); [0.7, 0.82, 0.96, 1.12].forEach(dt => this._click(now + dt * s, 420 + Math.random() * 200, 0.025, 0.3)); this._noise({ noiseType: 'white', filterType: 'bandpass', startFreq: 300, endFreq: 100, startTime: now + 1.6 * s, duration: 0.6 * s, peakVol: 0.25 }); } } window.EngineTransitionSynth = EngineTransitionSynth;