From 057b155a61a2fa4b0dde6ccdb9a090eb8497a47a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 09:43:45 -0700 Subject: [PATCH] Regenerate standalone display from current source --- dist/SciFiAmbientDisplay_v4.html | 1132 ++++++++++++++++++++++++++++-- 1 file changed, 1071 insertions(+), 61 deletions(-) diff --git a/dist/SciFiAmbientDisplay_v4.html b/dist/SciFiAmbientDisplay_v4.html index e2fdce4..8a92537 100644 --- a/dist/SciFiAmbientDisplay_v4.html +++ b/dist/SciFiAmbientDisplay_v4.html @@ -3545,7 +3545,7 @@ class TelemetrySynth { 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' + era: 'tng', // 'tng', 'voyager', 'tos', 'ds9', 'nx', 'who', 'bioship', 'belter' reverbMix: 0.25 }; @@ -3611,6 +3611,15 @@ class TelemetrySynth { if (this.isMuted || this.params.volume <= 0.01 || !this.am.ctx) return; switch (this.params.era) { + case 'who': + this.synthesizeWhoMechanicalTelemetry_(); + break; + case 'bioship': + this.synthesizeBioshipOrganicTelemetry_(); + break; + case 'belter': + this.synthesizeBelterJuryRiggedTelemetry_(); + break; case 'tos': Math.random() > 0.4 ? this.synthesizeTOSWarble() : this.synthesizeTOSRelayClick(); break; @@ -3987,6 +3996,312 @@ class TelemetrySynth { osc2.stop(now + 0.7); } + /** + * Whoniverse Mechanical Telemetry Engine (audio_gaps.md #25, Phase 3 era) + * The TARDIS console never chirps like Starfleet gear: each tick emits a + * 0.25-0.6 s cluster of mechanical foley - dual-filtered toggle snaps, + * low 60-120 Hz spring-lever clicks and damped rotary selector pings + * (no electronic beeps). All transients scheduled on absolute ctx time so + * the single scheduler timeout can never orphan nodes. + */ + synthesizeWhoMechanicalTelemetry_() { + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const strikeCount = 2 + Math.floor(Math.random() * 4); // 2-5 transients + let cursor = Math.random() * 0.12; + + // Toggle snap: dual-filtered short noise pulse (20-45 ms body) + const toggleSnap = (t, peak) => { + const buf = this.am.createNoiseBuffer('white', 0.06); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bpLo = ctx.createBiquadFilter(); + bpLo.type = 'bandpass'; + bpLo.frequency.setValueAtTime(300 + Math.random() * 200, t); + bpLo.Q.setValueAtTime(6, t); + const bpHi = ctx.createBiquadFilter(); + bpHi.type = 'bandpass'; + bpHi.frequency.setValueAtTime(900 + Math.random() * 500, t); + bpHi.Q.setValueAtTime(4, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 0.004); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.02 + Math.random() * 0.025); + noise.connect(bpLo); + bpLo.connect(env); + noise.connect(bpHi); + bpHi.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.06); + }; + + // Spring lever snap: dull square click collapsing in pitch (60-120 Hz) + const springLever = (t, peak) => { + const osc = ctx.createOscillator(); + osc.type = 'square'; + const f0 = 60 + Math.random() * 60; + osc.frequency.setValueAtTime(f0, t); + osc.frequency.exponentialRampToValueAtTime(f0 * 0.5, t + 0.03); + const env = ctx.createGain(); + env.gain.setValueAtTime(peak, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.03 + Math.random() * 0.02); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.055); + }; + + // Rotary selector clunk: short damped mechanical ping, never a clean beep + const rotaryClunk = (t, peak) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + const f0 = 180 + Math.random() * 260; + osc.frequency.setValueAtTime(f0, t); + osc.frequency.exponentialRampToValueAtTime(f0 * 0.7, t + 0.06); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 0.003); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.06 + Math.random() * 0.08); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.16); + }; + + for (let i = 0; i < strikeCount; i++) { + const r = Math.random(); + if (r < 0.5) toggleSnap(cursor, 0.08 + Math.random() * 0.08); + else if (r < 0.8) springLever(cursor, 0.07 + Math.random() * 0.06); + else rotaryClunk(cursor, 0.09 + Math.random() * 0.07); + cursor += 0.04 + Math.random() * 0.14; + } + } + + /** + * Bioship Organic Telemetry Engine (audio_gaps.md #47, Phase 3 era) + * Living-ship telemetry is wet and pulse-driven, never electronic: vascular + * surges (30-90 Hz asymmetric sine thumps, sometimes lub-dub double beats), + * chitinous joint snaps, bioplasmic fluid pops and membrane flutters. + * One 0.5-1.1 s phrase of 2-4 events on absolute ctx time. + */ + synthesizeBioshipOrganicTelemetry_() { + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const eventCount = 2 + Math.floor(Math.random() * 3); // 2-4 events + let cursor = Math.random() * 0.2; + + const fireEvent = (t) => { + const r = Math.random(); + if (r < 0.35) { + // Vascular surge: asymmetric sine thump with pitch drop + lub-dub echo + const f0 = 30 + Math.random() * 60; + const dur = 0.2 + Math.random() * 0.15; + const surge = (st, peak, freq) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, st); + osc.frequency.linearRampToValueAtTime(freq * 0.82, st + dur * 0.7); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, st); + env.gain.linearRampToValueAtTime(peak, st + 0.06 + Math.random() * 0.05); + env.gain.exponentialRampToValueAtTime(0.001, st + dur); + osc.connect(env); + env.connect(this.gainNode); + osc.start(st); + osc.stop(st + dur + 0.05); + }; + surge(t, 0.14 + Math.random() * 0.06, f0); + if (Math.random() < 0.5) { + surge(t + 0.22 + Math.random() * 0.06, 0.09, f0 * 0.9); // lub-dub + } + } else if (r < 0.6) { + // Chitin snap: very short high-Q bandpass noise tick + const buf = this.am.createNoiseBuffer('white', 0.03); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(1200 + Math.random() * 1400, t); + bp.Q.setValueAtTime(10 + Math.random() * 4, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.12 + Math.random() * 0.06, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.008 + Math.random() * 0.01); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.03); + } else if (r < 0.85) { + // Bioplasmic fluid pop: bandpass wet impulse with resonant tail + const buf = this.am.createNoiseBuffer('pink', 0.08); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(500 + Math.random() * 900, t); + bp.frequency.exponentialRampToValueAtTime(380, t + 0.05); + bp.Q.setValueAtTime(3 + Math.random() * 2, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(0.14, t + 0.008); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.05); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.09); + } else { + // Membrane flutter: pink noise AM-stepped between 300-700 Hz + const buf = this.am.createNoiseBuffer('pink', 0.2); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(300 + Math.random() * 400, t); + bp.Q.setValueAtTime(2, t); + const env = ctx.createGain(); + const flutterRate = 18 + Math.random() * 12; + const flutterDur = 0.08 + Math.random() * 0.07; + env.gain.setValueAtTime(0.001, t); + let step = 0; + while (step < flutterDur) { + env.gain.linearRampToValueAtTime(0.09 + Math.random() * 0.05, t + step + (1 / flutterRate) * 0.6); + env.gain.linearRampToValueAtTime(0.001, t + step + (1 / flutterRate)); + step += 1 / flutterRate; + } + env.gain.setValueAtTime(0.001, t + flutterDur + 0.02); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + flutterDur + 0.05); + } + }; + + for (let i = 0; i < eventCount; i++) { + fireEvent(cursor); + cursor += 0.16 + Math.random() * 0.3; + } + } + + /** + * Belter Jury-Rigged Telemetry Engine (audio_gaps.md #29, Phase 3 era) + * Rattling relays, worn copper contactors and erratic voltage on Ceres / + * Tycho / Rocinante hardware: jittered square pulses with randomized + * micro-dropouts, contact-bounce double clicks, harsh metallic ticks and + * unstable clock pings. One 3-6 event volley on absolute ctx time, with an + * occasional late sub-cluster to mimic an unsteady clock. + */ + synthesizeBelterJuryRiggedTelemetry_() { + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + let cursor = 0.05 + Math.random() * 0.25; + + 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.15, t); + env.gain.setValueAtTime(0.001, t + 0.012 + Math.random() * 0.015); + env.gain.setValueAtTime(0.12, t + 0.026 + Math.random() * 0.02); + env.gain.setValueAtTime(0.001, t + 0.05 + Math.random() * 0.025); + env.gain.setValueAtTime(0.09, 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) => { + 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.12, 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.08, 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); + }; + + const clockPing = (t) => { + // Unstable clock: dulled square collapsing hard, like a dying oscillator + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(700 + Math.random() * 400, t); + osc.frequency.exponentialRampToValueAtTime(200, t + 0.025); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.05, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.04); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.045); + }; + + const fireEvent = (t) => { + const r = Math.random(); + if (r < 0.35) relayPulse(t); + else if (r < 0.6) contactBounce(t); + else if (r < 0.85) metallicTick(t); + else clockPing(t); + }; + + const eventCount = 3 + Math.floor(Math.random() * 4); // 3-6 events + for (let i = 0; i < eventCount; i++) { + fireEvent(cursor); + cursor += 0.06 + Math.random() * 0.16; // jittered clock drift + } + // Occasional second volley: the station clock skips then catches up + if (Math.random() < 0.35) { + const volleyBase = cursor + 0.1 + Math.random() * 0.4; + const volleyCount = 2 + Math.floor(Math.random() * 2); + for (let i = 0; i < volleyCount; i++) { + fireEvent(volleyBase + i * (0.04 + Math.random() * 0.1)); + } + } + } + applyPreset(config) { if (config.volume !== undefined) this.params.volume = config.volume; if (config.density !== undefined) this.setDensity(config.density); @@ -4291,7 +4606,6 @@ class WhoniverseAudioSynth { this.gainNode = null; this.activeCloister = false; this.cloisterTimer = null; - this.typeTelemetryTimer = null; } init() { @@ -4830,34 +5144,12 @@ class WhoniverseAudioSynth { } /** - * 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. + * Stops all automated Whoniverse ambient loops. Phase 3 moved console + * "type" telemetry (audio_gaps.md #25) into TelemetrySynth era 'who' + * routing, which is stopped via telemetry.stop() / FULL STOP. Hook + * retained because js/app.js calls it at three ambience switch points. */ stopAmbientLoops() { - this.stopConsoleTypeTelemetry(); } } @@ -6729,15 +7021,6 @@ class ExpandedSciFiAudioSynth { } } - /** - * 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 @@ -7679,9 +7962,702 @@ class ExpandedSciFiAudioSynth { }); } + /** + * Transporter Dematerialization / Rematerialization Cycle (audio_gaps.md #48) + * The classic shimmering beam-up wash: an additive array of 5 detuned sines + * (330-990 Hz over a 660 Hz base) each interlocked with its own 5-11 Hz AM + * LFO, plus a high bandpass noise shimmer gated by a 30 Hz LFO, all under a + * slow rise / mid dip / release master envelope. ~3.0 s. + */ + synthesizeTransporterCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 3.0; + + // Tonal shimmer master envelope (dip mid-cycle reads as the demat/remat split) + const masterEnv = ctx.createGain(); + masterEnv.gain.setValueAtTime(0.001, now); + masterEnv.gain.linearRampToValueAtTime(0.15, now + 0.9); + masterEnv.gain.setValueAtTime(0.1, now + 1.35); + masterEnv.gain.linearRampToValueAtTime(0.17, now + 1.95); + masterEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + masterEnv.connect(this.gainNode); + + const ratios = [0.5, 0.75, 1.0, 1.25, 1.5]; + ratios.forEach((ratio, idx) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(660 * ratio, now); + osc.detune.setValueAtTime((idx % 2 ? 1 : -1) * (1.5 + Math.random() * 4.5), now); + + // Per-voice interlocking amplitude modulation (staggered 5-11 Hz LFOs) + const amBase = 0.5 + Math.random() * 0.2; + const voiceEnv = ctx.createGain(); + voiceEnv.gain.setValueAtTime(amBase, now); + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(5 + idx * 1.5 + Math.random(), now); + const amDepth = ctx.createGain(); + amDepth.gain.setValueAtTime(amBase * 0.7, now); + lfo.connect(amDepth); + amDepth.connect(voiceEnv.gain); + + osc.connect(voiceEnv); + voiceEnv.connect(masterEnv); + osc.start(now); + osc.stop(now + duration + 0.1); + lfo.start(now); + lfo.stop(now + duration + 0.1); + }); + + // High-frequency energy shimmer (4-9 kHz) following a parallel envelope + const shimmerEnv = ctx.createGain(); + shimmerEnv.gain.setValueAtTime(0.001, now); + shimmerEnv.gain.linearRampToValueAtTime(0.05, now + 0.9); + shimmerEnv.gain.setValueAtTime(0.035, now + 1.35); + shimmerEnv.gain.linearRampToValueAtTime(0.055, now + 1.95); + shimmerEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + shimmerEnv.connect(this.gainNode); + + const shimmerBuf = this.am.createNoiseBuffer('white', duration + 0.2); + const shimmer = ctx.createBufferSource(); + shimmer.buffer = shimmerBuf; + [4500, 7800].forEach((center) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(center, now); + bp.Q.setValueAtTime(1.2, now); + const amEnv = ctx.createGain(); + amEnv.gain.setValueAtTime(center === 4500 ? 0.6 : 0.4, now); + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(30, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(amEnv.gain.value * 0.9, now); + lfo.connect(lfoDepth); + lfoDepth.connect(amEnv.gain); + shimmer.connect(bp); + bp.connect(amEnv); + amEnv.connect(shimmerEnv); + lfo.start(now); + lfo.stop(now + duration + 0.1); + }); + shimmer.start(now); + shimmer.stop(now + duration + 0.05); + } + + /** + * Point-Defense Flak Barrage Burst (audio_gaps.md #49) + * Staggered concussive clusters of 4-8 noise transients 10-30 ms apart over + * a 40 Hz sub-bass punch, with a couple of high shell-crack transients per + * volley. Each hit self-releases; whole burst ~0.9-1.4 s. + */ + synthesizeFlakBarrageBurst() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 0.9 + Math.random() * 0.5; + + // 40 Hz sub-bass punch at volley start + const sub = ctx.createOscillator(); + sub.type = 'triangle'; + sub.frequency.setValueAtTime(40, now); + sub.frequency.linearRampToValueAtTime(32, now + 0.25); + const subEnv = ctx.createGain(); + subEnv.gain.setValueAtTime(0.001, now); + subEnv.gain.linearRampToValueAtTime(0.3, now + 0.008); + subEnv.gain.exponentialRampToValueAtTime(0.0001, now + 0.3); + sub.connect(subEnv); + subEnv.connect(this.gainNode); + sub.start(now); + sub.stop(now + 0.32); + + const hitCount = 4 + Math.floor(Math.random() * 5); // 4-8 hits + let cursor = 0.05; + for (let i = 0; i < hitCount; i++) { + const t = now + cursor; + const alternate = i % 2 ? 0.75 : 1.0; // mono proxy for spread + const isCrack = i % 3 === 1; + const buf = this.am.createNoiseBuffer('white', 0.12); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + if (isCrack) { + bp.frequency.setValueAtTime(2500 + Math.random() * 1500, t); + bp.Q.setValueAtTime(8 + Math.random() * 4, t); + } else { + bp.frequency.setValueAtTime(300 + Math.random() * 1300, t); + bp.Q.setValueAtTime(2 + Math.random() * 3, t); + } + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime((isCrack ? 0.1 : 0.17 + Math.random() * 0.12) * alternate, t + 0.004); + env.gain.exponentialRampToValueAtTime(0.001, t + (isCrack ? 0.015 : 0.03 + Math.random() * 0.04)); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.13); + cursor += 0.01 + Math.random() * 0.02; // 10-30 ms stagger + } + + // Second soft sub thump mid-volley (distant batteries) + const sub2 = ctx.createOscillator(); + sub2.type = 'triangle'; + sub2.frequency.setValueAtTime(38, now + 0.42); + sub2.frequency.linearRampToValueAtTime(30, now + 0.6); + const sub2Env = ctx.createGain(); + sub2Env.gain.setValueAtTime(0.001, now + 0.42); + sub2Env.gain.linearRampToValueAtTime(0.16, now + 0.43); + sub2Env.gain.exponentialRampToValueAtTime(0.0001, now + 0.72); + sub2.connect(sub2Env); + sub2Env.connect(this.gainNode); + sub2.start(now + 0.42); + sub2.stop(now + 0.74); + } + + /** + * Heavy Weapons Breech Locking Clank (audio_gags.md #50) + * Three-stage loading cycle: 80 ms pre-charge pneumatic hiss, 60 Hz square + * breech slam into a lowpass body, and a high-Q steel ringoff with damped + * inharmonic partials. ~1.6 s. + */ + synthesizeBreechClank() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // Stage 1: pneumatic pre-charge hiss (80 ms, highpass 2.5 kHz) + const hissBuf = this.am.createNoiseBuffer('white', 0.14); + const hiss = ctx.createBufferSource(); + hiss.buffer = hissBuf; + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(2500, now); + hp.Q.setValueAtTime(0.7, now); + const hissEnv = ctx.createGain(); + hissEnv.gain.setValueAtTime(0.001, now); + hissEnv.gain.linearRampToValueAtTime(0.14, now + 0.005); + hissEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.08); + hiss.connect(hp); + hp.connect(hissEnv); + hissEnv.connect(this.gainNode); + hiss.start(now); + hiss.stop(now + 0.1); + + // Stage 2: breech slam - 60 Hz square impact into a lowpass body + const slam = ctx.createOscillator(); + slam.type = 'square'; + slam.frequency.setValueAtTime(60, now + 0.08); + slam.frequency.linearRampToValueAtTime(48, now + 0.2); + const slamLp = ctx.createBiquadFilter(); + slamLp.type = 'lowpass'; + slamLp.frequency.setValueAtTime(300, now + 0.08); + const slamEnv = ctx.createGain(); + slamEnv.gain.setValueAtTime(0.001, now + 0.08); + slamEnv.gain.linearRampToValueAtTime(0.4, now + 0.086); + slamEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.28); + slam.connect(slamLp); + slamLp.connect(slamEnv); + slamEnv.connect(this.gainNode); + slam.start(now + 0.08); + slam.stop(now + 0.3); + + // Stage 3: high-Q steel ringoff with damped inharmonic partials + const ringBuf = this.am.createNoiseBuffer('white', 0.45); + const ring = ctx.createBufferSource(); + ring.buffer = ringBuf; + const ringBp = ctx.createBiquadFilter(); + ringBp.type = 'bandpass'; + ringBp.frequency.setValueAtTime(1800 + Math.random() * 1600, now + 0.09); + ringBp.Q.setValueAtTime(18 + Math.random() * 7, now + 0.09); + const ringEnv = ctx.createGain(); + ringEnv.gain.setValueAtTime(0.001, now + 0.09); + ringEnv.gain.linearRampToValueAtTime(0.16, now + 0.095); + ringEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.5); + ring.connect(ringBp); + ringBp.connect(ringEnv); + ringEnv.connect(this.gainNode); + ring.start(now + 0.09); + ring.stop(now + 0.52); + + [240, 630, 1120].forEach((freq, idx) => { + const partial = ctx.createOscillator(); + partial.type = 'sine'; + partial.frequency.setValueAtTime(freq, now + 0.09); + const pEnv = ctx.createGain(); + pEnv.gain.setValueAtTime(0.001, now + 0.09); + pEnv.gain.linearRampToValueAtTime([0.11, 0.07, 0.045][idx], now + 0.095); + pEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.09 + [0.9, 0.6, 0.35][idx]); + partial.connect(pEnv); + pEnv.connect(this.gainNode); + partial.start(now + 0.09); + partial.stop(now + [1.0, 0.7, 0.45][idx]); + }); + } + + /** + * Heavy Hydraulic Docking Groan & Mechanical Clang (audio_gaps.md #52) + * A slow ~2.4 s structural stress groan: brown noise through two parallel + * resonant bandpasses sweeping 38->62 Hz and 55->85 Hz under an asymmetric + * groaning envelope, resolving into a metallic anvil transient with an + * inharmonic sine stack at the moment the clamp seats. ~4.0 s. + */ + synthesizeDockingGroan() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const groanDur = 3.9; + + // Brown-noise stress bed through two sweeping resonant bandpasses + const bedBuf = this.am.createNoiseBuffer('brown', groanDur + 0.2); + const bed = ctx.createBufferSource(); + bed.buffer = bedBuf; + const groanEnv = ctx.createGain(); + groanEnv.gain.setValueAtTime(0.0001, now); + groanEnv.gain.linearRampToValueAtTime(0.001, now + 0.01); + groanEnv.gain.linearRampToValueAtTime(0.22, now + 1.1); // groan up + groanEnv.gain.setValueAtTime(0.1, now + 1.6); // stress dip + groanEnv.gain.linearRampToValueAtTime(0.26, now + 2.3); // groan peaks + groanEnv.gain.exponentialRampToValueAtTime(0.0001, now + groanDur); + groanEnv.connect(this.gainNode); + + [38, 55].forEach((startFreq, idx) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(startFreq, now); + bp.frequency.linearRampToValueAtTime(idx === 0 ? 62 : 85, now + 2.4); + bp.Q.setValueAtTime(idx === 0 ? 5.5 : 4.5, now); + bed.connect(bp); + bp.connect(groanEnv); + }); + bed.start(now); + bed.stop(now + groanDur + 0.05); + + // Metallic anvil clang as the groan resolves (~2.7 s in) + const clangT = now + 2.7; + const ringBuf = this.am.createNoiseBuffer('white', 0.4); + const ring = ctx.createBufferSource(); + ring.buffer = ringBuf; + const ringBp = ctx.createBiquadFilter(); + ringBp.type = 'bandpass'; + ringBp.frequency.setValueAtTime(2000, clangT); + ringBp.Q.setValueAtTime(20, clangT); + const ringEnv = ctx.createGain(); + ringEnv.gain.setValueAtTime(0.001, clangT); + ringEnv.gain.linearRampToValueAtTime(0.16, clangT + 0.004); + ringEnv.gain.exponentialRampToValueAtTime(0.001, clangT + 0.35); + ring.connect(ringBp); + ringBp.connect(ringEnv); + ringEnv.connect(this.gainNode); + ring.start(clangT); + ring.stop(clangT + 0.4); + + [420, 880, 1600].forEach((freq, idx) => { + const partial = ctx.createOscillator(); + partial.type = 'sine'; + partial.frequency.setValueAtTime(freq, clangT); + const pEnv = ctx.createGain(); + pEnv.gain.setValueAtTime(0.001, clangT); + pEnv.gain.linearRampToValueAtTime([0.08, 0.06, 0.04][idx], clangT + 0.005); + pEnv.gain.exponentialRampToValueAtTime(0.001, clangT + [0.8, 0.55, 0.3][idx]); + partial.connect(pEnv); + pEnv.connect(this.gainNode); + partial.start(clangT); + partial.stop(clangT + [0.85, 0.6, 0.35][idx]); + }); + } + + /** + * Dual-Reel Magnetic Tape Spooler & Pinch Roller Clank (audio_gaps.md #53) + * Solenoid pinch-roller engagement (110 Hz impulse), a fluttering pink tape + * bed (15 Hz AM on a bandpass noise loop) and a faint 3200 Hz capstan hum + * with spool-up glide, ending in an optional fast-forward whine tail. + * ~2.6-3.5 s. + */ + synthesizeTapeSpooler() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 2.6 + Math.random() * 0.9; + + // Solenoid clamp impulse (110 Hz) with a tiny mechanical tick + const solenoid = ctx.createOscillator(); + solenoid.type = 'square'; + solenoid.frequency.setValueAtTime(110, now); + solenoid.frequency.linearRampToValueAtTime(95, now + 0.05); + const solEnv = ctx.createGain(); + solEnv.gain.setValueAtTime(0.001, now); + solEnv.gain.linearRampToValueAtTime(0.22, now + 0.004); + solEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.07); + solenoid.connect(solEnv); + solEnv.connect(this.gainNode); + solenoid.start(now); + solenoid.stop(now + 0.09); + + const tickBuf = this.am.createNoiseBuffer('white', 0.03); + const tick = ctx.createBufferSource(); + tick.buffer = tickBuf; + const tickBp = ctx.createBiquadFilter(); + tickBp.type = 'bandpass'; + tickBp.frequency.setValueAtTime(1800, now + 0.02); + tickBp.Q.setValueAtTime(10, now + 0.02); + const tickEnv = ctx.createGain(); + tickEnv.gain.setValueAtTime(0.08, now + 0.02); + tickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.045); + tick.connect(tickBp); + tickBp.connect(tickEnv); + tickEnv.connect(this.gainNode); + tick.start(now + 0.02); + tick.stop(now + 0.06); + + // Fluttering pink-noise tape bed (15 Hz flutter AM) + const tapeBuf = this.am.createNoiseBuffer('pink', duration + 0.3); + const tape = ctx.createBufferSource(); + tape.buffer = tapeBuf; + tape.loop = true; + const tapeBp = ctx.createBiquadFilter(); + tapeBp.type = 'bandpass'; + tapeBp.frequency.setValueAtTime(500 + Math.random() * 1000, now); + tapeBp.Q.setValueAtTime(1, now); + const tapeEnv = ctx.createGain(); + tapeEnv.gain.setValueAtTime(0.0001, now); + tapeEnv.gain.linearRampToValueAtTime(0.05, now + 0.15); + tapeEnv.gain.setValueAtTime(0.03, now + duration * 0.45); + tapeEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + const flutter = ctx.createOscillator(); + flutter.type = 'sine'; + flutter.frequency.setValueAtTime(15, now); + const flutterDepth = ctx.createGain(); + flutterDepth.gain.setValueAtTime(0.035, now); + flutter.connect(flutterDepth); + flutterDepth.connect(tapeEnv.gain); + tape.connect(tapeBp); + tapeBp.connect(tapeEnv); + tapeEnv.connect(this.gainNode); + tape.start(now); + tape.stop(now + duration + 0.05); + flutter.start(now); + flutter.stop(now + duration + 0.05); + + // Capstan hum: faint 3200 Hz sine with a slow spool-up glide and wow + const capstan = ctx.createOscillator(); + capstan.type = 'sine'; + capstan.frequency.setValueAtTime(3136, now); + capstan.frequency.exponentialRampToValueAtTime(3264, now + 1.5); + const capstanEnv = ctx.createGain(); + capstanEnv.gain.setValueAtTime(0.0001, now); + capstanEnv.gain.linearRampToValueAtTime(0.016, now + 0.3); + capstanEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + const wow = ctx.createOscillator(); + wow.type = 'sine'; + wow.frequency.setValueAtTime(8, now); + const wowDepth = ctx.createGain(); + wowDepth.gain.setValueAtTime(12, now); + wow.connect(wowDepth); + wowDepth.connect(capstan.frequency); + capstan.connect(capstanEnv); + capstanEnv.connect(this.gainNode); + capstan.start(now); + capstan.stop(now + duration + 0.1); + wow.start(now); + wow.stop(now + duration + 0.1); + + // Occasional fast-forward whine tail + if (Math.random() < 0.3) { + const whineT = now + duration - 0.55; + const whine = ctx.createOscillator(); + whine.type = 'sine'; + whine.frequency.setValueAtTime(1400, whineT); + whine.frequency.exponentialRampToValueAtTime(2300, whineT + 0.5); + const whineEnv = ctx.createGain(); + whineEnv.gain.setValueAtTime(0.0001, whineT); + whineEnv.gain.linearRampToValueAtTime(0.02, whineT + 0.08); + whineEnv.gain.exponentialRampToValueAtTime(0.0001, whineT + 0.5); + whine.connect(whineEnv); + whineEnv.connect(this.gainNode); + whine.start(whineT); + whine.stop(whineT + 0.55); + } + } + + /** + * Dot-Matrix / Teletype Chatter Phrase (audio_gaps.md #51, one phrase unit) + * A burst of 6-16 pseudo-random high-Q ~4 kHz impact spikes with periodic + * 12 Hz stepper clicks, phrased with a soft attack/release. The continuous + * Nostromo terminal loop is startTeletypeChatter() below. + */ + synthesizeTeletypeChatterPhrase() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const phraseDur = 0.6 + Math.random() * 0.8; + + const masterEnv = ctx.createGain(); + masterEnv.gain.setValueAtTime(0.0001, now); + masterEnv.gain.linearRampToValueAtTime(1, now + 0.03); // soft phrase in + masterEnv.gain.setValueAtTime(1, now + phraseDur - 0.06); + masterEnv.gain.exponentialRampToValueAtTime(0.0001, now + phraseDur + 0.05); + masterEnv.connect(this.gainNode); + + const strikes = 6 + Math.floor(Math.random() * 11); // 6-16 spikes + let cursor = 0.02; + for (let i = 0; i < strikes; i++) { + if (cursor >= phraseDur - 0.1) break; // never schedule past the phrase out + const t = now + cursor; + const buf = this.am.createNoiseBuffer('white', 0.03); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(3400 + Math.random() * 1200, t); + bp.Q.setValueAtTime(10 + Math.random() * 6, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.1 + Math.random() * 0.1, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.012 + Math.random() * 0.01); + noise.connect(bp); + bp.connect(env); + env.connect(masterEnv); + noise.start(t); + noise.stop(t + 0.035); + + // Periodic 12 Hz carriage stepper click + if (i % 3 === 2) { + const stepper = ctx.createOscillator(); + stepper.type = 'square'; + stepper.frequency.setValueAtTime(90, t); + stepper.frequency.linearRampToValueAtTime(70, t + 0.02); + const sEnv = ctx.createGain(); + sEnv.gain.setValueAtTime(0.06, t); + sEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.025); + stepper.connect(sEnv); + sEnv.connect(masterEnv); + stepper.start(t); + stepper.stop(t + 0.03); + } + cursor += 0.045 + Math.random() * 0.095; // jittered 45-140 ms strikes + } + } + + /** + * Ambient Loop: Continuous Nostromo terminal teletype chatter (#51) + * Dense overlapping "typed line" phrases at a 0.9-2.0 s cadence keep the + * effect continuous while the preset runs. Dies with the loop registry. + */ + startTeletypeChatter(minIntervalMs = 900, maxIntervalMs = 2000) { + this._scheduleLoop('teletypeChatter', minIntervalMs, maxIntervalMs, () => { + this.synthesizeTeletypeChatterPhrase(); + }); + } + + /** + * Continuous Heterodyne / Theremin Navigational Drone (audio_gaps.md #54) + * A persistent room-tone element for the Jupiter 2 astrogator deck: two + * detuned sines (~1040 Hz / ~1048 Hz) beating to ~8 Hz, each with a very + * slow independent wander LFO so the beat drifts like a vacuum-tube theremin. + * True node-graph bed - long 3 s attack, room-tone level, gentle 1.8 s + * release on stop. Re-start always fades the previous bed out first. + */ + startHeterodyneDrone() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + this.stopHeterodyneDrone(); + + const now = ctx.currentTime; + const level = 0.06; + const masterGain = ctx.createGain(); + masterGain.gain.setValueAtTime(0.0001, now); + masterGain.gain.linearRampToValueAtTime(level, now + 3.0); + masterGain.connect(this.gainNode); + + const sources = []; + [1040, 1048].forEach((baseFreq, idx) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(baseFreq, now); + // Slow independent drift (0.04-0.09 Hz) keeps the beat wandering + const wander = ctx.createOscillator(); + wander.type = 'sine'; + wander.frequency.setValueAtTime(idx === 0 ? 0.04 + Math.random() * 0.02 : 0.07 + Math.random() * 0.02, now); + const wanderDepth = ctx.createGain(); + wanderDepth.gain.setValueAtTime(idx === 0 ? 4 : 3, now); + wander.connect(wanderDepth); + wanderDepth.connect(osc.frequency); + + const voiceGain = ctx.createGain(); + voiceGain.gain.setValueAtTime(0.5, now); + osc.connect(voiceGain); + voiceGain.connect(masterGain); + osc.start(now); + wander.start(now); + sources.push(osc, wander); + }); + + this.heterodyneDroneActive = true; + this.heterodyneDroneGain = masterGain; + this.heterodyneDroneSources = sources; + } + + stopHeterodyneDrone() { + this.heterodyneDroneActive = false; + const gain = this.heterodyneDroneGain; + if (!gain) return; + const ctx = this.am.ctx; + if (ctx) { + const now = ctx.currentTime; + // linearRamp continues from the computed value, so a mid-attack stop + // glides down smoothly without a click + gain.gain.linearRampToValueAtTime(0.0001, now + 1.8); + (this.heterodyneDroneSources || []).forEach((src) => { + try { src.stop(now + 2.0); } catch (e) { /* already stopped */ } + }); + } + this.heterodyneDroneGain = null; + this.heterodyneDroneSources = null; + } + + /** + * Zocalo Commerce-Plaza Chatter Bed (audio_gaps.md #46) + * A persistent crowd-murmur bed for Babylon 5: looped pink noise through + * four parallel formant bandpasses (300 Hz - 3 kHz) whose centers drift at + * 0.03-0.11 Hz, plus an accent layer of vowelish murmur blips scheduled on + * the loop registry. Re-start fades any previous bed out first. + */ + startZocaloChatter() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + this.stopZocaloChatter(); + + const now = ctx.currentTime; + const bedBuf = this.am.createNoiseBuffer('pink', 8); + const bed = ctx.createBufferSource(); + bed.buffer = bedBuf; + bed.loop = true; + + const masterGain = ctx.createGain(); + masterGain.gain.setValueAtTime(0.0001, now); + masterGain.gain.linearRampToValueAtTime(0.16, now + 2.5); + masterGain.connect(this.gainNode); + + // Formant filter bank with slow independent center drift + const sources = []; + const formants = [ + { center: 330, q: 6, weight: 0.3, driftHz: 55, driftRate: 0.03 + Math.random() * 0.02 }, + { center: 760, q: 5, weight: 0.24, driftHz: 130, driftRate: 0.05 + Math.random() * 0.03 }, + { center: 1500, q: 4, weight: 0.16, driftHz: 260, driftRate: 0.07 + Math.random() * 0.03 }, + { center: 2600, q: 3, weight: 0.08, driftHz: 480, driftRate: 0.09 + Math.random() * 0.02 } + ]; + formants.forEach((f) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(f.center, now); + bp.Q.setValueAtTime(f.q, now); + const drift = ctx.createOscillator(); + drift.type = 'sine'; + drift.frequency.setValueAtTime(f.driftRate, now); + const driftDepth = ctx.createGain(); + driftDepth.gain.setValueAtTime(f.driftHz, now); + drift.connect(driftDepth); + driftDepth.connect(bp.frequency); + const bandGain = ctx.createGain(); + bandGain.gain.setValueAtTime(f.weight, now); + bed.connect(bp); + bp.connect(bandGain); + bandGain.connect(masterGain); + drift.start(now); + sources.push(drift); + }); + + bed.start(now); + sources.push(bed); + + // Crowd accents: murmur blips layered over the wash + this._scheduleLoop('zocaloAccents', 2400, 6800, () => { + this.synthesizeZocaloAccent_(); + }); + + this.zocaloChatterActive = true; + this.zocaloChatterGain = masterGain; + this.zocaloChatterSources = sources; + } + + stopZocaloChatter() { + this.zocaloChatterActive = false; + this._stopLoop('zocaloAccents'); + const gain = this.zocaloChatterGain; + if (!gain) return; + const ctx = this.am.ctx; + if (ctx) { + const now = ctx.currentTime; + gain.gain.linearRampToValueAtTime(0.0001, now + 2.0); + (this.zocaloChatterSources || []).forEach((src) => { + try { src.stop(now + 2.2); } catch (e) { /* already stopped */ } + }); + } + this.zocaloChatterGain = null; + this.zocaloChatterSources = null; + } + + /** + * One crowd "voice" blip for the zocalo bed: a short formant-filtered noise + * impulse with a slight pitch glide, sometimes doubled. Self-releasing. + */ + synthesizeZocaloAccent_() { + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const count = Math.random() < 0.3 ? 2 : 1; + let cursor = 0; + for (let i = 0; i < count; i++) { + const t = now + cursor; + const center = 380 + Math.random() * 1500; + const glide = Math.random() < 0.4 ? (40 + Math.random() * 90) : -(30 + Math.random() * 60); + const dur = 0.1 + Math.random() * 0.15; + const buf = this.am.createNoiseBuffer('pink', dur + 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(center, t); + bp.frequency.linearRampToValueAtTime(Math.max(200, center + glide), t + dur); + bp.Q.setValueAtTime(2.2 + Math.random() * 1.5, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(0.05 + Math.random() * 0.04, t + 0.03); + env.gain.exponentialRampToValueAtTime(0.001, t + dur); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + dur + 0.05); + cursor += 0.09 + Math.random() * 0.15; + } + } + stopAllLoops() { this.stopMedicalMonitor(); this.stopStationSparks(); + this.stopZocaloChatter(); + this.stopHeterodyneDrone(); Object.keys(this.loopTimers).forEach((key) => { this._stopLoop(key); }); @@ -8280,6 +9256,7 @@ const UniverseRegistry = { { id: 'btn-cardassian-door', label: 'CARDASSIAN BULKHEAD DOOR' }, { id: 'btn-turbolift', label: 'TURBOLIFT WHOOSH' }, { id: 'btn-replicator', label: 'REPLICATOR SHIMMER' }, + { id: 'btn-transporter', label: 'TRANSPORTER CYCLE' }, { id: 'btn-chirp-single', label: 'LCARS SINGLE CHIRP' } ], presets: StarshipPresets @@ -8322,7 +9299,7 @@ const UniverseRegistry = { hull: { volume: 0.5, baseFreq: 68, filterCutoff: 150, resonance: 2.8, noiseMix: 0.35, harmonicSpread: 1.04 }, warp: { volume: 0.65, bpm: 52, carrierFreq: 96, filterCutoff: 210, pulseShape: 'tos', resonance: 3.5, swirlMix: 0.2 }, lifeSupport: { volume: 0.45, noiseType: 'pink', highpassFreq: 220, lowpassFreq: 1800, airflowModSpeed: 0.08, airflowModDepth: 0.06 }, - telemetry: { volume: 0.55, density: 0.6, era: 'tos' } + telemetry: { volume: 0.55, density: 0.6, era: 'who' } }, 'tardis-victorian': { id: 'tardis-victorian', @@ -8334,7 +9311,7 @@ const UniverseRegistry = { hull: { volume: 0.65, baseFreq: 44, filterCutoff: 85, resonance: 1.8, noiseMix: 0.45, harmonicSpread: 1.015 }, warp: { volume: 0.3, bpm: 40, carrierFreq: 50, filterCutoff: 120, pulseShape: 'nx', resonance: 2.0, swirlMix: 0.15 }, lifeSupport: { volume: 0.55, noiseType: 'brown', highpassFreq: 110, lowpassFreq: 1200, airflowModSpeed: 0.07, airflowModDepth: 0.12 }, - telemetry: { volume: 0.35, density: 0.4, era: 'nx' } + telemetry: { volume: 0.35, density: 0.4, era: 'who' } }, 'tardis-coral': { id: 'tardis-coral', @@ -8346,7 +9323,7 @@ const UniverseRegistry = { hull: { volume: 0.75, baseFreq: 52, filterCutoff: 135, resonance: 3.2, noiseMix: 0.5, harmonicSpread: 1.03 }, warp: { volume: 0.9, bpm: 48, carrierFreq: 64, filterCutoff: 230, pulseShape: 'tng', resonance: 4.0, swirlMix: 0.45 }, lifeSupport: { volume: 0.6, noiseType: 'pink', highpassFreq: 160, lowpassFreq: 1700, airflowModSpeed: 0.18, airflowModDepth: 0.15 }, - telemetry: { volume: 0.45, density: 0.5, era: 'tng' } + telemetry: { volume: 0.45, density: 0.5, era: 'who' } }, 'tardis-copper': { id: 'tardis-copper', @@ -8358,7 +9335,7 @@ const UniverseRegistry = { hull: { volume: 0.6, baseFreq: 56, filterCutoff: 125, resonance: 2.5, noiseMix: 0.4, harmonicSpread: 1.02 }, warp: { volume: 0.7, bpm: 62, carrierFreq: 72, filterCutoff: 210, pulseShape: 'voyager', resonance: 3.6, swirlMix: 0.35 }, lifeSupport: { volume: 0.65, noiseType: 'white', highpassFreq: 220, lowpassFreq: 2200, airflowModSpeed: 0.15, airflowModDepth: 0.1 }, - telemetry: { volume: 0.55, density: 0.7, era: 'voyager' } + telemetry: { volume: 0.55, density: 0.7, era: 'who' } }, 'tardis-cold-machine': { id: 'tardis-cold-machine', @@ -8370,7 +9347,7 @@ const UniverseRegistry = { hull: { volume: 0.6, baseFreq: 50, filterCutoff: 110, resonance: 2.2, noiseMix: 0.38, harmonicSpread: 1.02 }, warp: { volume: 0.75, bpm: 56, carrierFreq: 68, filterCutoff: 190, pulseShape: 'tng', resonance: 3.2, swirlMix: 0.3 }, lifeSupport: { volume: 0.5, noiseType: 'pink', highpassFreq: 200, lowpassFreq: 1600, airflowModSpeed: 0.12, airflowModDepth: 0.08 }, - telemetry: { volume: 0.4, density: 0.5, era: 'ds9' } + telemetry: { volume: 0.4, density: 0.5, era: 'who' } }, 'tardis-crystal': { id: 'tardis-crystal', @@ -8382,7 +9359,7 @@ const UniverseRegistry = { hull: { volume: 0.7, baseFreq: 46, filterCutoff: 100, resonance: 3.5, noiseMix: 0.5, harmonicSpread: 1.04 }, warp: { volume: 0.8, bpm: 44, carrierFreq: 88, filterCutoff: 260, pulseShape: 'defiant', resonance: 4.4, swirlMix: 0.4 }, lifeSupport: { volume: 0.55, noiseType: 'pink', highpassFreq: 180, lowpassFreq: 1900, airflowModSpeed: 0.16, airflowModDepth: 0.14 }, - telemetry: { volume: 0.5, density: 0.6, era: 'voyager' } + telemetry: { volume: 0.5, density: 0.6, era: 'who' } }, 'tardis-infinite-white': { id: 'tardis-infinite-white', @@ -8394,7 +9371,7 @@ const UniverseRegistry = { hull: { volume: 0.65, baseFreq: 48, filterCutoff: 95, resonance: 2.0, noiseMix: 0.42, harmonicSpread: 1.01 }, warp: { volume: 0.6, bpm: 50, carrierFreq: 60, filterCutoff: 160, pulseShape: 'tng', resonance: 2.8, swirlMix: 0.3 }, lifeSupport: { volume: 0.7, noiseType: 'white', highpassFreq: 160, lowpassFreq: 2600, airflowModSpeed: 0.14, airflowModDepth: 0.12 }, - telemetry: { volume: 0.45, density: 0.6, era: 'tng' } + telemetry: { volume: 0.45, density: 0.6, era: 'who' } } } }, @@ -8421,7 +9398,9 @@ const UniverseRegistry = { { id: 'btn-pipe-drips', label: 'CONDENSATION DRIPS' }, { id: 'btn-steam-vent', label: 'BOILER VENT HISS' }, { id: 'btn-crash-couch', label: 'CRASH-COUCH GIMBAL' }, - { id: 'btn-air-handler', label: 'HVAC AIR HANDLER' } + { id: 'btn-air-handler', label: 'HVAC AIR HANDLER' }, + { id: 'btn-dock-groan', label: 'DOCKING GROAN & CLANG' }, + { id: 'btn-teletype', label: 'DOT-MATRIX TELETYPE' } ], presets: { 'ind-nostromo': { @@ -8482,7 +9461,7 @@ const UniverseRegistry = { hull: { volume: 0.8, baseFreq: 64, filterCutoff: 145, resonance: 3.4, noiseMix: 0.55, harmonicSpread: 1.035 }, warp: { volume: 0.9, bpm: 58, carrierFreq: 84, filterCutoff: 250, pulseShape: 'defiant', resonance: 4.2, swirlMix: 0.4 }, lifeSupport: { volume: 0.6, noiseType: 'white', highpassFreq: 200, lowpassFreq: 2100, airflowModSpeed: 0.18, airflowModDepth: 0.14 }, - telemetry: { volume: 0.5, density: 0.65, era: 'voyager' } + telemetry: { volume: 0.5, density: 0.75, era: 'belter' } }, 'ind-eagle': { id: 'ind-eagle', @@ -8540,7 +9519,7 @@ const UniverseRegistry = { hull: { volume: 0.75, baseFreq: 45, filterCutoff: 110, resonance: 3.2, noiseMix: 0.5, harmonicSpread: 1.03 }, warp: { volume: 0.85, bpm: 38, carrierFreq: 58, filterCutoff: 180, pulseShape: 'tng', resonance: 4.2, swirlMix: 0.45 }, lifeSupport: { volume: 0.65, noiseType: 'pink', highpassFreq: 150, lowpassFreq: 1600, airflowModSpeed: 0.08, airflowModDepth: 0.2 }, - telemetry: { volume: 0.4, density: 0.4, era: 'tng' } + telemetry: { volume: 0.4, density: 0.4, era: 'bioship' } }, 'bio-talyn': { id: 'bio-talyn', @@ -8550,7 +9529,7 @@ const UniverseRegistry = { hull: { volume: 0.8, baseFreq: 58, filterCutoff: 140, resonance: 3.6, noiseMix: 0.52, harmonicSpread: 1.04 }, warp: { volume: 0.9, bpm: 64, carrierFreq: 76, filterCutoff: 240, pulseShape: 'defiant', resonance: 4.6, swirlMix: 0.4 }, lifeSupport: { volume: 0.55, noiseType: 'pink', highpassFreq: 180, lowpassFreq: 1800, airflowModSpeed: 0.16, airflowModDepth: 0.16 }, - telemetry: { volume: 0.45, density: 0.5, era: 'ds9' } + telemetry: { volume: 0.45, density: 0.5, era: 'bioship' } }, 'bio-lexx': { id: 'bio-lexx', @@ -8560,7 +9539,7 @@ const UniverseRegistry = { hull: { volume: 0.85, baseFreq: 38, filterCutoff: 90, resonance: 2.8, noiseMix: 0.6, harmonicSpread: 1.02 }, warp: { volume: 0.7, bpm: 34, carrierFreq: 48, filterCutoff: 140, pulseShape: 'nx', resonance: 3.4, swirlMix: 0.35 }, lifeSupport: { volume: 0.6, noiseType: 'brown', highpassFreq: 120, lowpassFreq: 1300, airflowModSpeed: 0.06, airflowModDepth: 0.22 }, - telemetry: { volume: 0.35, density: 0.3, era: 'nx' } + telemetry: { volume: 0.35, density: 0.4, era: 'bioship' } }, 'bio-vorlon': { id: 'bio-vorlon', @@ -8570,7 +9549,7 @@ const UniverseRegistry = { hull: { volume: 0.65, baseFreq: 74, filterCutoff: 160, resonance: 3.8, noiseMix: 0.35, harmonicSpread: 1.05 }, warp: { volume: 0.8, bpm: 52, carrierFreq: 104, filterCutoff: 260, pulseShape: 'voyager', resonance: 4.5, swirlMix: 0.5 }, lifeSupport: { volume: 0.5, noiseType: 'pink', highpassFreq: 220, lowpassFreq: 2200, airflowModSpeed: 0.12, airflowModDepth: 0.1 }, - telemetry: { volume: 0.5, density: 0.6, era: 'voyager' } + telemetry: { volume: 0.5, density: 0.6, era: 'bioship' } }, 'bio-wraith': { id: 'bio-wraith', @@ -8580,7 +9559,7 @@ const UniverseRegistry = { hull: { volume: 0.85, baseFreq: 34, filterCutoff: 85, resonance: 2.5, noiseMix: 0.65, harmonicSpread: 1.015 }, warp: { volume: 0.6, bpm: 32, carrierFreq: 44, filterCutoff: 120, pulseShape: 'nx', resonance: 3.0, swirlMix: 0.25 }, lifeSupport: { volume: 0.65, noiseType: 'brown', highpassFreq: 90, lowpassFreq: 900, airflowModSpeed: 0.06, airflowModDepth: 0.22 }, - telemetry: { volume: 0.3, density: 0.25, era: 'nx' } + telemetry: { volume: 0.3, density: 0.4, era: 'bioship' } }, 'bio-species-8472': { id: 'bio-species-8472', @@ -8590,7 +9569,7 @@ const UniverseRegistry = { hull: { volume: 0.7, baseFreq: 68, filterCutoff: 150, resonance: 3.5, noiseMix: 0.45, harmonicSpread: 1.035 }, warp: { volume: 0.85, bpm: 56, carrierFreq: 88, filterCutoff: 230, pulseShape: 'defiant', resonance: 4.4, swirlMix: 0.45 }, lifeSupport: { volume: 0.5, noiseType: 'pink', highpassFreq: 200, lowpassFreq: 1900, airflowModSpeed: 0.14, airflowModDepth: 0.12 }, - telemetry: { volume: 0.45, density: 0.55, era: 'voyager' } + telemetry: { volume: 0.45, density: 0.55, era: 'bioship' } } } }, @@ -8617,7 +9596,9 @@ const UniverseRegistry = { { id: 'btn-cygnus-chug', label: 'STEAM PISTON CHUG' }, { id: 'btn-chirp-single', label: 'ASTROGATOR BEAT' }, { id: 'btn-chirp-sweep', label: 'ANALOG GLISSANDO' }, - { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' } + { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' }, + { id: 'btn-tape-spooler', label: 'DUAL-REEL TAPE SPOOLER' }, + { id: 'btn-hetero-drone', label: 'HETERO-DRONE TONE' } ], presets: { 'ret-jupiter-2': { @@ -8725,7 +9706,8 @@ const UniverseRegistry = { { id: 'btn-dock-clamp', label: 'BULKHEAD SEAL' }, { id: 'btn-carousel-groan', label: 'CAROUSEL MOTOR GROAN' }, { id: 'btn-slipstream', label: 'SLIPSTREAM SURGE' }, - { id: 'btn-oxygen-reg', label: 'O2 DEMAND VALVE' } + { id: 'btn-oxygen-reg', label: 'O2 DEMAND VALVE' }, + { id: 'btn-breech-clank', label: 'WEAPONS BREECH CLANK' } ], presets: { 'mil-sulaco': { @@ -9009,6 +9991,7 @@ const UniverseRegistry = { { id: 'btn-commlock', label: 'COMMLOCK CALLING TONE' }, { id: 'btn-sevas-spark', label: 'CONDUIT SPARK ARC' }, { id: 'btn-station-ecg', label: 'CLINICAL VITAL (ECG)' }, + { id: 'btn-zocalo-chatter', label: 'ZOCALO PLAZA CHATTER' }, { id: 'btn-belter-telemetry', label: 'BELTER JURY-RIG TELEMETRY', span: 2 } ], presets: { @@ -9050,7 +10033,7 @@ const UniverseRegistry = { hull: { volume: 0.8, baseFreq: 48, filterCutoff: 110, resonance: 2.8, noiseMix: 0.55, harmonicSpread: 1.025 }, warp: { volume: 0.65, bpm: 42, carrierFreq: 66, filterCutoff: 180, pulseShape: 'defiant', resonance: 3.5, swirlMix: 0.3 }, lifeSupport: { volume: 0.65, noiseType: 'pink', highpassFreq: 160, lowpassFreq: 1700, airflowModSpeed: 0.12, airflowModDepth: 0.14 }, - telemetry: { volume: 0.45, density: 0.5, era: 'voyager' } + telemetry: { volume: 0.45, density: 0.65, era: 'belter' } }, 'sta-ceres': { id: 'sta-ceres', @@ -9060,7 +10043,7 @@ const UniverseRegistry = { hull: { volume: 0.85, baseFreq: 42, filterCutoff: 95, resonance: 2.6, noiseMix: 0.62, harmonicSpread: 1.02 }, warp: { volume: 0.45, bpm: 38, carrierFreq: 54, filterCutoff: 135, pulseShape: 'nx', resonance: 3.0, swirlMix: 0.2 }, lifeSupport: { volume: 0.7, noiseType: 'brown', highpassFreq: 120, lowpassFreq: 1400, airflowModSpeed: 0.08, airflowModDepth: 0.16 }, - telemetry: { volume: 0.35, density: 0.35, era: 'nx' } + telemetry: { volume: 0.35, density: 0.6, era: 'belter' } }, 'sta-gateway': { id: 'sta-gateway', @@ -13525,13 +14508,13 @@ document.addEventListener('DOMContentLoaded', () => { } // Phase 2 audio_gaps.md rows #25-#45 - if (activeUniverseId === 'whoniverse' && presetId.indexOf('tardis-') === 0) { - whoniverseAudio.startConsoleTypeTelemetry(2800, 6500); // #25 - } if (presetId === 'bio-vorlon') expandedAudio.startVorlonSong(); // #26 if (presetId === 'bio-wraith') expandedAudio.startHullRegen(); // #28 - if (presetId === 'sta-ceres' || presetId === 'sta-tycho') expandedAudio.startBelterTelemetry(); // #29 if (presetId === 'deep-event-horizon' || presetId === 'deep-icarus-ii') expandedAudio.startHullStrainMoans(); // #30 + // Phase 3 (audio_gaps.md #46-#54) + if (presetId === 'sta-babylon-5') expandedAudio.startZocaloChatter(); // #46 + if (presetId === 'ind-nostromo') expandedAudio.startTeletypeChatter(); // #51 + if (presetId === 'ret-jupiter-2') expandedAudio.startHeterodyneDrone(); // #54 if (presetId === 'deep-icarus-ii') expandedAudio.startIcarusBeacon(); // #31 if (presetId === 'deep-event-horizon' || presetId === 'deep-nightflyer') expandedAudio.startVoidWhispers(); // #32 if (presetId === 'com-protector') expandedAudio.startBerylliumThrum(); // #33 @@ -13900,10 +14883,12 @@ document.addEventListener('DOMContentLoaded', () => { expandedAudio.synthesizeHalChime(); break; case 'ftl-jump': - case 'flak': expandedAudio.synthesizeFtlJump(); visualizer.spawnWarpPulses(); break; + case 'flak': + expandedAudio.synthesizeFlakBarrageBurst(); + break; case 'singularity': case 'gravity': case 'rotation': @@ -14148,6 +15133,31 @@ document.addEventListener('DOMContentLoaded', () => { expandedAudio.synthesizeCygnusPistonChug(); break; + // Phase 3 (audio_gaps.md #48-#53) + case 'btn-transporter': + expandedAudio.synthesizeTransporterCycle(); + break; + case 'btn-breech-clank': + expandedAudio.synthesizeBreechClank(); + break; + case 'btn-dock-groan': + expandedAudio.synthesizeDockingGroan(); + break; + case 'btn-tape-spooler': + expandedAudio.synthesizeTapeSpooler(); + break; + case 'btn-teletype': + expandedAudio.synthesizeTeletypeChatterPhrase(); + break; + case 'btn-zocalo-chatter': + if (expandedAudio.zocaloChatterActive) expandedAudio.stopZocaloChatter(); + else expandedAudio.startZocaloChatter(); + break; + case 'btn-hetero-drone': + if (expandedAudio.heterodyneDroneActive) expandedAudio.stopHeterodyneDrone(); + else expandedAudio.startHeterodyneDrone(); + break; + default: telemetry.synthesizeLCARSSingleChirp(); break;