From 19ff051d58153448b581c4f73baad15b3d099af6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 23:56:30 -0700 Subject: [PATCH] Regenerate standalone display from current source --- dist/SciFiAmbientDisplay_v4.html | 2396 ++++++++++++++++++++++++++++-- 1 file changed, 2257 insertions(+), 139 deletions(-) diff --git a/dist/SciFiAmbientDisplay_v4.html b/dist/SciFiAmbientDisplay_v4.html index 0a549b0..1712b56 100644 --- a/dist/SciFiAmbientDisplay_v4.html +++ b/dist/SciFiAmbientDisplay_v4.html @@ -360,6 +360,62 @@ body { color: #fff; } +/* Master Engage / Full Stop Play Button */ +#btn-master-play { + background: #00cc66; + color: #000000; + box-shadow: 0 0 12px rgba(0, 204, 102, 0.45); + transition: transform 0.1s, filter 0.15s, background-color 0.2s, box-shadow 0.2s; +} + +#btn-master-play:hover { + filter: brightness(1.2); +} + +#btn-master-play.playing { + background: #ff3333; + color: #ffffff; + box-shadow: 0 0 14px rgba(255, 51, 51, 0.5); +} + +#btn-master-play.engaging { + animation: lcars-engage-flash 0.35s infinite alternate ease-in-out; +} + +#btn-master-play.stopping { + animation: lcars-stop-flash 0.35s infinite alternate ease-in-out; +} + +@keyframes lcars-engage-flash { + 0% { + background: #00cc66; + color: #000000; + box-shadow: 0 0 6px rgba(0, 204, 102, 0.4); + filter: brightness(0.95); + } + 100% { + background: #33ff99; + color: #000000; + box-shadow: 0 0 24px rgba(0, 255, 136, 0.95); + filter: brightness(1.35); + } +} + +@keyframes lcars-stop-flash { + 0% { + background: #ff3333; + color: #ffffff; + box-shadow: 0 0 6px rgba(255, 51, 51, 0.4); + filter: brightness(0.95); + } + 100% { + background: #ff6666; + color: #ffffff; + box-shadow: 0 0 24px rgba(255, 51, 51, 0.95); + filter: brightness(1.35); + } +} + /* Action Pill Buttons */ .pill-button-group { display: flex; @@ -2396,7 +2452,7 @@ body.theme-comedy .observation-overlay { background: #07061a; }
@@ -2886,14 +2942,20 @@ class HullDroneSynth { }; } - start() { + start(rampDuration = 0) { this.stop(); const ctx = this.am.ctx; if (!ctx) return; // Channel Gain Node this.gainNode = ctx.createGain(); - this.gainNode.gain.setValueAtTime(this.isMuted ? 0 : this.params.volume, ctx.currentTime); + 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(); @@ -2961,7 +3023,22 @@ class HullDroneSynth { this.nodes = [this.subOsc1, this.subOsc2, this.noiseSource, lfo, osc1Gain, osc2Gain, noiseGain, lfoGain, this.filterNode, filterStage2, this.gainNode]; } - stop() { + 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(); @@ -2974,6 +3051,10 @@ class HullDroneSynth { try { node.disconnect(); } catch (e) {} }); this.nodes = []; + this.subOsc1 = null; + this.subOsc2 = null; + this.noiseSource = null; + this.gainNode = null; } } @@ -3047,13 +3128,19 @@ class WarpCoreSynth { this.onPulse = null; // Callback for UI visualizer pulse animation! } - start() { + start(rampDuration = 0) { 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); + 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(); @@ -3067,12 +3154,22 @@ class WarpCoreSynth { // Carrier & Modulator Oscillators (FM Engine) this.carrier = ctx.createOscillator(); this.carrier.type = this.params.pulseShape === 'tos' ? 'sawtooth' : (this.params.pulseShape === 'nx' ? 'triangle' : 'sine'); - this.carrier.frequency.setValueAtTime(this.params.carrierFreq, ctx.currentTime); + 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'; - this.subOsc.frequency.setValueAtTime(this.params.carrierFreq * 0.5, ctx.currentTime); + 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); @@ -3220,7 +3317,29 @@ class WarpCoreSynth { } } - stop() { + 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; @@ -3234,6 +3353,9 @@ class WarpCoreSynth { try { node.disconnect(); } catch (e) {} }); this.nodes = []; + this.carrier = null; + this.subOsc = null; + this.gainNode = null; } } @@ -3274,13 +3396,19 @@ class LifeSupportSynth { }; } - start() { + start(rampDuration = 0) { 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); + 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(); @@ -3341,7 +3469,22 @@ class LifeSupportSynth { ]; } - stop() { + 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(); @@ -3350,6 +3493,8 @@ class LifeSupportSynth { try { node.disconnect(); } catch (e) {} }); this.nodes = []; + this.noiseSource = null; + this.gainNode = null; } } @@ -3472,20 +3617,26 @@ class TelemetrySynth { case 'ds9': Math.random() > 0.5 ? this.synthesizeCardassianSensor() : this.synthesizeLCARSSingleChirp(); break; - case 'voyager': - Math.random() > 0.4 ? this.synthesizeLCARSDoubleChirp() : this.synthesizeSensorSweep(); + 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: + default: { const r = Math.random(); - if (r < 0.45) this.synthesizeLCARSSingleChirp(); - else if (r < 0.75) this.synthesizeLCARSDoubleChirp(); - else if (r < 0.90) this.synthesizeLCARSSequence(); + 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, @@ -3499,6 +3650,33 @@ class TelemetrySynth { })); } + /** + * 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) */ @@ -4421,6 +4599,105 @@ class WhoniverseAudioSynth { 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); + }); + } } window.WhoniverseAudioSynth = WhoniverseAudioSynth; @@ -4436,6 +4713,8 @@ class ExpandedSciFiAudioSynth { constructor(audioManager) { this.am = audioManager; this.gainNode = null; + this.medicalTimer = null; + this.sparkTimer = null; } init() { @@ -4809,6 +5088,81 @@ class ExpandedSciFiAudioSynth { }, 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) */ @@ -4924,15 +5278,1267 @@ class ExpandedSciFiAudioSynth { 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; + } + } + + stopAllLoops() { + this.stopMedicalMonitor(); + this.stopStationSparks(); + } } window.ExpandedSciFiAudioSynth = ExpandedSciFiAudioSynth; /** - * Canonical Starship & Location Sound Profiles Matrix - * Each preset defines the exact parameters for Hull Drone, Warp Core, Life Support, and Telemetry. + * 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; + +