From 71252ff84fde28be824e20f334a834ad6ddeb77f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 08:49:58 -0700 Subject: [PATCH] Implement Phase 2 audio gaps: 25 medium-difficulty synthesizers and ambient loops - 23 new ExpandedSciFiAudioSynth methods + 2 WhoniverseAudioSynth methods covering audio_gaps.md rows #21-#45: Cardassian door, turbolift, replicator, scanner activation, console type clatter, Vorlon song, neural-bond swell, hull regen, Belter jury-rig telemetry, hull strain moan, Icarus beacon, void whisper, Beryllium thrum, Omega-13, repulsorlift drone, Bebop chug, carousel groan, slipstream surge, O2 regulator, pipe drips, steam vent, crash-couch gimbal, HAL respiration, Death Blossom, Cygnus piston chug - Keyed ambient-loop registry (_scheduleLoop/_stopLoop) replacing ad-hoc timer fields; stopAllLoops clears registry plus legacy timers - Single startPresetAmbience funnel in app.js engages ambience per preset on engage/switch and tears down on full stop; whoniverse ambience stopped on every preset change - All 10 universe soundboards rebuilt (6-8 buttons each, span-2 Belter telemetry); NEURAL PULSE event now fires the real neural-bond swell - TelemetrySynth era routing intentionally untouched (Phase 3 scope); docs updated in audio_gaps.md and sound_reference.md --- audio_gaps.md | 68 +- dist/SciFiAmbientDisplay_v4.html | 1769 +++++++++++++++++++++++++++++- js/app.js | 130 ++- js/audio.js | 1567 ++++++++++++++++++++++++++ js/config.js | 72 +- sound_reference.md | 117 ++ 6 files changed, 3602 insertions(+), 121 deletions(-) diff --git a/audio_gaps.md b/audio_gaps.md index 197be18..d0b99f5 100644 --- a/audio_gaps.md +++ b/audio_gaps.md @@ -37,31 +37,31 @@ The table below catalogs all **54 discrete sound gaps** identified across the 10 | 18 | **True RF Radio Static Burst** | Outlaw & Frontier | The Betty, The Marauder | Rugged CB-radio squelch break and atmospheric RF hash between short-range ship comms. | `ExpandedSciFiAudioSynth.synthesizeRadioStatic()`.
• **Soundboard:** Outlaw cockpit (`btn-radio-static`). Bandpass-filtered pink/white noise burst (1400 Hz, Q 1.1, hard-knee 180 ms gate). | High | **Low** | **Implemented** | | 19 | **TARDIS door open/close** | Whoniverse | All TARDIS Console Rooms | Friction creak of heavy wooden police box exterior doors followed by an iron mortise lock click. | `WhoniverseAudioSynth.synthesizeTardisDoor()`.
• **Soundboard:** TARDIS controls (`btn-tardis-door`). Bandpass friction noise (wood creak, 260 to 540 Hz) layered over dual metallic mortise latch clicks (1100 Hz / 750 Hz). | High | **Low–Medium** | **Implemented** | | 20 | **Magnetic boot ("mag-boot") latch** | Space Stations | Ceres, Tycho (*The Expanse*) | Heavy physical boot heel impact on iron decking immediately followed by an electromagnetic clamping buzz. | `ExpandedSciFiAudioSynth.synthesizeMagBootLatch()`.
• **Soundboard:** Station ops (`btn-mag-boot`). 80 Hz impact click (30 ms) followed instantly by a 120 Hz square-wave clamping buzz (160 ms decay). | High | **Low–Medium** | **Implemented** | -| 21 | **Cardassian door** | Star Trek | DS9 (Terok Nor) | Oppressive, heavy Cardassian pneumatic bulkheads grinding open along metal gears. | Resonant bandpass filter sweep across brown/pink noise (120 Hz to 650 Hz) layered with a scraping harmonic metallic saw undertone. | High | **Medium** | Backlog | -| 22 | **Turbolift** | Star Trek | TNG, VOY | High-speed magnetic turbolift car passing through deck shafts with a rising whoosh and decelerating settle. | Bandpass-filtered white noise pitch sweep (180 Hz to 450 Hz and back) with resonant boost and soft stereo panning drift. | Medium | **Medium** | Backlog | -| 23 | **Replicator** | Star Trek | TNG, VOY | Molecular dematerialization / materialization shimmer when dispensing food or tools. | High-frequency white noise shimmer (passband 2500–8000 Hz) modulated by a fast sine LFO (30 Hz) with soft ramp decay. | Low | **Medium** | Backlog | -| 24 | **Scanner/monitor activation** | Whoniverse | All TARDIS Console Rooms | Distinctive cathode-ray / crystal scanner screen powering on above the central console. | Rapid upward electronic pitch sweep (sine 200 Hz to 1600 Hz over 120 ms) terminating in a steady 15.6 kHz CRT flyback whine. | Medium | **Medium** | Backlog | -| 25 | **Console "type" input telemetry** | Whoniverse | All TARDIS Console Rooms | Rhythmic, mechanical clatter of physical toggle switches, spring-loaded buttons, and tumbler relays. | Specialized telemetry routine generating clustered wooden/plastic clicks and solenoid snaps rather than tonal beeps. | High | **Medium** | Backlog | -| 26 | **"Singing" resonance element** | Bioships | Vorlon Cruiser (*Babylon 5*) | Ethereal, telepathic crystal harmonics echoing through living Vorlon organic hulls. | Dual detuned sine wave oscillators (528 Hz and 532 Hz) running through a narrow bandpass filter with slow undulating tremolo (0.2 Hz). | High | **Medium** | Backlog | -| 27 | **Neural-bond swell** | Bioships | Moya, Talyn, Wraith Hive | Symbiotic neural link between pilot and bioship swelling during heightened emotional or flight states. | Deep sub-bass triangle wave (55 Hz) sweeping into a resonant vowel-formant filter (350–850 Hz) over 2.5 seconds. | Medium | **Medium** | Backlog | -| 28 | **Hull self-repair / regeneration texture** | Bioships | Wraith Hive Ship | Microscopic organic tissue knit and chitinous shell regrowth during ship recovery phases. | Granular amplitude-modulated pink noise (8–24 Hz modulation rate) creating a quiet, wet, scabbing/cracking textural bed. | Low | **Medium** | Backlog | -| 29 | **Belter "jury-rigged" telemetry era** | Space Stations | Ceres, Tycho (*The Expanse*) | Rattle of loose relays, worn copper contactors, and erratic voltage drops across secondhand station hardware. | Telemetry synthesis routine combining noisy square pulses, contact-bounce jitter, and unstable clock drift. | High | **Medium** | Backlog | -| 30 | **True Hull Strain Moan** | Deep Space | Event Horizon, Ark One, Icarus II | Massive structural groan and creak of stressed titanium and composite bulkheads flexing under gravitational shear. | Resonant bandpass filter sweeping slowly (40 Hz to 220 Hz, high Q) over shaped low-frequency pink noise with asymmetric decay. | High | **Medium** | Backlog | -| 31 | **Icarus I distress beacon** | Deep Space | Icarus II (*Sunshine*) | Eerie, hypnotic modal electronic arpeggio echoing across the void from the ghost ship. | Multi-stage sine arpeggiator playing a haunting 4-note modal sequence through long feedback delay and soft lowpass dampening. | High | **Medium** | Backlog | -| 32 | **Extradimensional / psychic static whisper** | Deep Space | Event Horizon, Nightflyer | Menacing psychoacoustic hallucination burst simulating whispers bleeding through dimensional rifts. | Multi-formant filtered white noise modulated by randomized micro-envelopes, evoking whispered vowels in the 800–2400 Hz range. | Medium | **Medium** | Backlog | -| 33 | **Beryllium Sphere Resonant Thrum** | Comedy & Adventure | NSEA Protector (*Galaxy Quest*) | Deep, glassy, crystalline reactor hum vibrating with pure alien harmonic energy. | Dual pure sine waves (65 Hz + 195 Hz 3rd harmonic) with a slow beating detune and subtle ringing resonant comb filter. | Medium | **Medium** | Backlog | -| 34 | **Omega-13 Temporal Capacitor Whine** | Comedy & Adventure | NSEA Protector (*Galaxy Quest*) | Tremendous temporal capacitor power buildup rising into a deafening dimensional snap. | Deep 40 Hz sub-bass swell exponentially ramping in frequency to 3200 Hz over 3 seconds, culminating in a wide white-noise discharge pop. | High | **Medium** | Backlog | -| 35 | **Repulsorlift Engine Drone** | Outlaw & Frontier | The Marauder (*Star Wars*) | Quintessential atmospheric anti-gravity vehicle wash and repulsor grid drone. | Dual detuned sine wave carrier (68 Hz / 72 Hz) driving an asymmetric overdrive wave-shaper with lowpass filtering at 280 Hz. | Medium | **Medium** | Backlog | -| 36 | **Converted Marine Trawler Engine Chug** | Outlaw & Frontier | Bebop (*Cowboy Bebop*) | Rhythmic, heavy diesel-like marine piston strokes echoing through the Bebop's converted hull. | Periodic low-frequency square pulse (4–8 Hz repetition) passing through an 80 Hz resonant lowpass filter with mechanical piston wheeze. | Medium | **Medium** | Backlog | -| 37 | **Centrifugal Habitat Carousel Motor Groan** | Military Space | EAS Agamemnon (*Babylon 5*) | Low-frequency rotational strain and mechanical bearing hum of the 1 rpm rotating crew carousel. | 16 Hz fundamental sine drone modulated in amplitude by an ultra-slow LFO (0.016 Hz / 60-second full rotation cycle). | Medium | **Medium** | Backlog | -| 38 | **Slipstream Drive Transition Surge** | Military Space | Andromeda Ascendant | Ascending harmonic pitch sweep followed by crystalline string-like resonance upon entering slipstream. | Dual sweeping bandpass filters (200 Hz to 2400 Hz) with escalating resonance Q factor driving a comb-filter shimmer tail. | Medium | **Medium** | Backlog | -| 39 | **Viper Pilot Oxygen Regulator Demand Valve** | Military Space | Colonial Viper Mk II (*BSG*) | Rhythmic breathing hiss and mechanical click of cockpit life-support demand regulator during high-G combat. | Pulsed highpass white-noise envelope (1200–4500 Hz, 1.2s duration) prefixed by a sharp 15 ms mechanical diaphragm click. | Medium | **Medium** | Backlog | -| 40 | **Condensation Pipe Drip & Expansion Ticks** | Industrial Space | USCSS Nostromo, Serenity | Water droplets hitting grated industrial deck plates and irregular metallic pings from cooling steam pipes. | High-Q sine pings (1400–2600 Hz, 10–25 ms decay) triggered on a randomized Poisson probability clock with varied amplitudes. | Medium | **Medium** | Backlog | -| 41 | **High-Pressure Continuous Steam / Boiler Venting** | Industrial Space | Serenity, Starbug | Sustained high-pressure thermal exhaust venting from leaky boiler seals and cooling radiators. | Bandpass-filtered white noise (800–2800 Hz) with continuous slow amplitude swell and random micro-flutter modulation. | Medium | **Medium** | Backlog | -| 42 | **Crash Couch Hydraulic Gimbal / G-Load Strain** | Industrial Space | Rocinante (*The Expanse*) | High-torque hydraulic servos straining and whining as gimbaled couches pivot under 6G burn maneuvers. | Frequency-modulated triangle wave (140 Hz carrier, 35 Hz modulator) with lowpass damping tracking ship acceleration changes. | Medium | **Medium** | Backlog | -| 43 | **HAL 9000 Breathing / Vocal Carrier Ambient Loop** | Retro Future | Discovery One (*2001*) | Chillingly calm, measured human respiration echoing inside Dave Bowman's helmet and HAL's logic core. | Rhythmic bandpass pink noise (450–1100 Hz) modulated with a slow 3.5-second inhalation/exhalation envelope cycle. | Medium | **Medium** | Backlog | -| 44 | **Death Blossom Energy Surge Capacitor Ramp** | Retro Future | Gunstar (*The Last Starfighter*) | Rapid, escalating multi-oscillator power buildup prior to deploying the 360-degree weapons barrage. | Cascaded sawtooth oscillators performing an exponential frequency ramp from 200 Hz to 8000 Hz over 2.5 seconds with rising overdrive. | Medium | **Medium** | Backlog | -| 45 | **Victorian Steam Piston Chug & Iron Camshaft Clatter** | Retro Future | USS Cygnus (*The Black Hole*) | Massive reciprocating iron steam pistons and heavy camshaft linkages turning the Cygnus's cathedral engines. | Lowpass-filtered noise burst synchronized with a heavy 40 Hz triangle body thump and metallic slapback reflection. | Medium | **Medium** | Backlog | +| 21 | **Cardassian door** | Star Trek | DS9 (Terok Nor) | Oppressive, heavy Cardassian pneumatic bulkheads grinding open along metal gears. | `ExpandedSciFiAudioSynth.synthesizeCardassianDoor()`.
• **Soundboard:** Starfleet keypad (`btn-cardassian-door`). Resonant brown-noise bandpass sweep (120→650 Hz, Q 3.5) layered with a scraping 110→70 Hz saw undertone. | High | **Medium** | **Implemented** | +| 22 | **Turbolift** | Star Trek | TNG, VOY | High-speed magnetic turbolift car passing through deck shafts with a rising whoosh and decelerating settle. | `ExpandedSciFiAudioSynth.synthesizeTurboliftWhoosh()`.
• **Soundboard:** Starfleet keypad (`btn-turbolift`). White-noise bandpass sweep (180→450→180 Hz, Q 2.2) with −0.45→+0.45 stereo pan drift. | Medium | **Medium** | **Implemented** | +| 23 | **Replicator** | Star Trek | TNG, VOY | Molecular dematerialization / materialization shimmer when dispensing food or tools. | `ExpandedSciFiAudioSynth.synthesizeReplicatorShimmer()`.
• **Soundboard:** Starfleet keypad (`btn-replicator`). Dual parallel bands (3.2 kHz / 6.4 kHz) of white noise with 30 Hz sine AM and 2 s ramp decay. | Low | **Medium** | **Implemented** | +| 24 | **Scanner/monitor activation** | Whoniverse | All TARDIS Console Rooms | Distinctive cathode-ray / crystal scanner screen powering on above the central console. | `WhoniverseAudioSynth.synthesizeScannerActivate()`.
• **Soundboard:** TARDIS controls (`btn-scanner-activate`). Sine sweep (200→1600 Hz / 120 ms) terminating in a 10.6 kHz CRT flyback partial (15.6 kHz spec compromised: near-Nyquist/ear-fatiguing). | Medium | **Medium** | **Implemented** | +| 25 | **Console "type" input telemetry** | Whoniverse | All TARDIS Console Rooms | Rhythmic, mechanical clatter of physical toggle switches, spring-loaded buttons, and tumbler relays. | `WhoniverseAudioSynth.synthesizeTypeClatter()` + `startConsoleTypeTelemetry()`.
• **Soundboard:** TARDIS controls (`btn-type-clatter`).
• **Ambient Loop:** Random-window clatter clusters (2.8–6.5 s) on every `tardis-*` console room. Delivered as a standalone routine; `TelemetrySynth` `era: 'who'` routing remains Phase 3. | High | **Medium** | **Implemented** | +| 26 | **"Singing" resonance element** | Bioships | Vorlon Cruiser (*Babylon 5*) | Ethereal, telepathic crystal harmonics echoing through living Vorlon organic hulls. | `ExpandedSciFiAudioSynth.synthesizeVorlonSingingSwell()` + `startVorlonSong()`.
• **Soundboard:** Bioship keypad (`btn-vorlon-song`).
• **Ambient Loop:** 8.5 s swell phrases (11–18 s cadence) while `bio-vorlon` is engaged. Dual detuned sines (528/532 Hz) through narrow bandpass (Q 22) with 0.2 Hz tremolo. | High | **Medium** | **Implemented** | +| 27 | **Neural-bond swell** | Bioships | Moya, Talyn, Wraith Hive | Symbiotic neural link between pilot and bioship swelling during heightened emotional or flight states. | `ExpandedSciFiAudioSynth.synthesizeNeuralBondSwell()`.
• **Soundboard:** Bioship keypad (`btn-neural-bond`).
• **Scripted Event:** Bioship universe event `btn-neural-pulse` ("NEURAL PULSE") now fires this swell. Deep 55 Hz triangle sweeping through a vowel-formant bandpass (350→850 Hz, Q 4.5) over 2.5 s. | Medium | **Medium** | **Implemented** | +| 28 | **Hull self-repair / regeneration texture** | Bioships | Wraith Hive Ship | Microscopic organic tissue knit and chitinous shell regrowth during ship recovery phases. | `ExpandedSciFiAudioSynth.synthesizeHullRegenBurst()` + `startHullRegen()`.
• **Soundboard:** Bioship keypad (`btn-hull-regen`).
• **Ambient Loop:** Regeneration bursts (6–13 s cadence) while `bio-wraith` is engaged. Granular square-LFO AM (9–17 Hz) over bandpass pink noise plus wet chitin knit pops. | Low | **Medium** | **Implemented** | +| 29 | **Belter "jury-rigged" telemetry era** | Space Stations | Ceres, Tycho (*The Expanse*) | Rattle of loose relays, worn copper contactors, and erratic voltage drops across secondhand station hardware. | `ExpandedSciFiAudioSynth.synthesizeBelterTelemetryJitter()` + `startBelterTelemetry()`.
• **Soundboard:** Station ops (`btn-belter-telemetry`, full-width).
• **Ambient Loop:** Jittered clusters (1.8–4.5 s cadence) while `sta-ceres`/`sta-tycho` are engaged. Delivered as a standalone routine; `TelemetrySynth` `era: 'belter'` routing remains Phase 3. | High | **Medium** | **Implemented** | +| 30 | **True Hull Strain Moan** | Deep Space | Event Horizon, Ark One, Icarus II | Massive structural groan and creak of stressed titanium and composite bulkheads flexing under gravitational shear. | `ExpandedSciFiAudioSynth.synthesizeHullStrainMoan()` + `startHullStrainMoans()`.
• **Soundboard:** Void console (`btn-hull-strain`).
• **Ambient Loop:** Sporadic moans (16–40 s cadence) on `deep-event-horizon`/`deep-icarus-ii`. Brown-noise bandpass sweep (48→210 Hz, Q 10) with asymmetric attack/decay. | High | **Medium** | **Implemented** | +| 31 | **Icarus I distress beacon** | Deep Space | Icarus II (*Sunshine*) | Eerie, hypnotic modal electronic arpeggio echoing across the void from the ghost ship. | `ExpandedSciFiAudioSynth.synthesizeIcarusBeacon()` + `startIcarusBeacon()`.
• **Soundboard:** Void console (`btn-icarus-beacon`).
• **Ambient Loop:** Modal arpeggio (22–40 s cadence) while `deep-icarus-ii` is engaged. 4-note A-minor modal sine sequence through a 0.45 s feedback delay with 2.2 kHz dampening. | High | **Medium** | **Implemented** | +| 32 | **Extradimensional / psychic static whisper** | Deep Space | Event Horizon, Nightflyer | Menacing psychoacoustic hallucination burst simulating whispers bleeding through dimensional rifts. | `ExpandedSciFiAudioSynth.synthesizeVoidWhisper()` + `startVoidWhispers()`.
• **Soundboard:** Void console (`btn-void-whisper`).
• **Ambient Loop:** Whisper bursts (15–32 s cadence) on `deep-event-horizon`/`deep-nightflyer`. Three wobbling vowel-formants (700/1250/2200 Hz) with randomized syllable micro-envelopes. | Medium | **Medium** | **Implemented** | +| 33 | **Beryllium Sphere Resonant Thrum** | Comedy & Adventure | NSEA Protector (*Galaxy Quest*) | Deep, glassy, crystalline reactor hum vibrating with pure alien harmonic energy. | `ExpandedSciFiAudioSynth.synthesizeBerylliumThrumSwell()` + `startBerylliumThrum()`.
• **Soundboard:** Cheerful keypad (`btn-beryllium-thrum`).
• **Ambient Loop:** 10 s thrum swell phrases (9–16 s cadence) while `com-protector` is engaged. Pure sines (65 + 65.3 Hz beating + 195 Hz third) ringing through a 5.1 ms comb reflection. | Medium | **Medium** | **Implemented** | +| 34 | **Omega-13 Temporal Capacitor Whine** | Comedy & Adventure | NSEA Protector (*Galaxy Quest*) | Tremendous temporal capacitor power buildup rising into a deafening dimensional snap. | `ExpandedSciFiAudioSynth.synthesizeOmega13Whine()`.
• **Soundboard:** Cheerful keypad (`btn-omega-13`). 40 Hz sub-bass exponentially ramping to 3200 Hz over 3 s, culminating in a wide highpass white-noise discharge pop. | High | **Medium** | **Implemented** | +| 35 | **Repulsorlift Engine Drone** | Outlaw & Frontier | The Marauder (*Star Wars*) | Quintessential atmospheric anti-gravity vehicle wash and repulsor grid drone. | `ExpandedSciFiAudioSynth.synthesizeRepulsorliftDrone()` + `startRepulsorliftDrone()`.
• **Soundboard:** Outlaw cockpit (`btn-repulsor-drone`).
• **Ambient Loop:** 10 s wash phrases (13 s cadence) while `out-marauder` is engaged. Dual detuned sines (68/72 Hz) through a tanh waveshaper (2x oversampled) and 280 Hz lowpass. | Medium | **Medium** | **Implemented** | +| 36 | **Converted Marine Trawler Engine Chug** | Outlaw & Frontier | Bebop (*Cowboy Bebop*) | Rhythmic, heavy diesel-like marine piston strokes echoing through the Bebop's converted hull. | `ExpandedSciFiAudioSynth.synthesizeBebopChugStroke()` + `startBebopChug()`.
• **Soundboard:** Outlaw cockpit (`btn-bebop-chug`).
• **Ambient Loop:** 2–4 stroke clusters (2.2–4.2 s cadence) while `out-bebop` is engaged. Square pulses (45–70 Hz) through an 80 Hz resonant lowpass with piston wheeze. | Medium | **Medium** | **Implemented** | +| 37 | **Centrifugal Habitat Carousel Motor Groan** | Military Space | EAS Agamemnon (*Babylon 5*) | Low-frequency rotational strain and mechanical bearing hum of the 1 rpm rotating crew carousel. | `ExpandedSciFiAudioSynth.synthesizeCarouselGroanCycle()` + `startCarouselGroan()`.
• **Soundboard:** Military keypad (`btn-carousel-groan`).
• **Ambient Loop:** One 58 s rotation pass every 60 s while `mil-agamemnon` is engaged. 16 Hz rotational beating pair + 48/96 Hz bearing harmonics, strain-swell LFO at exactly 1/60 Hz. | Medium | **Medium** | **Implemented** | +| 38 | **Slipstream Drive Transition Surge** | Military Space | Andromeda Ascendant | Ascending harmonic pitch sweep followed by crystalline string-like resonance upon entering slipstream. | `ExpandedSciFiAudioSynth.synthesizeSlipstreamSurge()`.
• **Soundboard:** Military keypad (`btn-slipstream`). Dual staggered saw surges (200→2400 Hz) through bandpass filters with Q escalation (2→10) driving a 120 ms comb shimmer tail. | Medium | **Medium** | **Implemented** | +| 39 | **Viper Pilot Oxygen Regulator Demand Valve** | Military Space | Colonial Viper Mk II (*BSG*) | Rhythmic breathing hiss and mechanical click of cockpit life-support demand regulator during high-G combat. | `ExpandedSciFiAudioSynth.synthesizeOxygenRegulatorCycle()` + `startOxygenRegulator()`.
• **Soundboard:** Military keypad (`btn-oxygen-reg`).
• **Ambient Loop:** Breath cycles (3.4–5.2 s cadence) while `mil-viper` is engaged. 15 ms diaphragm click prefixed to highpass band (1.2–4.5 kHz) white-noise breath. | Medium | **Medium** | **Implemented** | +| 40 | **Condensation Pipe Drip & Expansion Ticks** | Industrial Space | USCSS Nostromo, Serenity | Water droplets hitting grated industrial deck plates and irregular metallic pings from cooling steam pipes. | `ExpandedSciFiAudioSynth.synthesizePipeDrip()` + `startPipeDrips()`.
• **Soundboard:** Industrial keypad (`btn-pipe-drips`).
• **Ambient Loop:** Randomized drips (0.5–3.5 s cadence) while `ind-nostromo` is engaged. High-Q sine pings (1.4–2.6 kHz) with randomized 10–25 ms exponential decays and amplitudes. | Medium | **Medium** | **Implemented** | +| 41 | **High-Pressure Continuous Steam / Boiler Venting** | Industrial Space | Serenity, Starbug | Sustained high-pressure thermal exhaust venting from leaky boiler seals and cooling radiators. | `ExpandedSciFiAudioSynth.synthesizeSteamVentSwell()` + `startSteamVent()`.
• **Soundboard:** Industrial keypad (`btn-steam-vent`).
• **Ambient Loop:** 9 s vent passes (6–10 s cadence) on `ind-serenity`/`ind-starbug`. Looping white noise through a 1.5 kHz bandpass (Q 0.7) with 6–9 Hz boiler micro-flutter. | Medium | **Medium** | **Implemented** | +| 42 | **Crash Couch Hydraulic Gimbal / G-Load Strain** | Industrial Space | Rocinante (*The Expanse*) | High-torque hydraulic servos straining and whining as gimbaled couches pivot under 6G burn maneuvers. | `ExpandedSciFiAudioSynth.synthesizeCrashCouchGimbal()`.
• **Soundboard:** Industrial keypad (`btn-crash-couch`). Dual-layer FM triangle (140 Hz carrier / 35 Hz modulator, then 152/38 Hz) with lowpass damping tracking the strain swell. | Medium | **Medium** | **Implemented** | +| 43 | **HAL 9000 Breathing / Vocal Carrier Ambient Loop** | Retro Future | Discovery One (*2001*) | Chillingly calm, measured human respiration echoing inside Dave Bowman's helmet and HAL's logic core. | `ExpandedSciFiAudioSynth.synthesizeHalBreathCycle()` + `startHalBreathing()`.
• **Soundboard:** Retro keypad (`btn-hal-breath`).
• **Ambient Loop:** Continuous 3.5 s in/out breaths (1.3 s inhale / 2.2 s exhale) while `ret-discovery-one` is engaged. Looping pink noise bandpass (700 Hz center, Q 1.2). | Medium | **Medium** | **Implemented** | +| 44 | **Death Blossom Energy Surge Capacitor Ramp** | Retro Future | Gunstar (*The Last Starfighter*) | Rapid, escalating multi-oscillator power buildup prior to deploying the 360-degree weapons barrage. | `ExpandedSciFiAudioSynth.synthesizeDeathBlossomSurge()`.
• **Soundboard:** Retro keypad (`btn-death-blossom`). Three cascaded saws (0/150/300 ms offsets) ramping 200→8000 Hz over 2.5 s through rising highpass, cut off abruptly at full bloom. | Medium | **Medium** | **Implemented** | +| 45 | **Victorian Steam Piston Chug & Iron Camshaft Clatter** | Retro Future | USS Cygnus (*The Black Hole*) | Massive reciprocating iron steam pistons and heavy camshaft linkages turning the Cygnus's cathedral engines. | `ExpandedSciFiAudioSynth.synthesizeCygnusPistonChug()` + `startCygnusChug()`.
• **Soundboard:** Retro keypad (`btn-cygnus-chug`).
• **Ambient Loop:** 2–3 stroke clusters (1.8–3.2 s cadence) while `ret-cygnus` is engaged. 40 Hz triangle thumps with lowpass-filtered steam puffs and a 0.35 s metallic slapback. | Medium | **Medium** | **Implemented** | | 46 | **Zocalo commerce-plaza chatter** | Space Stations | Babylon 5 | Bustling multi-species concourse background bed with indistinct alien murmurs and footsteps. | Multi-voice filtered pink noise bed shaped by 4 randomized parallel formant filters (300–3000 Hz) simulating crowd presence. | Low | **Medium–High** | Backlog | | 47 | **Bespoke bioship telemetry era** | Bioships | Moya, Lexx, Species 8472, Wraith | Complete replacement of inorganic Starfleet chirps with organic clicks, wet pulses, and membrane flutters. | New telemetry engine era (`bioship`): procedural scheduling of bio-acoustic primitives (spore pops, chitin snaps, vascular beats). | High | **Medium–High** | Backlog | | 48 | **Transporter dematerialization / rematerialization** | Star Trek | All Eras | Iconic Starfleet molecular transport cycle (~3.0 seconds duration, shimmer-dense). | Complex multi-oscillator additive array (5 detuned sines) with interlocking amplitude modulation and high-frequency noise shimmer. | Medium | **High** | Backlog | @@ -112,13 +112,19 @@ graph TD - **Ludicrous Speed Plaid Alarm** (`ExpandedSciFiAudioSynth.synthesizePlaidAlarm`): Comedy Soundboard (`btn-plaid-alarm`), alternating two-tone siren (440 Hz / 880 Hz). - **True RF radio static burst** (`ExpandedSciFiAudioSynth.synthesizeRadioStatic`): Outlaw Soundboard (`btn-radio-static`), bandpass-filtered noise burst with hard-knee 180 ms gate. -### Phase 2: Core Enhancements (Medium Difficulty) -- **Target Count**: 25 elements. -- **Architectural Scope**: Modulate existing continuous synth nodes (`HullDroneSynth`, `LifeSupportSynth`) and introduce multi-oscillator discrete events. +### Phase 2: Core Enhancements (Medium Difficulty) — **[COMPLETED]** +- **Target Count**: 25 elements — **100% Implemented**. +- **Architectural Scope**: 23 new procedural methods in `ExpandedSciFiAudioSynth` and 2 in `WhoniverseAudioSynth`, a keyed ambient-loop registry (`_scheduleLoop`/`_stopLoop`) replacing ad-hoc timer fields, a single `startPresetAmbience(presetId)` funnel in `js/app.js` that stops and re-engages all loops on preset change/engage/full-stop, and soundboard rebuilds across all 10 universe keypads (6–8 buttons each; `span: 2` support used by the Belter telemetry button). +- **Implementation Notes**: + - Rows #25 (`era: 'who'`) and #29 (`era: 'belter'`) are delivered as standalone routines (`startConsoleTypeTelemetry`, `startBelterTelemetry`) driven by preset ambience — **not** through `TelemetrySynth` era routing, which remains Phase 3 scope. + - #24 CRT flyback whine lands at a 10.6 kHz partial (the spec's 15.6 kHz is near-Nyquist on low-rate audio contexts and ear-fatiguing at audible levels). + - Continuous rows (#26, #33, #35, #37, #41) are rendered as slow "phrase" one-shots (8–58 s) with in/out envelopes, re-triggered on fixed/random cadence — no permanent node graphs. + - Every ambient phrase self-releases (scheduled node stops) and every `start*` kills its own registry key first; `stopAllLoops()` clears the whole registry, so rapid preset flips and FULL STOP cannot stack loops. + - One-shot soundboard buttons are interactive one-shots; ambient rows additionally auto-engage with their matching presets (see per-row trigger details in the master table). - **Key Deliverables**: - 1. **Structural Stress & Engines**: True Hull Strain Moan (`synthesizeHullStrainMoan()`), Repulsorlift engine drone, Bebop marine diesel chug, Centrifugal carousel 1 rpm LFO modulation. - 2. **Energy Fields & Weapons**: Vorlon crystal singing resonance, Omega-13 temporal capacitor ramp, Cardassian heavy bulkhead grind, Slipstream transition surge, Death Blossom capacitor sweep. - 3. **Environmental Ambience**: Nostromo condensation pipe drips (Poisson ping generator), Continuous steam vent hiss, HAL 9000 respiration envelope loop. + 1. **Structural Stress & Engines**: True Hull Strain Moan (#30, `synthesizeHullStrainMoan` + loop on Event Horizon/Icarus II), Repulsorlift engine drone (#35), Bebop marine diesel chug (#36), Centrifugal carousel 1 rpm LFO modulation (#37), Crash-couch gimbal strain (#42), Cygnus steam piston chug (#45). + 2. **Energy Fields & Weapons**: Vorlon crystal singing resonance (#26), Omega-13 temporal capacitor ramp (#34), Cardassian heavy bulkhead grind (#21), Slipstream transition surge (#38), Death Blossom capacitor sweep (#44), Icarus I distress beacon (#31), Beryllium sphere thrum (#33). + 3. **Environmental Ambience**: Nostromo condensation pipe drips (#40, randomized drip clock), Continuous steam vent hiss (#41), HAL 9000 respiration envelope loop (#43), hull regeneration texture (#28), psychic static whisper (#32), turbolift whoosh (#22), replicator shimmer (#23), scanner activation (#24), TARDIS console type clatter (#25), neural-bond swell (#27, also rewired to the Bioships NEURAL PULSE event), Belter jury-rig telemetry (#29), Viper O₂ regulator breathing (#39). ### Phase 3: Major Architectural Additions (Medium–High & High Difficulty) - **Target Count**: 11 elements. diff --git a/dist/SciFiAmbientDisplay_v4.html b/dist/SciFiAmbientDisplay_v4.html index 1712b56..e2fdce4 100644 --- a/dist/SciFiAmbientDisplay_v4.html +++ b/dist/SciFiAmbientDisplay_v4.html @@ -4291,6 +4291,7 @@ class WhoniverseAudioSynth { this.gainNode = null; this.activeCloister = false; this.cloisterTimer = null; + this.typeTelemetryTimer = null; } init() { @@ -4698,6 +4699,166 @@ class WhoniverseAudioSynth { click.stop(now + offset + 0.04); }); } + + /** + * Scanner / Monitor Screen Activation (Whoniverse, audio_gaps.md #24) + * Rapid upward sine sweep (200 Hz -> 1600 Hz over 120 ms) terminating in a + * CRT flyback whine. The 15.6 kHz spec tail is compromised to ~10.6 kHz: + * near/above Nyquist on low-rate contexts and ear-fatiguing at audible levels. + */ + synthesizeScannerActivate() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // 1. Cathode-ray startup pitch sweep + const sweep = ctx.createOscillator(); + sweep.type = 'sine'; + sweep.frequency.setValueAtTime(200, now); + sweep.frequency.exponentialRampToValueAtTime(1600, now + 0.12); + + const sweepEnv = ctx.createGain(); + sweepEnv.gain.setValueAtTime(0.001, now); + sweepEnv.gain.linearRampToValueAtTime(0.32, now + 0.03); + sweepEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.16); + + sweep.connect(sweepEnv); + sweepEnv.connect(this.gainNode); + sweep.start(now); + sweep.stop(now + 0.17); + + // 2. CRT flyback whine partial (10.6 kHz compromise, low amplitude) + const whine = ctx.createOscillator(); + whine.type = 'sine'; + whine.frequency.setValueAtTime(10600, now + 0.1); + + const wobble = ctx.createOscillator(); + wobble.type = 'sine'; + wobble.frequency.setValueAtTime(1.0, now + 0.1); + const wobbleDepth = ctx.createGain(); + wobbleDepth.gain.setValueAtTime(8, now + 0.1); + + wobble.connect(wobbleDepth); + wobbleDepth.connect(whine.frequency); + + const whineEnv = ctx.createGain(); + whineEnv.gain.setValueAtTime(0.001, now + 0.1); + whineEnv.gain.linearRampToValueAtTime(0.03, now + 0.16); + whineEnv.gain.exponentialRampToValueAtTime(0.0001, now + 0.85); + + whine.connect(whineEnv); + whineEnv.connect(this.gainNode); + whine.start(now + 0.1); + whine.stop(now + 0.87); + wobble.start(now + 0.1); + wobble.stop(now + 0.87); + } + + /** + * Console "Type" Input Clatter Cluster (Whoniverse, audio_gaps.md #25) + * Rhythmic mechanical clatter of toggle switches, spring-loaded buttons and + * tumbler relays: clustered wooden/plastic clicks and solenoid snaps rather + * than tonal beeps. Produces a single 0.3-0.6 s cluster of 3-7 transients. + */ + synthesizeTypeClatter() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const transientCount = 3 + Math.floor(Math.random() * 5); // 3-7 strikes + let cursor = 0; + + const strike = (kind, offset, peak) => { + const t = now + offset; + if (kind === 'toggle') { + // Wooden relay toggle: short bandpass pink-noise burst with downward snap + const buf = this.am.createNoiseBuffer('pink', 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(700 + Math.random() * 700, t); + bp.frequency.exponentialRampToValueAtTime(240, t + 0.03); + bp.Q.setValueAtTime(8 + Math.random() * 4, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 0.005); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.045); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.05); + } else if (kind === 'button') { + // Spring-loaded plastic button: fast square click collapsing in pitch + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(900, t); + osc.frequency.exponentialRampToValueAtTime(200, t + 0.012); + const env = ctx.createGain(); + env.gain.setValueAtTime(peak * 0.8, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.022); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.025); + } else { + // Heavy solenoid snap: dull low square thump + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(120, t); + osc.frequency.exponentialRampToValueAtTime(55, t + 0.025); + const env = ctx.createGain(); + env.gain.setValueAtTime(peak, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.035); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.04); + } + }; + + for (let i = 0; i < transientCount; i++) { + const kinds = ['toggle', 'toggle', 'button', 'button', 'solenoid']; + const kind = kinds[Math.floor(Math.random() * kinds.length)]; + cursor += 0.015 + Math.random() * 0.075; + strike(kind, cursor, 0.07 + Math.random() * 0.07); + } + } + + /** + * Ambient Loop: TARDIS Console "Type" Input Telemetry (#25) + * Recursive random-window clatter while any TARDIS console room is engaged. + */ + startConsoleTypeTelemetry(minIntervalMs = 2800, maxIntervalMs = 6500) { + this.stopConsoleTypeTelemetry(); + const scheduleNext = () => { + const delay = minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); + this.typeTelemetryTimer = setTimeout(() => { + this.synthesizeTypeClatter(); + scheduleNext(); + }, delay); + }; + scheduleNext(); + } + + stopConsoleTypeTelemetry() { + if (this.typeTelemetryTimer) { + clearTimeout(this.typeTelemetryTimer); + this.typeTelemetryTimer = null; + } + } + + /** + * Stops all automated Whoniverse ambient loops. Cloister bell is an + * interactive toggle and keeps its own explicit call sites. + */ + stopAmbientLoops() { + this.stopConsoleTypeTelemetry(); + } } window.WhoniverseAudioSynth = WhoniverseAudioSynth; @@ -4715,6 +4876,7 @@ class ExpandedSciFiAudioSynth { this.gainNode = null; this.medicalTimer = null; this.sparkTimer = null; + this.loopTimers = {}; } init() { @@ -6115,9 +6277,1414 @@ class ExpandedSciFiAudioSynth { } } + /** + * Ambient Loop Registry (Phase 2) + * Generic recursive-timeout scheduler keyed by name. A start call always + * kills any previous instance of the same key first. If min === max the + * cadence is fixed; otherwise it is random within [min, max]. + */ + _scheduleLoop(key, minIntervalMs, maxIntervalMs, fn) { + this._stopLoop(key); + const scheduleNext = () => { + const delay = maxIntervalMs === minIntervalMs + ? minIntervalMs + : minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); + this.loopTimers[key] = setTimeout(() => { + fn(); + scheduleNext(); + }, delay); + }; + scheduleNext(); + } + + _stopLoop(key) { + if (this.loopTimers[key]) { + clearTimeout(this.loopTimers[key]); + delete this.loopTimers[key]; + } + } + + /** + * Cardassian Bulkhead Door Grind (Star Trek DS9, audio_gaps.md #21) + * Oppressive pneumatic bulkhead grinding open: resonant bandpass sweep + * across brown noise (120 Hz -> 650 Hz) layered with a scraping metallic + * saw undertone. + */ + synthesizeCardassianDoor() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.4; + + const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.2); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(120, now); + bp.frequency.exponentialRampToValueAtTime(650, now + 1.1); + bp.Q.setValueAtTime(3.5, now); + + // Scraping metallic saw undertone through the same sweeping bandpass + const saw = ctx.createOscillator(); + saw.type = 'sawtooth'; + saw.frequency.setValueAtTime(110, now); + saw.frequency.exponentialRampToValueAtTime(70, now + duration); + const sawGain = ctx.createGain(); + sawGain.gain.setValueAtTime(0.001, now); + sawGain.gain.linearRampToValueAtTime(0.09, now + 0.6); + sawGain.gain.exponentialRampToValueAtTime(0.001, now + duration); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.5, now + 0.4); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + noise.connect(bp); + saw.connect(sawGain); + sawGain.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + + noise.start(now); + noise.stop(now + duration + 0.05); + saw.start(now); + saw.stop(now + duration + 0.05); + } + + /** + * Turbolift Pass Whoosh (Star Trek TNG/VOY, audio_gaps.md #22) + * Bandpass-filtered white noise pitch sweep (180 -> 450 Hz and back) with + * resonant boost and soft stereo panning drift past the listener. + */ + synthesizeTurboliftWhoosh() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.6; + + const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(180, now); + bp.frequency.exponentialRampToValueAtTime(450, now + 0.55); + bp.frequency.exponentialRampToValueAtTime(180, now + 1.4); + bp.Q.setValueAtTime(2.2, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.42, now + 0.5); + env.gain.linearRampToValueAtTime(0.3, now + 0.8); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + let panOut; + if (ctx.createStereoPanner) { + const panner = ctx.createStereoPanner(); + panner.pan.setValueAtTime(-0.45, now); + panner.pan.linearRampToValueAtTime(0.45, now + duration); + panOut = panner; + } + if (panOut) { + env.connect(panOut); + panOut.connect(this.gainNode); + } else { + env.connect(this.gainNode); + } + + noise.connect(bp); + bp.connect(env); + noise.start(now); + noise.stop(now + duration + 0.02); + } + + /** + * Replicator Materialization Shimmer (Star Trek TNG/VOY, audio_gaps.md #23) + * High-frequency white-noise shimmer (2.5-8 kHz) amplitude-modulated by a + * fast 30 Hz sine LFO with a soft ramp decay. + */ + synthesizeReplicatorShimmer() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 2.0; + + const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const masterEnv = ctx.createGain(); + masterEnv.gain.setValueAtTime(0.001, now); + masterEnv.gain.linearRampToValueAtTime(0.3, now + 0.5); + masterEnv.gain.exponentialRampToValueAtTime(0.001, now + duration); + masterEnv.connect(this.gainNode); + + // Two parallel shimmer bands (2.5-4 kHz and 5-8 kHz) + const bands = [ + { center: 3200, q: 1.3, depth: 0.22 }, + { center: 6400, q: 1.2, depth: 0.16 } + ]; + bands.forEach((band) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(band.center, now); + bp.Q.setValueAtTime(band.q, now); + + const amEnv = ctx.createGain(); + amEnv.gain.setValueAtTime(band.depth, now); + + // 30 Hz shimmer AM + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(30, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(band.depth * 0.9, now); + lfo.connect(lfoDepth); + lfoDepth.connect(amEnv.gain); + + noise.connect(bp); + bp.connect(amEnv); + amEnv.connect(masterEnv); + + lfo.start(now); + lfo.stop(now + duration + 0.05); + }); + + noise.start(now); + noise.stop(now + duration + 0.02); + } + + /** + * Vorlon Crystal "Singing" Resonance (Bioships, audio_gaps.md #26) + * Ethereal telepathic crystal harmonics: dual detuned sines (528/532 Hz) + * through a narrow bandpass with a slow 0.2 Hz undulating tremolo. + * One 8.5 s swell phrasing. + */ + synthesizeVorlonSingingSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 8.5; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(530, now); + bp.Q.setValueAtTime(22, now); + + const tremolo = ctx.createGain(); + tremolo.gain.setValueAtTime(0.5, now); + + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(0.2, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(0.24, now); + lfo.connect(lfoDepth); + lfoDepth.connect(tremolo.gain); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.16, now + 3.0); + env.gain.linearRampToValueAtTime(0.13, now + 5.5); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + [528, 532].forEach((freq, idx) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + if (idx === 1) osc.detune.setValueAtTime(3, now); + osc.connect(bp); + osc.start(now); + osc.stop(now + duration + 0.05); + }); + + bp.connect(tremolo); + tremolo.connect(env); + + lfo.start(now); + lfo.stop(now + duration + 0.05); + } + + /** + * Ambient Loop: Vorlon singing resonance while the Vorlon cruiser is engaged + */ + startVorlonSong(minIntervalMs = 11000, maxIntervalMs = 18000) { + this._scheduleLoop('vorlonSong', minIntervalMs, maxIntervalMs, () => { + this.synthesizeVorlonSingingSwell(); + }); + } + + /** + * Neural-Bond Swell (Bioships, audio_gaps.md #27) + * Symbiotic neural link between pilot and bioship swelling with emotion: + * deep 55 Hz triangle sweeping into a resonant vowel-formant filter + * (350-850 Hz) over 2.5 seconds. + */ + synthesizeNeuralBondSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 3.4; + + // Sub-bass carrier + const osc = ctx.createOscillator(); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(55, now); + osc.frequency.linearRampToValueAtTime(52, now + duration); + + // Formant sweep adding vowel-like harmonics + const formant = ctx.createBiquadFilter(); + formant.type = 'bandpass'; + formant.frequency.setValueAtTime(350, now); + formant.frequency.linearRampToValueAtTime(850, now + 2.5); + formant.Q.setValueAtTime(4.5, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.34, now + 0.9); + env.gain.linearRampToValueAtTime(0.42, now + 1.9); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + osc.connect(formant); + formant.connect(env); + env.connect(this.gainNode); + osc.start(now); + osc.stop(now + duration + 0.05); + } + + /** + * Hull Self-Repair / Regeneration Texture (Bioships, audio_gaps.md #28) + * Microscopic organic tissue knit and chitin regrowth: granular + * amplitude-modulated pink noise (8-24 Hz modulation) with wet low + * regenerative pops. One ~1.8 s regeneration burst. + */ + synthesizeHullRegenBurst() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.8; + + const noiseBuf = this.am.createNoiseBuffer('pink', duration + 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(650, now); + bp.frequency.linearRampToValueAtTime(1600, now + duration); + bp.Q.setValueAtTime(2.0, now); + + const modEnv = ctx.createGain(); + modEnv.gain.setValueAtTime(0.16, now); + + // Rapid scabbing modulation (~13 Hz) over the wet tissue bed + const lfo = ctx.createOscillator(); + lfo.type = 'square'; + lfo.frequency.setValueAtTime(9 + Math.random() * 8, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(0.12, now); + lfo.connect(lfoDepth); + lfoDepth.connect(modEnv.gain); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.2, now + 0.25); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + noise.connect(bp); + bp.connect(modEnv); + modEnv.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + duration + 0.05); + lfo.start(now); + lfo.stop(now + duration + 0.05); + + // Wet bioplasmic pops (chitin knit) + const popCount = 2 + Math.floor(Math.random() * 3); + for (let i = 0; i < popCount; i++) { + const popTime = now + 0.25 + Math.random() * (duration - 0.6); + const pop = ctx.createOscillator(); + pop.type = 'sine'; + pop.frequency.setValueAtTime(110 + Math.random() * 60, popTime); + pop.frequency.exponentialRampToValueAtTime(60, popTime + 0.06); + const popEnv = ctx.createGain(); + popEnv.gain.setValueAtTime(0.001, popTime); + popEnv.gain.linearRampToValueAtTime(0.14, popTime + 0.008); + popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.07); + pop.connect(popEnv); + popEnv.connect(this.gainNode); + pop.start(popTime); + pop.stop(popTime + 0.08); + } + } + + /** + * Ambient Loop: Periodic hull regeneration texture on Wraith hives + */ + startHullRegen(minIntervalMs = 6000, maxIntervalMs = 13000) { + this._scheduleLoop('hullRegen', minIntervalMs, maxIntervalMs, () => { + this.synthesizeHullRegenBurst(); + }); + } + + /** + * Belter Jury-Rigged Telemetry Jitter (Space Stations, audio_gaps.md #29) + * Rattle of loose relays, worn copper contactors and erratic voltage drops: + * noisy square relay pulses with micro-dropouts, contact-bounce double + * clicks and harsh metallic ticks on a jittered clock. One ~1.1 s cluster. + */ + synthesizeBelterTelemetryJitter() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const eventCount = 3 + Math.floor(Math.random() * 5); // 3-7 events + let cursor = now + 0.05 + Math.random() * 0.2; + + const relayPulse = (t) => { + const osc = ctx.createOscillator(); + osc.type = 'square'; + const baseFreq = 60 + Math.random() * 60; + osc.frequency.setValueAtTime(baseFreq, t); + osc.frequency.linearRampToValueAtTime(baseFreq * 0.8, t + 0.06); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(420, t); + const env = ctx.createGain(); + // Erratic voltage: randomized micro-dropouts in the pulse body + env.gain.setValueAtTime(0.17, t); + env.gain.setValueAtTime(0.001, t + 0.012 + Math.random() * 0.015); + env.gain.setValueAtTime(0.14, t + 0.026 + Math.random() * 0.02); + env.gain.setValueAtTime(0.001, t + 0.05 + Math.random() * 0.025); + env.gain.setValueAtTime(0.1, t + 0.07); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.1); + osc.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.11); + }; + + const contactBounce = (t) => { + // Worn contactor: double/triple click 1-3 ms apart + const clicks = 2 + (Math.random() < 0.35 ? 1 : 0); + for (let i = 0; i < clicks; i++) { + const ct = t + i * (0.001 + Math.random() * 0.002); + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(500 + Math.random() * 500, ct); + osc.frequency.exponentialRampToValueAtTime(120, ct + 0.008); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.14, ct); + env.gain.exponentialRampToValueAtTime(0.001, ct + 0.012); + osc.connect(env); + env.connect(this.gainNode); + osc.start(ct); + osc.stop(ct + 0.015); + } + }; + + const metallicTick = (t) => { + const buf = this.am.createNoiseBuffer('white', 0.05); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(400 + Math.random() * 1400, t); + bp.Q.setValueAtTime(6, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.09, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.02); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.025); + }; + + for (let i = 0; i < eventCount; i++) { + const kind = Math.random(); + if (kind < 0.4) relayPulse(cursor); + else if (kind < 0.75) contactBounce(cursor); + else metallicTick(cursor); + cursor += 0.08 + Math.random() * 0.22; // jittered clock drift + } + } + + /** + * Ambient Loop: Intermittent jury-rigged telemetry on Belter stations + */ + startBelterTelemetry(minIntervalMs = 1800, maxIntervalMs = 4500) { + this._scheduleLoop('belterTelemetry', minIntervalMs, maxIntervalMs, () => { + this.synthesizeBelterTelemetryJitter(); + }); + } + + /** + * True Hull Strain Moan (Deep Space, audio_gaps.md #30) + * Massive structural groan of stressed bulkheads flexing under gravitational + * shear: resonant bandpass slowly sweeping 40-220 Hz (high Q) over shaped + * brown noise with an asymmetric attack/decay. ~5.5 s. + */ + synthesizeHullStrainMoan() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 5.5; + + const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.3); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(48, now); + bp.frequency.exponentialRampToValueAtTime(210, now + 3.4); + bp.Q.setValueAtTime(10, now); + + // Asymmetric envelope: slow menacing rise, long creaking decay + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.3, now + 0.9); + env.gain.linearRampToValueAtTime(0.22, now + 1.7); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + duration + 0.05); + } + + /** + * Ambient Loop: Sporadic structural strain moans on haunted deep-space hulls + */ + startHullStrainMoans(minIntervalMs = 16000, maxIntervalMs = 40000) { + this._scheduleLoop('hullStrainMoans', minIntervalMs, maxIntervalMs, () => { + this.synthesizeHullStrainMoan(); + }); + } + + /** + * Icarus I Distress Beacon (Deep Space, audio_gaps.md #31) + * Eerie hypnotic modal arpeggio echoing from the ghost ship: a 4-note + * modal sine sequence through a long feedback delay with soft lowpass + * dampening. ~7 s including delay tail. + */ + synthesizeIcarusBeacon() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const notes = [220.0, 261.63, 329.63, 440.0]; // A minor modal beacon + + const delay = ctx.createDelay(1.2); + delay.delayTime.setValueAtTime(0.45, now); + const feedback = ctx.createGain(); + feedback.gain.setValueAtTime(0.35, now); + const dampen = ctx.createBiquadFilter(); + dampen.type = 'lowpass'; + dampen.frequency.setValueAtTime(2200, now); + const wet = ctx.createGain(); + wet.gain.setValueAtTime(0.5, now); + + delay.connect(feedback); + feedback.connect(dampen); + dampen.connect(delay); + delay.connect(wet); + wet.connect(this.gainNode); + + notes.forEach((freq, idx) => { + const t = now + idx * 0.95; + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(0.17, t + 0.05); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.85); + osc.connect(env); + env.connect(this.gainNode); + env.connect(delay); + osc.start(t); + osc.stop(t + 0.9); + }); + } + + /** + * Ambient Loop: Periodic distress beacon arpeggio while Icarus II is engaged + */ + startIcarusBeacon(minIntervalMs = 22000, maxIntervalMs = 40000) { + this._scheduleLoop('icarusBeacon', minIntervalMs, maxIntervalMs, () => { + this.synthesizeIcarusBeacon(); + }); + } + + /** + * Extradimensional Psychic Static Whisper (Deep Space, audio_gaps.md #32) + * Menacing psychoacoustic burst: multi-formant filtered white noise with + * randomized micro-envelopes evoking whispered vowels (800-2400 Hz). + */ + synthesizeVoidWhisper() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 2.2; + + const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.12, now + 0.4); + env.gain.linearRampToValueAtTime(0.09, now + 1.3); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + // Three parallel whispered-vowel formants with independent wobbles + const formants = [ + { center: 700, q: 7, wobbleHz: 0.8, wobbleAmt: 160 }, + { center: 1250, q: 8, wobbleHz: 1.1, wobbleAmt: 280 }, + { center: 2200, q: 10, wobbleHz: 1.6, wobbleAmt: 420 } + ]; + formants.forEach((formant) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(formant.center, now); + bp.Q.setValueAtTime(formant.q, now); + + const wobble = ctx.createOscillator(); + wobble.type = 'sine'; + wobble.frequency.setValueAtTime(formant.wobbleHz, now); + const wobbleDepth = ctx.createGain(); + wobbleDepth.gain.setValueAtTime(formant.wobbleAmt, now); + wobble.connect(wobbleDepth); + wobbleDepth.connect(bp.frequency); + + const breath = ctx.createGain(); + breath.gain.setValueAtTime(0.3, now); + // Randomized micro-envelopes: syllable-like gulps of the formant + const gulpCount = 3 + Math.floor(Math.random() * 3); + for (let i = 0; i < gulpCount; i++) { + const gt = now + 0.15 + Math.random() * (duration - 0.55); + breath.gain.setValueAtTime(0.12, gt); + breath.gain.linearRampToValueAtTime(0.3 + Math.random() * 0.18, gt + 0.04); + breath.gain.linearRampToValueAtTime(0.05, gt + 0.12 + Math.random() * 0.1); + } + + noise.connect(bp); + bp.connect(breath); + breath.connect(env); + + wobble.start(now); + wobble.stop(now + duration + 0.05); + }); + + noise.start(now); + noise.stop(now + duration + 0.02); + } + + /** + * Ambient Loop: Psychic whisper bursts on haunted deep-space hulls + */ + startVoidWhispers(minIntervalMs = 15000, maxIntervalMs = 32000) { + this._scheduleLoop('voidWhispers', minIntervalMs, maxIntervalMs, () => { + this.synthesizeVoidWhisper(); + }); + } + + /** + * Beryllium Sphere Resonant Thrum (Comedy, audio_gaps.md #33) + * Deep glassy crystalline reactor hum: dual pure sines (65 + 195 Hz third + * harmonic) with slow beating detune and a subtle comb-filter ring. + * One ~10 s swell phrasing. + */ + synthesizeBerylliumThrumSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 10.0; + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.16, now + 4.0); + env.gain.linearRampToValueAtTime(0.14, now + 6.5); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + // Comb ring: short 5.1 ms reflection + const comb = ctx.createDelay(0.05); + comb.delayTime.setValueAtTime(0.0051, now); + const combFb = ctx.createGain(); + combFb.gain.setValueAtTime(0.45, now); + comb.connect(combFb); + combFb.connect(comb); + const combWet = ctx.createGain(); + combWet.gain.setValueAtTime(0.6, now); + comb.connect(combWet); + combWet.connect(env); + + const partials = [ + { freq: 65, amp: 0.55 }, + { freq: 65.3, amp: 0.3 }, // slow 0.3 Hz beating partner + { freq: 195, amp: 0.3 } + ]; + partials.forEach((partial) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(partial.freq, now); + const g = ctx.createGain(); + g.gain.setValueAtTime(partial.amp, now); + osc.connect(g); + g.connect(env); + g.connect(comb); + osc.start(now); + osc.stop(now + duration + 0.05); + }); + } + + /** + * Ambient Loop: Beryllium sphere thrum while the NSEA Protector is engaged + */ + startBerylliumThrum(minIntervalMs = 9000, maxIntervalMs = 16000) { + this._scheduleLoop('berylliumThrum', minIntervalMs, maxIntervalMs, () => { + this.synthesizeBerylliumThrumSwell(); + }); + } + + /** + * Omega-13 Temporal Capacitor Whine (Comedy, audio_gaps.md #34) + * Tremendous temporal capacitor power buildup: deep 40 Hz sub-bass swelling + * exponentially to 3200 Hz over 3 seconds, culminating in a wide + * white-noise discharge pop. + */ + synthesizeOmega13Whine() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 3.1; + + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(40, now); + osc.frequency.exponentialRampToValueAtTime(3200, now + 3.0); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.exponentialRampToValueAtTime(0.5, now + 2.7); + env.gain.setValueAtTime(0.5, now + 2.95); + env.gain.exponentialRampToValueAtTime(0.001, now + 3.2); + + osc.connect(env); + env.connect(this.gainNode); + osc.start(now); + osc.stop(now + duration + 0.1); + + // Dimensional discharge pop (wide white-noise transient) + const popTime = now + 3.0; + const popBuf = this.am.createNoiseBuffer('white', 0.3); + const pop = ctx.createBufferSource(); + pop.buffer = popBuf; + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(2000, popTime); + const popEnv = ctx.createGain(); + popEnv.gain.setValueAtTime(0.001, popTime); + popEnv.gain.linearRampToValueAtTime(0.42, popTime + 0.008); + popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.16); + pop.connect(hp); + hp.connect(popEnv); + popEnv.connect(this.gainNode); + pop.start(popTime); + pop.stop(popTime + 0.18); + } + + /** + * Repulsorlift Engine Drone (Outlaw, audio_gaps.md #35) + * Quintessential anti-gravity vehicle wash: dual detuned sines (68/72 Hz) + * through an asymmetric overdrive waveshaper, lowpass-filtered at 280 Hz. + * One ~10 s wash phrasing. + */ + synthesizeRepulsorliftDrone() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 10.0; + + let shaperOut = null; + if (ctx.createWaveShaper) { + const shaper = ctx.createWaveShaper(); + shaper.oversample = '2x'; + const curve = new Float32Array(1024); + for (let i = 0; i < 1024; i++) { + const x = (i / 512) - 1; // -1..1 + curve[i] = Math.tanh(2.4 * x); // asymmetric-ish saturation + } + shaper.curve = curve; + shaperOut = shaper; + } + + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(280, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.2, now + 2.6); + env.gain.linearRampToValueAtTime(0.17, now + 7.2); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + + if (shaperOut) shaperOut.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + + [68, 72].forEach((freq) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + osc.connect(shaperOut || lp); + osc.start(now); + osc.stop(now + duration + 0.05); + }); + } + + /** + * Ambient Loop: Repulsorlift wash while the Marauder is engaged + */ + startRepulsorliftDrone(intervalMs = 13000) { + this._scheduleLoop('repulsorliftDrone', intervalMs, intervalMs, () => { + this.synthesizeRepulsorliftDrone(); + }); + } + + /** + * Converted Marine Trawler Engine Chug (Outlaw, audio_gaps.md #36) + * Rhythmic heavy diesel-like piston strokes: low-frequency square pulses + * (4-8 Hz cadence) through an 80 Hz resonant lowpass with mechanical + * piston wheeze. One cluster of 2-4 strokes. + */ + synthesizeBebopChugStroke() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const strokes = 2 + Math.floor(Math.random() * 3); // 2-4 strokes + + const stroke = (t, peak) => { + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(45 + Math.random() * 25, t); + osc.frequency.linearRampToValueAtTime(38, t + 0.3); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(80, t); + lp.Q.setValueAtTime(3, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 0.02); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.42); + osc.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.45); + + // Mechanical piston wheeze + const wheezeBuf = this.am.createNoiseBuffer('pink', 0.3); + const wheeze = ctx.createBufferSource(); + wheeze.buffer = wheezeBuf; + const wheezeBp = ctx.createBiquadFilter(); + wheezeBp.type = 'bandpass'; + wheezeBp.frequency.setValueAtTime(380, t); + wheezeBp.Q.setValueAtTime(1.6, t); + const wheezeEnv = ctx.createGain(); + wheezeEnv.gain.setValueAtTime(0.001, t + 0.02); + wheezeEnv.gain.linearRampToValueAtTime(0.05, t + 0.08); + wheezeEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.3); + wheeze.connect(wheezeBp); + wheezeBp.connect(wheezeEnv); + wheezeEnv.connect(this.gainNode); + wheeze.start(t); + wheeze.stop(t + 0.32); + }; + + for (let i = 0; i < strokes; i++) { + stroke(now + i * (0.34 + Math.random() * 0.2), 0.3 - i * 0.04); + } + } + + /** + * Ambient Loop: Rhythmic diesel chug while the Bebop is engaged + */ + startBebopChug(minIntervalMs = 2200, maxIntervalMs = 4200) { + this._scheduleLoop('bebopChug', minIntervalMs, maxIntervalMs, () => { + this.synthesizeBebopChugStroke(); + }); + } + + /** + * Centrifugal Habitat Carousel Motor Groan (Military, audio_gaps.md #37) + * Deep rotational strain of a kilometer-long rotating drum: sub-audible + * 16 Hz rotational pair with bearing harmonics, modulated by a single + * ~1 RPM (0.0167 Hz) revolution LFO. One 58 s rotation pass. + */ + synthesizeCarouselGroanCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 58.0; + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.55, now + 8.0); + env.gain.linearRampToValueAtTime(0.5, now + 50.0); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + // One rotation per ~60 s: slow strain swell and release + const rotation = ctx.createOscillator(); + rotation.type = 'sine'; + rotation.frequency.setValueAtTime(1 / 60, now); + const rotationDepth = ctx.createGain(); + rotationDepth.gain.setValueAtTime(0.38, now); + rotation.connect(rotationDepth); + rotationDepth.connect(env.gain); + + const partials = [ + { freq: 16, amp: 0.55 }, + { freq: 16.04, amp: 0.32 }, // rotational beating pair + { freq: 48, amp: 0.22 }, // structural harmonic + { freq: 96, amp: 0.07 } // bearing hum harmonic + ]; + partials.forEach((partial) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(partial.freq, now); + const g = ctx.createGain(); + g.gain.setValueAtTime(partial.amp, now); + osc.connect(g); + g.connect(env); + osc.start(now); + osc.stop(now + duration + 0.1); + }); + + rotation.start(now); + rotation.stop(now + duration + 0.1); + } + + /** + * Ambient Loop: Carousel rotation groan while the Agamemnon is engaged + * (one 58 s pass every 60 s; the 2 s gap reads as a rotation splice) + */ + startCarouselGroan(intervalMs = 60000) { + this._scheduleLoop('carouselGroan', intervalMs, intervalMs, () => { + this.synthesizeCarouselGroanCycle(); + }); + } + + /** + * Slipstream Transition Surge (Military, audio_gaps.md #38) + * Ship tearing through an exotic slipstream: dual sweeping bandpass surges + * (200 -> 2400 Hz) with escalating resonance Q and a comb-filter shimmer + * tail. ~4.2 s. + */ + synthesizeSlipstreamSurge() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // Comb shimmer tail bus + const shimmer = ctx.createDelay(0.5); + shimmer.delayTime.setValueAtTime(0.12, now); + const shimmerFb = ctx.createGain(); + shimmerFb.gain.setValueAtTime(0.3, now); + const shimmerLp = ctx.createBiquadFilter(); + shimmerLp.type = 'lowpass'; + shimmerLp.frequency.setValueAtTime(4000, now); + shimmer.connect(shimmerFb); + shimmerFb.connect(shimmerLp); + shimmerLp.connect(shimmer); + const shimmerWet = ctx.createGain(); + shimmerWet.gain.setValueAtTime(0.5, now); + shimmer.connect(shimmerWet); + shimmerWet.connect(this.gainNode); + + const surge = (offset, peak) => { + const t = now + offset; + const osc = ctx.createOscillator(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(200, t); + osc.frequency.exponentialRampToValueAtTime(2400, t + 2.6); + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(200, t); + bp.frequency.exponentialRampToValueAtTime(2400, t + 2.6); + bp.Q.setValueAtTime(2, t); + bp.Q.linearRampToValueAtTime(10, t + 2.6); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 1.4); + env.gain.setValueAtTime(peak, t + 2.7); + env.gain.exponentialRampToValueAtTime(0.001, t + 3.8); + + osc.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + env.connect(shimmer); + osc.start(t); + osc.stop(t + 4.0); + }; + + surge(0, 0.22); + surge(0.3, 0.16); + } + + /** + * Viper Pilot Oxygen Regulator Demand Valve (Military, audio_gaps.md #39) + * Pulsing rebreather of a Viper pilot mid-combat: a sharp 15 ms mechanical + * diaphragm click preceding pulsed highpass-filtered white noise breath + * (1.2-4.5 kHz, ~1.1 s). One breath cycle. + */ + synthesizeOxygenRegulatorCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // Sharp mechanical diaphragm click + const click = ctx.createOscillator(); + click.type = 'triangle'; + click.frequency.setValueAtTime(1800, now); + click.frequency.exponentialRampToValueAtTime(600, now + 0.015); + const clickEnv = ctx.createGain(); + clickEnv.gain.setValueAtTime(0.3, now); + clickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.02); + click.connect(clickEnv); + clickEnv.connect(this.gainNode); + click.start(now); + click.stop(now + 0.025); + + // Regulated airflow breath + const noiseBuf = this.am.createNoiseBuffer('white', 1.3); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(1200, now + 0.02); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(4500, now + 0.02); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now + 0.02); + env.gain.linearRampToValueAtTime(0.13, now + 0.45); + env.gain.linearRampToValueAtTime(0.09, now + 0.62); + env.gain.exponentialRampToValueAtTime(0.001, now + 1.15); + noise.connect(hp); + hp.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + noise.start(now + 0.02); + noise.stop(now + 1.2); + } + + /** + * Ambient Loop: Rhythmic oxygen demand breathing while a Viper cockpit + * is engaged + */ + startOxygenRegulator(minIntervalMs = 3400, maxIntervalMs = 5200) { + this._scheduleLoop('oxygenRegulator', minIntervalMs, maxIntervalMs, () => { + this.synthesizeOxygenRegulatorCycle(); + }); + } + + /** + * Condensation Pipe Drip & Expansion Tick (Industrial, audio_gaps.md #40) + * Lonely water droplet pinging inside a kilometres-long cargo hauler: + * high-Q sine pings (1400-2600 Hz) with fast exponential decay on a + * randomized clock. One drip. + */ + synthesizePipeDrip() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const decay = 0.01 + Math.random() * 0.015; + + const osc = ctx.createOscillator(); + osc.type = 'sine'; + const freq = 1400 + Math.random() * 1200; + osc.frequency.setValueAtTime(freq, now); + osc.frequency.exponentialRampToValueAtTime(freq * 0.92, now + decay); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.1 + Math.random() * 0.2, now + 0.001); + env.gain.exponentialRampToValueAtTime(0.0001, now + decay + 0.01); + + osc.connect(env); + env.connect(this.gainNode); + osc.start(now); + osc.stop(now + decay + 0.03); + } + + /** + * Ambient Loop: Sporadic condensation drips while the Nostromo is engaged + */ + startPipeDrips(minIntervalMs = 500, maxIntervalMs = 3500) { + this._scheduleLoop('pipeDrips', minIntervalMs, maxIntervalMs, () => { + this.synthesizePipeDrip(); + }); + } + + /** + * High-Pressure Steam / Boiler Venting (Industrial, audio_gaps.md #41) + * Continuous hot-vent hiss of a steam-era mining vessel: bandpass-filtered + * white noise (800-2800 Hz) with a slow amplitude swell and random + * micro-flutter. One ~9 s vent pass. + */ + synthesizeSteamVentSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 9.0; + + // Looping noise bed (short buffer, sustained vent) + const noiseBuf = this.am.createNoiseBuffer('white', 4.0); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + noise.loop = true; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(1500, now); + bp.Q.setValueAtTime(0.7, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.17, now + 3.0); + env.gain.linearRampToValueAtTime(0.15, now + 5.5); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + + // Boiler pressure micro-flutter (~6-9 Hz jitter) + const flutter = ctx.createOscillator(); + flutter.type = 'sine'; + flutter.frequency.setValueAtTime(6 + Math.random() * 3, now); + const flutterDepth = ctx.createGain(); + flutterDepth.gain.setValueAtTime(0.04, now); + flutter.connect(flutterDepth); + flutterDepth.connect(env.gain); + + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + duration + 0.1); + flutter.start(now); + flutter.stop(now + duration + 0.1); + } + + /** + * Ambient Loop: Boiler vent passes while Serenity-class and Starbug + * presets are engaged + */ + startSteamVent(minIntervalMs = 6000, maxIntervalMs = 10000) { + this._scheduleLoop('steamVent', minIntervalMs, maxIntervalMs, () => { + this.synthesizeSteamVentSwell(); + }); + } + + /** + * Crash-Couch Hydraulic Gimbal Strain (Industrial, audio_gaps.md #42) + * Pilot's couch swinging into launch position: FM triangle carrier + * (140 Hz / 35 Hz modulator) through lowpass damping that tracks the + * acceleration strain. ~1.9 s. + */ + synthesizeCrashCouchGimbal() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.9; + + const layer = (offset, carrierFreq, modFreq) => { + const t = now + offset; + const carrier = ctx.createOscillator(); + carrier.type = 'triangle'; + carrier.frequency.setValueAtTime(carrierFreq, t); + + const mod = ctx.createOscillator(); + mod.type = 'sine'; + mod.frequency.setValueAtTime(modFreq, t); + const modDepth = ctx.createGain(); + modDepth.gain.setValueAtTime(carrierFreq * 4, t); + mod.connect(modDepth); + modDepth.connect(carrier.frequency); + + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(300, t); + lp.frequency.linearRampToValueAtTime(900, t + 0.5); + lp.frequency.exponentialRampToValueAtTime(350, t + duration - offset); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(0.24, t + 0.55); + env.gain.linearRampToValueAtTime(0.2, t + 1.0); + env.gain.exponentialRampToValueAtTime(0.0001, t + duration - offset); + + carrier.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + carrier.start(t); + carrier.stop(t + (duration - offset) + 0.05); + mod.start(t); + mod.stop(t + (duration - offset) + 0.05); + }; + + layer(0, 140, 35); + layer(0.42, 152, 38); + } + + /** + * HAL 9000 Breathing Loop (Retro Future, audio_gaps.md #43) + * Eerie slow respiration of a quiet, observant ship computer: rhythmic + * bandpass-filtered pink noise (450-1100 Hz) with a gentle 3.5 s + * inhalation/exhalation envelope. One breath. + */ + synthesizeHalBreathCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const inhale = 1.3; + const exhale = 2.2; + + const noiseBuf = this.am.createNoiseBuffer('pink', 4.0); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + noise.loop = true; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(700, now); + bp.Q.setValueAtTime(1.2, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.085, now + inhale); + env.gain.exponentialRampToValueAtTime(0.001, now + inhale + exhale); + + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + inhale + exhale + 0.05); + } + + /** + * Ambient Loop: Continuous respiration while the Discovery One is engaged + */ + startHalBreathing(intervalMs = 3500) { + this._scheduleLoop('halBreathing', intervalMs, intervalMs, () => { + this.synthesizeHalBreathCycle(); + }); + } + + /** + * Death Blossom Energy Surge Ramp (Retro Future, audio_gaps.md #44) + * Fearsome gunstar overdrive: cascaded sawtooth exponential ramp + * (200 Hz -> 8000 Hz over 2.5 s) with rising overdrive, cutting off + * abruptly at full bloom. + */ + synthesizeDeathBlossomSurge() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const rampDur = 2.5; + + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(400, now); + hp.frequency.exponentialRampToValueAtTime(4000, now + rampDur); + + const hp2 = ctx.createBiquadFilter(); + hp2.type = 'highpass'; + hp2.frequency.setValueAtTime(200, now); + hp2.frequency.exponentialRampToValueAtTime(2000, now + rampDur); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.32, now + rampDur); + env.gain.setValueAtTime(0.32, now + rampDur + 0.03); + env.gain.exponentialRampToValueAtTime(0.001, now + rampDur + 0.4); + + hp.connect(hp2); + hp2.connect(env); + env.connect(this.gainNode); + + // Cascaded overdriven saws with staggered ignition + [0, 0.15, 0.3].forEach((offset, idx) => { + const t = now + offset; + const osc = ctx.createOscillator(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(200, t); + osc.frequency.exponentialRampToValueAtTime(8000, t + rampDur - offset); + const g = ctx.createGain(); + g.gain.setValueAtTime(0.33 - idx * 0.08, t); + osc.connect(g); + g.connect(hp); + osc.start(t); + osc.stop(t + (rampDur - offset) + 0.45); + }); + + // Abrupt bloom transient + const burstTime = now + rampDur; + const burstBuf = this.am.createNoiseBuffer('white', 0.25); + const burst = ctx.createBufferSource(); + burst.buffer = burstBuf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(3500, burstTime); + bp.Q.setValueAtTime(1.5, burstTime); + const burstEnv = ctx.createGain(); + burstEnv.gain.setValueAtTime(0.001, burstTime); + burstEnv.gain.linearRampToValueAtTime(0.3, burstTime + 0.01); + burstEnv.gain.exponentialRampToValueAtTime(0.001, burstTime + 0.22); + burst.connect(bp); + bp.connect(burstEnv); + burstEnv.connect(this.gainNode); + burst.start(burstTime); + burst.stop(burstTime + 0.25); + } + + /** + * Victorian Steam Piston Chug (Retro Future, audio_gaps.md #45) + * The steam-age skeleton of the Cygnus: lowpass-filtered noise bursts + * synchronized with heavy 40 Hz triangle thumps and a metallic slapback. + * One cluster of 2-3 stroke cycles. + */ + synthesizeCygnusPistonChug() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const strokes = 2 + Math.floor(Math.random() * 2); // 2-3 strokes + + const slap = ctx.createDelay(0.6); + slap.delayTime.setValueAtTime(0.35, now); + const slapFb = ctx.createGain(); + slapFb.gain.setValueAtTime(0.3, now); + const slapWet = ctx.createGain(); + slapWet.gain.setValueAtTime(0.4, now); + slap.connect(slapFb); + slapFb.connect(slap); + slap.connect(slapWet); + slapWet.connect(this.gainNode); + + const stroke = (t, peak) => { + // Heavy piston body thump + const thump = ctx.createOscillator(); + thump.type = 'triangle'; + thump.frequency.setValueAtTime(40, t); + thump.frequency.linearRampToValueAtTime(32, t + 0.4); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(120, t); + const thumpEnv = ctx.createGain(); + thumpEnv.gain.setValueAtTime(0.001, t); + thumpEnv.gain.linearRampToValueAtTime(peak, t + 0.02); + thumpEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.45); + thump.connect(lp); + lp.connect(thumpEnv); + thumpEnv.connect(this.gainNode); + thumpEnv.connect(slap); + thump.start(t); + thump.stop(t + 0.5); + + // Synchronized steam exhaust puff + const puffBuf = this.am.createNoiseBuffer('white', 0.16); + const puff = ctx.createBufferSource(); + puff.buffer = puffBuf; + const puffLp = ctx.createBiquadFilter(); + puffLp.type = 'lowpass'; + puffLp.frequency.setValueAtTime(500, t); + const puffEnv = ctx.createGain(); + puffEnv.gain.setValueAtTime(0.001, t + 0.01); + puffEnv.gain.linearRampToValueAtTime(0.13, t + 0.03); + puffEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.15); + puff.connect(puffLp); + puffLp.connect(puffEnv); + puffEnv.connect(this.gainNode); + puff.start(t); + puff.stop(t + 0.17); + }; + + for (let i = 0; i < strokes; i++) { + stroke(now + i * (0.5 + Math.random() * 0.25), 0.26 - i * 0.03); + } + } + + /** + * Ambient Loop: Steam piston chug while the Cygnus is engaged + */ + startCygnusChug(minIntervalMs = 1800, maxIntervalMs = 3200) { + this._scheduleLoop('cygnusChug', minIntervalMs, maxIntervalMs, () => { + this.synthesizeCygnusPistonChug(); + }); + } + stopAllLoops() { this.stopMedicalMonitor(); this.stopStationSparks(); + Object.keys(this.loopTimers).forEach((key) => { + this._stopLoop(key); + }); } } @@ -6710,8 +8277,10 @@ const UniverseRegistry = { { id: 'btn-door-swish', label: 'DOOR PNEUMATIC SWISH' }, { id: 'btn-bosun-whistle', label: "BOSUN'S PIPE WHISTLE" }, { id: 'btn-medical-monitor', label: 'SICKBAY ECG MONITOR' }, - { id: 'btn-chirp-single', label: 'LCARS SINGLE CHIRP' }, - { id: 'btn-chirp-ack', label: 'LCARS DATA ACK' } + { id: 'btn-cardassian-door', label: 'CARDASSIAN BULKHEAD DOOR' }, + { id: 'btn-turbolift', label: 'TURBOLIFT WHOOSH' }, + { id: 'btn-replicator', label: 'REPLICATOR SHIMMER' }, + { id: 'btn-chirp-single', label: 'LCARS SINGLE CHIRP' } ], presets: StarshipPresets }, @@ -6738,7 +8307,9 @@ const UniverseRegistry = { { id: 'btn-tardis-door', label: 'POLICE BOX WOODEN DOOR' }, { id: 'btn-fast-return', label: 'FAST-RETURN LEVER' }, { id: 'btn-demat-switch', label: 'DEMAT SWITCH' }, - { id: 'btn-telepathic', label: 'TELEPATHIC CHIME' } + { id: 'btn-telepathic', label: 'TELEPATHIC CHIME' }, + { id: 'btn-scanner-activate', label: 'SCANNER ACTIVATION' }, + { id: 'btn-type-clatter', label: 'CONSOLE TYPE CLATTER' } ], presets: { 'tardis-1963': { @@ -6847,9 +8418,9 @@ const UniverseRegistry = { soundboard: [ { id: 'btn-geiger', label: 'GEIGER COUNTER' }, { id: 'btn-dock-clamp', label: 'HYDRAULIC CLAMPS' }, - { id: 'btn-relay-click', label: 'M/TH/UR TELETYPE' }, - { id: 'btn-switch-pop', label: 'HEAVY SOLENOID' }, - { id: 'btn-pneumatic-door', label: 'PNEUMATIC SEAL PURGE' }, + { 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' } ], presets: { @@ -6953,12 +8524,12 @@ const UniverseRegistry = { { id: 'btn-bio-defense', label: 'BIO-DEFENSE BURST', type: 'biodefense' } ], soundboard: [ - { id: 'btn-telepathic-chime', label: 'NEURAL TWITCH' }, - { id: 'btn-fast-return', label: 'VASCULAR PUMP' }, - { id: 'btn-demat-switch', label: 'CHITIN CREAK' }, - { id: 'btn-chirp-sweep', label: 'BIOPLASMIC HISS' }, - { id: 'btn-sonic', label: 'SYMBIOTE VOCALIZATION' }, - { id: 'btn-relay-click', label: 'MEMBRANE FLUTTER' } + { id: 'btn-neural-bond', label: 'NEURAL-BOND SWELL' }, + { id: 'btn-fast-return', label: 'VASCULAR PUMP' }, + { id: 'btn-demat-switch', label: 'CHITIN CREAK' }, + { id: 'btn-chirp-sweep', label: 'BIOPLASMIC HISS' }, + { id: 'btn-vorlon-song', label: 'VORLON SINGING RESONANCE' }, + { id: 'btn-hull-regen', label: 'HULL REGEN TEXTURE' } ], presets: { 'bio-moya': { @@ -7041,12 +8612,12 @@ const UniverseRegistry = { { id: 'btn-hal-override', label: 'HAL 9000 OVERRIDE', type: 'hal' } ], soundboard: [ - { id: 'btn-hal-chime', label: 'HAL LOGIC CONFIRM' }, - { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' }, + { id: 'btn-hal-breath', label: 'HAL RESPIRATION' }, + { id: 'btn-death-blossom',label: 'DEATH BLOSSOM SURGE' }, + { id: 'btn-cygnus-chug', label: 'STEAM PISTON CHUG' }, { id: 'btn-chirp-single', label: 'ASTROGATOR BEAT' }, { id: 'btn-chirp-sweep', label: 'ANALOG GLISSANDO' }, - { id: 'btn-fast-return', label: 'MAGNETIC TAPE LATCH' }, - { id: 'btn-dock-clamp', label: 'PNEUMATIC SEAL PURGE' } + { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' } ], presets: { 'ret-jupiter-2': { @@ -7149,12 +8720,12 @@ const UniverseRegistry = { { id: 'btn-flak', label: 'FLAK BARRAGE', type: 'flak' } ], soundboard: [ - { id: 'btn-dradis-ping', label: 'DRADIS SCAN PING' }, - { id: 'btn-switch-pop', label: 'WEAPONS ARMED' }, - { id: 'btn-dock-clamp', label: 'BULKHEAD SEAL' }, - { id: 'btn-chirp-sweep', label: 'RADAR TARGET LOCK' }, - { id: 'btn-fast-return', label: 'TURBO-THRUSTER SWELL' }, - { id: 'btn-air-handler', label: 'RCS THRUSTER BURST' } + { id: 'btn-dradis-ping', label: 'DRADIS SCAN PING' }, + { id: 'btn-switch-pop', label: 'WEAPONS ARMED' }, + { 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' } ], presets: { 'mil-sulaco': { @@ -7247,12 +8818,12 @@ const UniverseRegistry = { { id: 'btn-solar-swell', label: 'SOLAR SHIELD SWELL', type: 'solar' } ], soundboard: [ - { id: 'btn-solar-roar', label: 'HEAT SHIELD ROAR' }, - { id: 'btn-cryo-sigh', label: 'CRYO POD SIGH' }, - { id: 'btn-orion-thump', label: 'ORION PULSE THUMP' }, - { id: 'btn-dock-clamp', label: 'HULL STRAIN MOAN' }, - { id: 'btn-telepathic-chime', label: 'VOID RESONANCE' }, - { id: 'btn-chirp-sweep', label: 'TACHYON SENSOR PING' } + { id: 'btn-solar-roar', label: 'HEAT SHIELD ROAR' }, + { id: 'btn-cryo-sigh', label: 'CRYO POD SIGH' }, + { id: 'btn-orion-thump', label: 'ORION PULSE THUMP' }, + { id: 'btn-hull-strain', label: 'TRUE HULL STRAIN MOAN' }, + { id: 'btn-void-whisper',label: 'PSYCHIC VOID WHISPER' }, + { id: 'btn-icarus-beacon', label: 'ICARUS I DISTRESS BEACON' } ], presets: { 'deep-avalon': { @@ -7348,9 +8919,9 @@ const UniverseRegistry = { { id: 'btn-tape-clunk', label: 'CASSETTE CLUNK & WHIRR' }, { id: 'btn-grappler-servo', label: 'GRAPPLER SERVO MOTOR' }, { id: 'btn-radio-static', label: 'RF SQUELCH STATIC BURST' }, - { id: 'btn-dock-clamp', label: 'FUEL LINE VALVE' }, - { id: 'btn-afterburner-snd',label: 'AFTERBURNER BOOST' }, - { id: 'btn-fast-return', label: 'TURBO JET BURN' } + { id: 'btn-repulsor-drone', label: 'REPULSORLIFT DRONE' }, + { id: 'btn-bebop-chug', label: 'MARINE DIESEL CHUG' }, + { id: 'btn-afterburner-snd',label: 'AFTERBURNER BOOST' } ], presets: { 'out-betty': { @@ -7438,7 +9009,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-dock-clamp', label: 'DOCKING CLAMP LATCH' } + { id: 'btn-belter-telemetry', label: 'BELTER JURY-RIG TELEMETRY', span: 2 } ], presets: { 'sta-babylon-5': { @@ -7525,8 +9096,8 @@ const UniverseRegistry = { { id: 'btn-tea-machine', label: 'TEA DISPENSER GURGLE' }, { id: 'btn-kettle-whistle', label: 'KETTLE STEAM WHISTLE' }, { id: 'btn-plaid-alarm', label: 'LUDICROUS PLAID ALARM' }, - { id: 'btn-beryllium-pulse',label: 'BERYLLIUM CORE PULSE' }, - { id: 'btn-chirp-ack', label: 'ORVILLE ACK CHIRP' } + { id: 'btn-beryllium-thrum',label: 'BERYLLIUM SPHERE THRUM' }, + { id: 'btn-omega-13', label: 'OMEGA-13 CAPACITOR WHINE' } ], presets: { 'com-orville': { @@ -11939,6 +13510,41 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // 4a. Phase 2 Preset Ambience + // Single funnel that stops every automated loop (legacy + registry) and then + // re-engages the ambience matching the currently selected preset. + function startPresetAmbience(presetId) { + expandedAudio.stopAllLoops(); + whoniverseAudio.stopAmbientLoops(); + + // Legacy Phase 1 loops, kept on their original presets + if (presetId === 'sta-sevastopol') { + expandedAudio.startStationSparks(4000, 11000); + } else if (presetId === 'sta-gateway' || presetId.includes('sickbay') || presetId.includes('med')) { + expandedAudio.startMedicalMonitor(1.15); + } + + // 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 + 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 + if (presetId === 'out-marauder') expandedAudio.startRepulsorliftDrone(); // #35 + if (presetId === 'out-bebop') expandedAudio.startBebopChug(); // #36 + if (presetId === 'mil-agamemnon') expandedAudio.startCarouselGroan(); // #37 + if (presetId === 'mil-viper') expandedAudio.startOxygenRegulator(); // #39 + if (presetId === 'ind-nostromo') expandedAudio.startPipeDrips(); // #40 + if (presetId === 'ind-serenity' || presetId === 'ind-starbug') expandedAudio.startSteamVent(); // #41 + if (presetId === 'ret-discovery-one') expandedAudio.startHalBreathing(); // #43 + if (presetId === 'ret-cygnus') expandedAudio.startCygnusChug(); // #45 + } + // 4. Select & Apply Preset function selectPreset(presetId) { const universe = UniverseRegistry[activeUniverseId]; @@ -11967,13 +13573,9 @@ document.addEventListener('DOMContentLoaded', () => { syncSlidersToSynthParams(); expandedAudio.stopAllLoops(); + whoniverseAudio.stopAmbientLoops(); if (isPlaying) { - if (presetId === 'sta-sevastopol') { - expandedAudio.startStationSparks(4000, 11000); - } else if (presetId === 'sta-gateway' || presetId.includes('sickbay') || presetId.includes('med')) { - expandedAudio.startMedicalMonitor(1.15); - } - + startPresetAmbience(presetId); if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeDematSwitch(); else if (activeUniverseId === 'industrial') expandedAudio.synthesizeDockingClamp(); else telemetry.synthesizeLCARSSingleChirp(); @@ -12056,10 +13658,8 @@ document.addEventListener('DOMContentLoaded', () => { statAudioStatus.textContent = 'ONLINE'; statAudioStatus.style.color = '#00ff88'; - if (activePresetId === 'sta-sevastopol') { - expandedAudio.startStationSparks(4000, 11000); - } else if (activePresetId === 'sta-gateway' || activePresetId.includes('sickbay') || activePresetId.includes('med')) { - expandedAudio.startMedicalMonitor(1.15); + if (activePresetId) { + startPresetAmbience(activePresetId); } }, ENGINE_TRANSITION_SECS * 1000); @@ -12084,6 +13684,7 @@ document.addEventListener('DOMContentLoaded', () => { lifeSupport.stop(ENGINE_TRANSITION_SECS); telemetry.stop(); expandedAudio.stopAllLoops(); + whoniverseAudio.stopAmbientLoops(); alerts.stopAlert(); whoniverseAudio.stopCloisterBell(); resetAlertButtons(); @@ -12288,7 +13889,8 @@ document.addEventListener('DOMContentLoaded', () => { visualizer.spawnWarpPulses(); break; case 'neural': - whoniverseAudio.synthesizeTelepathicChime(); + expandedAudio.synthesizeNeuralBondSwell(); + visualizer.spawnWarpPulses(); break; case 'centrifuge': case 'astrogator': @@ -12469,6 +14071,83 @@ document.addEventListener('DOMContentLoaded', () => { expandedAudio.synthesizeHalChime(); break; + // Phase 2 (audio_gaps.md #21-#45) + case 'btn-cardassian-door': + expandedAudio.synthesizeCardassianDoor(); + break; + case 'btn-turbolift': + expandedAudio.synthesizeTurboliftWhoosh(); + break; + case 'btn-replicator': + expandedAudio.synthesizeReplicatorShimmer(); + break; + case 'btn-scanner-activate': + whoniverseAudio.synthesizeScannerActivate(); + break; + case 'btn-type-clatter': + whoniverseAudio.synthesizeTypeClatter(); + break; + case 'btn-vorlon-song': + expandedAudio.synthesizeVorlonSingingSwell(); + break; + case 'btn-neural-bond': + expandedAudio.synthesizeNeuralBondSwell(); + break; + case 'btn-hull-regen': + expandedAudio.synthesizeHullRegenBurst(); + break; + case 'btn-belter-telemetry': + expandedAudio.synthesizeBelterTelemetryJitter(); + break; + case 'btn-hull-strain': + expandedAudio.synthesizeHullStrainMoan(); + break; + case 'btn-icarus-beacon': + expandedAudio.synthesizeIcarusBeacon(); + break; + case 'btn-void-whisper': + expandedAudio.synthesizeVoidWhisper(); + break; + case 'btn-beryllium-thrum': + expandedAudio.synthesizeBerylliumThrumSwell(); + break; + case 'btn-omega-13': + expandedAudio.synthesizeOmega13Whine(); + break; + case 'btn-repulsor-drone': + expandedAudio.synthesizeRepulsorliftDrone(); + break; + case 'btn-bebop-chug': + expandedAudio.synthesizeBebopChugStroke(); + break; + case 'btn-carousel-groan': + expandedAudio.synthesizeCarouselGroanCycle(); + break; + case 'btn-slipstream': + expandedAudio.synthesizeSlipstreamSurge(); + break; + case 'btn-oxygen-reg': + expandedAudio.synthesizeOxygenRegulatorCycle(); + break; + case 'btn-pipe-drips': + expandedAudio.synthesizePipeDrip(); + break; + case 'btn-steam-vent': + expandedAudio.synthesizeSteamVentSwell(); + break; + case 'btn-crash-couch': + expandedAudio.synthesizeCrashCouchGimbal(); + break; + case 'btn-hal-breath': + expandedAudio.synthesizeHalBreathCycle(); + break; + case 'btn-death-blossom': + expandedAudio.synthesizeDeathBlossomSurge(); + break; + case 'btn-cygnus-chug': + expandedAudio.synthesizeCygnusPistonChug(); + break; + default: telemetry.synthesizeLCARSSingleChirp(); break; diff --git a/js/app.js b/js/app.js index 04acf68..fcfe877 100644 --- a/js/app.js +++ b/js/app.js @@ -283,6 +283,41 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // 4a. Phase 2 Preset Ambience + // Single funnel that stops every automated loop (legacy + registry) and then + // re-engages the ambience matching the currently selected preset. + function startPresetAmbience(presetId) { + expandedAudio.stopAllLoops(); + whoniverseAudio.stopAmbientLoops(); + + // Legacy Phase 1 loops, kept on their original presets + if (presetId === 'sta-sevastopol') { + expandedAudio.startStationSparks(4000, 11000); + } else if (presetId === 'sta-gateway' || presetId.includes('sickbay') || presetId.includes('med')) { + expandedAudio.startMedicalMonitor(1.15); + } + + // 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 + 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 + if (presetId === 'out-marauder') expandedAudio.startRepulsorliftDrone(); // #35 + if (presetId === 'out-bebop') expandedAudio.startBebopChug(); // #36 + if (presetId === 'mil-agamemnon') expandedAudio.startCarouselGroan(); // #37 + if (presetId === 'mil-viper') expandedAudio.startOxygenRegulator(); // #39 + if (presetId === 'ind-nostromo') expandedAudio.startPipeDrips(); // #40 + if (presetId === 'ind-serenity' || presetId === 'ind-starbug') expandedAudio.startSteamVent(); // #41 + if (presetId === 'ret-discovery-one') expandedAudio.startHalBreathing(); // #43 + if (presetId === 'ret-cygnus') expandedAudio.startCygnusChug(); // #45 + } + // 4. Select & Apply Preset function selectPreset(presetId) { const universe = UniverseRegistry[activeUniverseId]; @@ -311,13 +346,9 @@ document.addEventListener('DOMContentLoaded', () => { syncSlidersToSynthParams(); expandedAudio.stopAllLoops(); + whoniverseAudio.stopAmbientLoops(); if (isPlaying) { - if (presetId === 'sta-sevastopol') { - expandedAudio.startStationSparks(4000, 11000); - } else if (presetId === 'sta-gateway' || presetId.includes('sickbay') || presetId.includes('med')) { - expandedAudio.startMedicalMonitor(1.15); - } - + startPresetAmbience(presetId); if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeDematSwitch(); else if (activeUniverseId === 'industrial') expandedAudio.synthesizeDockingClamp(); else telemetry.synthesizeLCARSSingleChirp(); @@ -400,10 +431,8 @@ document.addEventListener('DOMContentLoaded', () => { statAudioStatus.textContent = 'ONLINE'; statAudioStatus.style.color = '#00ff88'; - if (activePresetId === 'sta-sevastopol') { - expandedAudio.startStationSparks(4000, 11000); - } else if (activePresetId === 'sta-gateway' || activePresetId.includes('sickbay') || activePresetId.includes('med')) { - expandedAudio.startMedicalMonitor(1.15); + if (activePresetId) { + startPresetAmbience(activePresetId); } }, ENGINE_TRANSITION_SECS * 1000); @@ -428,6 +457,7 @@ document.addEventListener('DOMContentLoaded', () => { lifeSupport.stop(ENGINE_TRANSITION_SECS); telemetry.stop(); expandedAudio.stopAllLoops(); + whoniverseAudio.stopAmbientLoops(); alerts.stopAlert(); whoniverseAudio.stopCloisterBell(); resetAlertButtons(); @@ -632,7 +662,8 @@ document.addEventListener('DOMContentLoaded', () => { visualizer.spawnWarpPulses(); break; case 'neural': - whoniverseAudio.synthesizeTelepathicChime(); + expandedAudio.synthesizeNeuralBondSwell(); + visualizer.spawnWarpPulses(); break; case 'centrifuge': case 'astrogator': @@ -813,6 +844,83 @@ document.addEventListener('DOMContentLoaded', () => { expandedAudio.synthesizeHalChime(); break; + // Phase 2 (audio_gaps.md #21-#45) + case 'btn-cardassian-door': + expandedAudio.synthesizeCardassianDoor(); + break; + case 'btn-turbolift': + expandedAudio.synthesizeTurboliftWhoosh(); + break; + case 'btn-replicator': + expandedAudio.synthesizeReplicatorShimmer(); + break; + case 'btn-scanner-activate': + whoniverseAudio.synthesizeScannerActivate(); + break; + case 'btn-type-clatter': + whoniverseAudio.synthesizeTypeClatter(); + break; + case 'btn-vorlon-song': + expandedAudio.synthesizeVorlonSingingSwell(); + break; + case 'btn-neural-bond': + expandedAudio.synthesizeNeuralBondSwell(); + break; + case 'btn-hull-regen': + expandedAudio.synthesizeHullRegenBurst(); + break; + case 'btn-belter-telemetry': + expandedAudio.synthesizeBelterTelemetryJitter(); + break; + case 'btn-hull-strain': + expandedAudio.synthesizeHullStrainMoan(); + break; + case 'btn-icarus-beacon': + expandedAudio.synthesizeIcarusBeacon(); + break; + case 'btn-void-whisper': + expandedAudio.synthesizeVoidWhisper(); + break; + case 'btn-beryllium-thrum': + expandedAudio.synthesizeBerylliumThrumSwell(); + break; + case 'btn-omega-13': + expandedAudio.synthesizeOmega13Whine(); + break; + case 'btn-repulsor-drone': + expandedAudio.synthesizeRepulsorliftDrone(); + break; + case 'btn-bebop-chug': + expandedAudio.synthesizeBebopChugStroke(); + break; + case 'btn-carousel-groan': + expandedAudio.synthesizeCarouselGroanCycle(); + break; + case 'btn-slipstream': + expandedAudio.synthesizeSlipstreamSurge(); + break; + case 'btn-oxygen-reg': + expandedAudio.synthesizeOxygenRegulatorCycle(); + break; + case 'btn-pipe-drips': + expandedAudio.synthesizePipeDrip(); + break; + case 'btn-steam-vent': + expandedAudio.synthesizeSteamVentSwell(); + break; + case 'btn-crash-couch': + expandedAudio.synthesizeCrashCouchGimbal(); + break; + case 'btn-hal-breath': + expandedAudio.synthesizeHalBreathCycle(); + break; + case 'btn-death-blossom': + expandedAudio.synthesizeDeathBlossomSurge(); + break; + case 'btn-cygnus-chug': + expandedAudio.synthesizeCygnusPistonChug(); + break; + default: telemetry.synthesizeLCARSSingleChirp(); break; diff --git a/js/audio.js b/js/audio.js index 9b48ccf..5c6c227 100644 --- a/js/audio.js +++ b/js/audio.js @@ -1557,6 +1557,7 @@ class WhoniverseAudioSynth { this.gainNode = null; this.activeCloister = false; this.cloisterTimer = null; + this.typeTelemetryTimer = null; } init() { @@ -1964,6 +1965,166 @@ class WhoniverseAudioSynth { click.stop(now + offset + 0.04); }); } + + /** + * Scanner / Monitor Screen Activation (Whoniverse, audio_gaps.md #24) + * Rapid upward sine sweep (200 Hz -> 1600 Hz over 120 ms) terminating in a + * CRT flyback whine. The 15.6 kHz spec tail is compromised to ~10.6 kHz: + * near/above Nyquist on low-rate contexts and ear-fatiguing at audible levels. + */ + synthesizeScannerActivate() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // 1. Cathode-ray startup pitch sweep + const sweep = ctx.createOscillator(); + sweep.type = 'sine'; + sweep.frequency.setValueAtTime(200, now); + sweep.frequency.exponentialRampToValueAtTime(1600, now + 0.12); + + const sweepEnv = ctx.createGain(); + sweepEnv.gain.setValueAtTime(0.001, now); + sweepEnv.gain.linearRampToValueAtTime(0.32, now + 0.03); + sweepEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.16); + + sweep.connect(sweepEnv); + sweepEnv.connect(this.gainNode); + sweep.start(now); + sweep.stop(now + 0.17); + + // 2. CRT flyback whine partial (10.6 kHz compromise, low amplitude) + const whine = ctx.createOscillator(); + whine.type = 'sine'; + whine.frequency.setValueAtTime(10600, now + 0.1); + + const wobble = ctx.createOscillator(); + wobble.type = 'sine'; + wobble.frequency.setValueAtTime(1.0, now + 0.1); + const wobbleDepth = ctx.createGain(); + wobbleDepth.gain.setValueAtTime(8, now + 0.1); + + wobble.connect(wobbleDepth); + wobbleDepth.connect(whine.frequency); + + const whineEnv = ctx.createGain(); + whineEnv.gain.setValueAtTime(0.001, now + 0.1); + whineEnv.gain.linearRampToValueAtTime(0.03, now + 0.16); + whineEnv.gain.exponentialRampToValueAtTime(0.0001, now + 0.85); + + whine.connect(whineEnv); + whineEnv.connect(this.gainNode); + whine.start(now + 0.1); + whine.stop(now + 0.87); + wobble.start(now + 0.1); + wobble.stop(now + 0.87); + } + + /** + * Console "Type" Input Clatter Cluster (Whoniverse, audio_gaps.md #25) + * Rhythmic mechanical clatter of toggle switches, spring-loaded buttons and + * tumbler relays: clustered wooden/plastic clicks and solenoid snaps rather + * than tonal beeps. Produces a single 0.3-0.6 s cluster of 3-7 transients. + */ + synthesizeTypeClatter() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const transientCount = 3 + Math.floor(Math.random() * 5); // 3-7 strikes + let cursor = 0; + + const strike = (kind, offset, peak) => { + const t = now + offset; + if (kind === 'toggle') { + // Wooden relay toggle: short bandpass pink-noise burst with downward snap + const buf = this.am.createNoiseBuffer('pink', 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(700 + Math.random() * 700, t); + bp.frequency.exponentialRampToValueAtTime(240, t + 0.03); + bp.Q.setValueAtTime(8 + Math.random() * 4, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 0.005); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.045); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.05); + } else if (kind === 'button') { + // Spring-loaded plastic button: fast square click collapsing in pitch + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(900, t); + osc.frequency.exponentialRampToValueAtTime(200, t + 0.012); + const env = ctx.createGain(); + env.gain.setValueAtTime(peak * 0.8, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.022); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.025); + } else { + // Heavy solenoid snap: dull low square thump + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(120, t); + osc.frequency.exponentialRampToValueAtTime(55, t + 0.025); + const env = ctx.createGain(); + env.gain.setValueAtTime(peak, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.035); + osc.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.04); + } + }; + + for (let i = 0; i < transientCount; i++) { + const kinds = ['toggle', 'toggle', 'button', 'button', 'solenoid']; + const kind = kinds[Math.floor(Math.random() * kinds.length)]; + cursor += 0.015 + Math.random() * 0.075; + strike(kind, cursor, 0.07 + Math.random() * 0.07); + } + } + + /** + * Ambient Loop: TARDIS Console "Type" Input Telemetry (#25) + * Recursive random-window clatter while any TARDIS console room is engaged. + */ + startConsoleTypeTelemetry(minIntervalMs = 2800, maxIntervalMs = 6500) { + this.stopConsoleTypeTelemetry(); + const scheduleNext = () => { + const delay = minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); + this.typeTelemetryTimer = setTimeout(() => { + this.synthesizeTypeClatter(); + scheduleNext(); + }, delay); + }; + scheduleNext(); + } + + stopConsoleTypeTelemetry() { + if (this.typeTelemetryTimer) { + clearTimeout(this.typeTelemetryTimer); + this.typeTelemetryTimer = null; + } + } + + /** + * Stops all automated Whoniverse ambient loops. Cloister bell is an + * interactive toggle and keeps its own explicit call sites. + */ + stopAmbientLoops() { + this.stopConsoleTypeTelemetry(); + } } window.WhoniverseAudioSynth = WhoniverseAudioSynth; @@ -1981,6 +2142,7 @@ class ExpandedSciFiAudioSynth { this.gainNode = null; this.medicalTimer = null; this.sparkTimer = null; + this.loopTimers = {}; } init() { @@ -3381,9 +3543,1414 @@ class ExpandedSciFiAudioSynth { } } + /** + * Ambient Loop Registry (Phase 2) + * Generic recursive-timeout scheduler keyed by name. A start call always + * kills any previous instance of the same key first. If min === max the + * cadence is fixed; otherwise it is random within [min, max]. + */ + _scheduleLoop(key, minIntervalMs, maxIntervalMs, fn) { + this._stopLoop(key); + const scheduleNext = () => { + const delay = maxIntervalMs === minIntervalMs + ? minIntervalMs + : minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs); + this.loopTimers[key] = setTimeout(() => { + fn(); + scheduleNext(); + }, delay); + }; + scheduleNext(); + } + + _stopLoop(key) { + if (this.loopTimers[key]) { + clearTimeout(this.loopTimers[key]); + delete this.loopTimers[key]; + } + } + + /** + * Cardassian Bulkhead Door Grind (Star Trek DS9, audio_gaps.md #21) + * Oppressive pneumatic bulkhead grinding open: resonant bandpass sweep + * across brown noise (120 Hz -> 650 Hz) layered with a scraping metallic + * saw undertone. + */ + synthesizeCardassianDoor() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.4; + + const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.2); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(120, now); + bp.frequency.exponentialRampToValueAtTime(650, now + 1.1); + bp.Q.setValueAtTime(3.5, now); + + // Scraping metallic saw undertone through the same sweeping bandpass + const saw = ctx.createOscillator(); + saw.type = 'sawtooth'; + saw.frequency.setValueAtTime(110, now); + saw.frequency.exponentialRampToValueAtTime(70, now + duration); + const sawGain = ctx.createGain(); + sawGain.gain.setValueAtTime(0.001, now); + sawGain.gain.linearRampToValueAtTime(0.09, now + 0.6); + sawGain.gain.exponentialRampToValueAtTime(0.001, now + duration); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.5, now + 0.4); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + noise.connect(bp); + saw.connect(sawGain); + sawGain.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + + noise.start(now); + noise.stop(now + duration + 0.05); + saw.start(now); + saw.stop(now + duration + 0.05); + } + + /** + * Turbolift Pass Whoosh (Star Trek TNG/VOY, audio_gaps.md #22) + * Bandpass-filtered white noise pitch sweep (180 -> 450 Hz and back) with + * resonant boost and soft stereo panning drift past the listener. + */ + synthesizeTurboliftWhoosh() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.6; + + const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(180, now); + bp.frequency.exponentialRampToValueAtTime(450, now + 0.55); + bp.frequency.exponentialRampToValueAtTime(180, now + 1.4); + bp.Q.setValueAtTime(2.2, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.42, now + 0.5); + env.gain.linearRampToValueAtTime(0.3, now + 0.8); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + let panOut; + if (ctx.createStereoPanner) { + const panner = ctx.createStereoPanner(); + panner.pan.setValueAtTime(-0.45, now); + panner.pan.linearRampToValueAtTime(0.45, now + duration); + panOut = panner; + } + if (panOut) { + env.connect(panOut); + panOut.connect(this.gainNode); + } else { + env.connect(this.gainNode); + } + + noise.connect(bp); + bp.connect(env); + noise.start(now); + noise.stop(now + duration + 0.02); + } + + /** + * Replicator Materialization Shimmer (Star Trek TNG/VOY, audio_gaps.md #23) + * High-frequency white-noise shimmer (2.5-8 kHz) amplitude-modulated by a + * fast 30 Hz sine LFO with a soft ramp decay. + */ + synthesizeReplicatorShimmer() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 2.0; + + const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const masterEnv = ctx.createGain(); + masterEnv.gain.setValueAtTime(0.001, now); + masterEnv.gain.linearRampToValueAtTime(0.3, now + 0.5); + masterEnv.gain.exponentialRampToValueAtTime(0.001, now + duration); + masterEnv.connect(this.gainNode); + + // Two parallel shimmer bands (2.5-4 kHz and 5-8 kHz) + const bands = [ + { center: 3200, q: 1.3, depth: 0.22 }, + { center: 6400, q: 1.2, depth: 0.16 } + ]; + bands.forEach((band) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(band.center, now); + bp.Q.setValueAtTime(band.q, now); + + const amEnv = ctx.createGain(); + amEnv.gain.setValueAtTime(band.depth, now); + + // 30 Hz shimmer AM + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(30, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(band.depth * 0.9, now); + lfo.connect(lfoDepth); + lfoDepth.connect(amEnv.gain); + + noise.connect(bp); + bp.connect(amEnv); + amEnv.connect(masterEnv); + + lfo.start(now); + lfo.stop(now + duration + 0.05); + }); + + noise.start(now); + noise.stop(now + duration + 0.02); + } + + /** + * Vorlon Crystal "Singing" Resonance (Bioships, audio_gaps.md #26) + * Ethereal telepathic crystal harmonics: dual detuned sines (528/532 Hz) + * through a narrow bandpass with a slow 0.2 Hz undulating tremolo. + * One 8.5 s swell phrasing. + */ + synthesizeVorlonSingingSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 8.5; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(530, now); + bp.Q.setValueAtTime(22, now); + + const tremolo = ctx.createGain(); + tremolo.gain.setValueAtTime(0.5, now); + + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(0.2, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(0.24, now); + lfo.connect(lfoDepth); + lfoDepth.connect(tremolo.gain); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.16, now + 3.0); + env.gain.linearRampToValueAtTime(0.13, now + 5.5); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + [528, 532].forEach((freq, idx) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + if (idx === 1) osc.detune.setValueAtTime(3, now); + osc.connect(bp); + osc.start(now); + osc.stop(now + duration + 0.05); + }); + + bp.connect(tremolo); + tremolo.connect(env); + + lfo.start(now); + lfo.stop(now + duration + 0.05); + } + + /** + * Ambient Loop: Vorlon singing resonance while the Vorlon cruiser is engaged + */ + startVorlonSong(minIntervalMs = 11000, maxIntervalMs = 18000) { + this._scheduleLoop('vorlonSong', minIntervalMs, maxIntervalMs, () => { + this.synthesizeVorlonSingingSwell(); + }); + } + + /** + * Neural-Bond Swell (Bioships, audio_gaps.md #27) + * Symbiotic neural link between pilot and bioship swelling with emotion: + * deep 55 Hz triangle sweeping into a resonant vowel-formant filter + * (350-850 Hz) over 2.5 seconds. + */ + synthesizeNeuralBondSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 3.4; + + // Sub-bass carrier + const osc = ctx.createOscillator(); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(55, now); + osc.frequency.linearRampToValueAtTime(52, now + duration); + + // Formant sweep adding vowel-like harmonics + const formant = ctx.createBiquadFilter(); + formant.type = 'bandpass'; + formant.frequency.setValueAtTime(350, now); + formant.frequency.linearRampToValueAtTime(850, now + 2.5); + formant.Q.setValueAtTime(4.5, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.34, now + 0.9); + env.gain.linearRampToValueAtTime(0.42, now + 1.9); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + osc.connect(formant); + formant.connect(env); + env.connect(this.gainNode); + osc.start(now); + osc.stop(now + duration + 0.05); + } + + /** + * Hull Self-Repair / Regeneration Texture (Bioships, audio_gaps.md #28) + * Microscopic organic tissue knit and chitin regrowth: granular + * amplitude-modulated pink noise (8-24 Hz modulation) with wet low + * regenerative pops. One ~1.8 s regeneration burst. + */ + synthesizeHullRegenBurst() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.8; + + const noiseBuf = this.am.createNoiseBuffer('pink', duration + 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(650, now); + bp.frequency.linearRampToValueAtTime(1600, now + duration); + bp.Q.setValueAtTime(2.0, now); + + const modEnv = ctx.createGain(); + modEnv.gain.setValueAtTime(0.16, now); + + // Rapid scabbing modulation (~13 Hz) over the wet tissue bed + const lfo = ctx.createOscillator(); + lfo.type = 'square'; + lfo.frequency.setValueAtTime(9 + Math.random() * 8, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(0.12, now); + lfo.connect(lfoDepth); + lfoDepth.connect(modEnv.gain); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.2, now + 0.25); + env.gain.exponentialRampToValueAtTime(0.001, now + duration); + + noise.connect(bp); + bp.connect(modEnv); + modEnv.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + duration + 0.05); + lfo.start(now); + lfo.stop(now + duration + 0.05); + + // Wet bioplasmic pops (chitin knit) + const popCount = 2 + Math.floor(Math.random() * 3); + for (let i = 0; i < popCount; i++) { + const popTime = now + 0.25 + Math.random() * (duration - 0.6); + const pop = ctx.createOscillator(); + pop.type = 'sine'; + pop.frequency.setValueAtTime(110 + Math.random() * 60, popTime); + pop.frequency.exponentialRampToValueAtTime(60, popTime + 0.06); + const popEnv = ctx.createGain(); + popEnv.gain.setValueAtTime(0.001, popTime); + popEnv.gain.linearRampToValueAtTime(0.14, popTime + 0.008); + popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.07); + pop.connect(popEnv); + popEnv.connect(this.gainNode); + pop.start(popTime); + pop.stop(popTime + 0.08); + } + } + + /** + * Ambient Loop: Periodic hull regeneration texture on Wraith hives + */ + startHullRegen(minIntervalMs = 6000, maxIntervalMs = 13000) { + this._scheduleLoop('hullRegen', minIntervalMs, maxIntervalMs, () => { + this.synthesizeHullRegenBurst(); + }); + } + + /** + * Belter Jury-Rigged Telemetry Jitter (Space Stations, audio_gaps.md #29) + * Rattle of loose relays, worn copper contactors and erratic voltage drops: + * noisy square relay pulses with micro-dropouts, contact-bounce double + * clicks and harsh metallic ticks on a jittered clock. One ~1.1 s cluster. + */ + synthesizeBelterTelemetryJitter() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const eventCount = 3 + Math.floor(Math.random() * 5); // 3-7 events + let cursor = now + 0.05 + Math.random() * 0.2; + + const relayPulse = (t) => { + const osc = ctx.createOscillator(); + osc.type = 'square'; + const baseFreq = 60 + Math.random() * 60; + osc.frequency.setValueAtTime(baseFreq, t); + osc.frequency.linearRampToValueAtTime(baseFreq * 0.8, t + 0.06); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(420, t); + const env = ctx.createGain(); + // Erratic voltage: randomized micro-dropouts in the pulse body + env.gain.setValueAtTime(0.17, t); + env.gain.setValueAtTime(0.001, t + 0.012 + Math.random() * 0.015); + env.gain.setValueAtTime(0.14, t + 0.026 + Math.random() * 0.02); + env.gain.setValueAtTime(0.001, t + 0.05 + Math.random() * 0.025); + env.gain.setValueAtTime(0.1, t + 0.07); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.1); + osc.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.11); + }; + + const contactBounce = (t) => { + // Worn contactor: double/triple click 1-3 ms apart + const clicks = 2 + (Math.random() < 0.35 ? 1 : 0); + for (let i = 0; i < clicks; i++) { + const ct = t + i * (0.001 + Math.random() * 0.002); + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(500 + Math.random() * 500, ct); + osc.frequency.exponentialRampToValueAtTime(120, ct + 0.008); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.14, ct); + env.gain.exponentialRampToValueAtTime(0.001, ct + 0.012); + osc.connect(env); + env.connect(this.gainNode); + osc.start(ct); + osc.stop(ct + 0.015); + } + }; + + const metallicTick = (t) => { + const buf = this.am.createNoiseBuffer('white', 0.05); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(400 + Math.random() * 1400, t); + bp.Q.setValueAtTime(6, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.09, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.02); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.025); + }; + + for (let i = 0; i < eventCount; i++) { + const kind = Math.random(); + if (kind < 0.4) relayPulse(cursor); + else if (kind < 0.75) contactBounce(cursor); + else metallicTick(cursor); + cursor += 0.08 + Math.random() * 0.22; // jittered clock drift + } + } + + /** + * Ambient Loop: Intermittent jury-rigged telemetry on Belter stations + */ + startBelterTelemetry(minIntervalMs = 1800, maxIntervalMs = 4500) { + this._scheduleLoop('belterTelemetry', minIntervalMs, maxIntervalMs, () => { + this.synthesizeBelterTelemetryJitter(); + }); + } + + /** + * True Hull Strain Moan (Deep Space, audio_gaps.md #30) + * Massive structural groan of stressed bulkheads flexing under gravitational + * shear: resonant bandpass slowly sweeping 40-220 Hz (high Q) over shaped + * brown noise with an asymmetric attack/decay. ~5.5 s. + */ + synthesizeHullStrainMoan() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 5.5; + + const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.3); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(48, now); + bp.frequency.exponentialRampToValueAtTime(210, now + 3.4); + bp.Q.setValueAtTime(10, now); + + // Asymmetric envelope: slow menacing rise, long creaking decay + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.3, now + 0.9); + env.gain.linearRampToValueAtTime(0.22, now + 1.7); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + duration + 0.05); + } + + /** + * Ambient Loop: Sporadic structural strain moans on haunted deep-space hulls + */ + startHullStrainMoans(minIntervalMs = 16000, maxIntervalMs = 40000) { + this._scheduleLoop('hullStrainMoans', minIntervalMs, maxIntervalMs, () => { + this.synthesizeHullStrainMoan(); + }); + } + + /** + * Icarus I Distress Beacon (Deep Space, audio_gaps.md #31) + * Eerie hypnotic modal arpeggio echoing from the ghost ship: a 4-note + * modal sine sequence through a long feedback delay with soft lowpass + * dampening. ~7 s including delay tail. + */ + synthesizeIcarusBeacon() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const notes = [220.0, 261.63, 329.63, 440.0]; // A minor modal beacon + + const delay = ctx.createDelay(1.2); + delay.delayTime.setValueAtTime(0.45, now); + const feedback = ctx.createGain(); + feedback.gain.setValueAtTime(0.35, now); + const dampen = ctx.createBiquadFilter(); + dampen.type = 'lowpass'; + dampen.frequency.setValueAtTime(2200, now); + const wet = ctx.createGain(); + wet.gain.setValueAtTime(0.5, now); + + delay.connect(feedback); + feedback.connect(dampen); + dampen.connect(delay); + delay.connect(wet); + wet.connect(this.gainNode); + + notes.forEach((freq, idx) => { + const t = now + idx * 0.95; + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(0.17, t + 0.05); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.85); + osc.connect(env); + env.connect(this.gainNode); + env.connect(delay); + osc.start(t); + osc.stop(t + 0.9); + }); + } + + /** + * Ambient Loop: Periodic distress beacon arpeggio while Icarus II is engaged + */ + startIcarusBeacon(minIntervalMs = 22000, maxIntervalMs = 40000) { + this._scheduleLoop('icarusBeacon', minIntervalMs, maxIntervalMs, () => { + this.synthesizeIcarusBeacon(); + }); + } + + /** + * Extradimensional Psychic Static Whisper (Deep Space, audio_gaps.md #32) + * Menacing psychoacoustic burst: multi-formant filtered white noise with + * randomized micro-envelopes evoking whispered vowels (800-2400 Hz). + */ + synthesizeVoidWhisper() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 2.2; + + const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.1); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.12, now + 0.4); + env.gain.linearRampToValueAtTime(0.09, now + 1.3); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + // Three parallel whispered-vowel formants with independent wobbles + const formants = [ + { center: 700, q: 7, wobbleHz: 0.8, wobbleAmt: 160 }, + { center: 1250, q: 8, wobbleHz: 1.1, wobbleAmt: 280 }, + { center: 2200, q: 10, wobbleHz: 1.6, wobbleAmt: 420 } + ]; + formants.forEach((formant) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(formant.center, now); + bp.Q.setValueAtTime(formant.q, now); + + const wobble = ctx.createOscillator(); + wobble.type = 'sine'; + wobble.frequency.setValueAtTime(formant.wobbleHz, now); + const wobbleDepth = ctx.createGain(); + wobbleDepth.gain.setValueAtTime(formant.wobbleAmt, now); + wobble.connect(wobbleDepth); + wobbleDepth.connect(bp.frequency); + + const breath = ctx.createGain(); + breath.gain.setValueAtTime(0.3, now); + // Randomized micro-envelopes: syllable-like gulps of the formant + const gulpCount = 3 + Math.floor(Math.random() * 3); + for (let i = 0; i < gulpCount; i++) { + const gt = now + 0.15 + Math.random() * (duration - 0.55); + breath.gain.setValueAtTime(0.12, gt); + breath.gain.linearRampToValueAtTime(0.3 + Math.random() * 0.18, gt + 0.04); + breath.gain.linearRampToValueAtTime(0.05, gt + 0.12 + Math.random() * 0.1); + } + + noise.connect(bp); + bp.connect(breath); + breath.connect(env); + + wobble.start(now); + wobble.stop(now + duration + 0.05); + }); + + noise.start(now); + noise.stop(now + duration + 0.02); + } + + /** + * Ambient Loop: Psychic whisper bursts on haunted deep-space hulls + */ + startVoidWhispers(minIntervalMs = 15000, maxIntervalMs = 32000) { + this._scheduleLoop('voidWhispers', minIntervalMs, maxIntervalMs, () => { + this.synthesizeVoidWhisper(); + }); + } + + /** + * Beryllium Sphere Resonant Thrum (Comedy, audio_gaps.md #33) + * Deep glassy crystalline reactor hum: dual pure sines (65 + 195 Hz third + * harmonic) with slow beating detune and a subtle comb-filter ring. + * One ~10 s swell phrasing. + */ + synthesizeBerylliumThrumSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 10.0; + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.16, now + 4.0); + env.gain.linearRampToValueAtTime(0.14, now + 6.5); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + // Comb ring: short 5.1 ms reflection + const comb = ctx.createDelay(0.05); + comb.delayTime.setValueAtTime(0.0051, now); + const combFb = ctx.createGain(); + combFb.gain.setValueAtTime(0.45, now); + comb.connect(combFb); + combFb.connect(comb); + const combWet = ctx.createGain(); + combWet.gain.setValueAtTime(0.6, now); + comb.connect(combWet); + combWet.connect(env); + + const partials = [ + { freq: 65, amp: 0.55 }, + { freq: 65.3, amp: 0.3 }, // slow 0.3 Hz beating partner + { freq: 195, amp: 0.3 } + ]; + partials.forEach((partial) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(partial.freq, now); + const g = ctx.createGain(); + g.gain.setValueAtTime(partial.amp, now); + osc.connect(g); + g.connect(env); + g.connect(comb); + osc.start(now); + osc.stop(now + duration + 0.05); + }); + } + + /** + * Ambient Loop: Beryllium sphere thrum while the NSEA Protector is engaged + */ + startBerylliumThrum(minIntervalMs = 9000, maxIntervalMs = 16000) { + this._scheduleLoop('berylliumThrum', minIntervalMs, maxIntervalMs, () => { + this.synthesizeBerylliumThrumSwell(); + }); + } + + /** + * Omega-13 Temporal Capacitor Whine (Comedy, audio_gaps.md #34) + * Tremendous temporal capacitor power buildup: deep 40 Hz sub-bass swelling + * exponentially to 3200 Hz over 3 seconds, culminating in a wide + * white-noise discharge pop. + */ + synthesizeOmega13Whine() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 3.1; + + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(40, now); + osc.frequency.exponentialRampToValueAtTime(3200, now + 3.0); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.exponentialRampToValueAtTime(0.5, now + 2.7); + env.gain.setValueAtTime(0.5, now + 2.95); + env.gain.exponentialRampToValueAtTime(0.001, now + 3.2); + + osc.connect(env); + env.connect(this.gainNode); + osc.start(now); + osc.stop(now + duration + 0.1); + + // Dimensional discharge pop (wide white-noise transient) + const popTime = now + 3.0; + const popBuf = this.am.createNoiseBuffer('white', 0.3); + const pop = ctx.createBufferSource(); + pop.buffer = popBuf; + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(2000, popTime); + const popEnv = ctx.createGain(); + popEnv.gain.setValueAtTime(0.001, popTime); + popEnv.gain.linearRampToValueAtTime(0.42, popTime + 0.008); + popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.16); + pop.connect(hp); + hp.connect(popEnv); + popEnv.connect(this.gainNode); + pop.start(popTime); + pop.stop(popTime + 0.18); + } + + /** + * Repulsorlift Engine Drone (Outlaw, audio_gaps.md #35) + * Quintessential anti-gravity vehicle wash: dual detuned sines (68/72 Hz) + * through an asymmetric overdrive waveshaper, lowpass-filtered at 280 Hz. + * One ~10 s wash phrasing. + */ + synthesizeRepulsorliftDrone() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 10.0; + + let shaperOut = null; + if (ctx.createWaveShaper) { + const shaper = ctx.createWaveShaper(); + shaper.oversample = '2x'; + const curve = new Float32Array(1024); + for (let i = 0; i < 1024; i++) { + const x = (i / 512) - 1; // -1..1 + curve[i] = Math.tanh(2.4 * x); // asymmetric-ish saturation + } + shaper.curve = curve; + shaperOut = shaper; + } + + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(280, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.2, now + 2.6); + env.gain.linearRampToValueAtTime(0.17, now + 7.2); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + + if (shaperOut) shaperOut.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + + [68, 72].forEach((freq) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + osc.connect(shaperOut || lp); + osc.start(now); + osc.stop(now + duration + 0.05); + }); + } + + /** + * Ambient Loop: Repulsorlift wash while the Marauder is engaged + */ + startRepulsorliftDrone(intervalMs = 13000) { + this._scheduleLoop('repulsorliftDrone', intervalMs, intervalMs, () => { + this.synthesizeRepulsorliftDrone(); + }); + } + + /** + * Converted Marine Trawler Engine Chug (Outlaw, audio_gaps.md #36) + * Rhythmic heavy diesel-like piston strokes: low-frequency square pulses + * (4-8 Hz cadence) through an 80 Hz resonant lowpass with mechanical + * piston wheeze. One cluster of 2-4 strokes. + */ + synthesizeBebopChugStroke() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const strokes = 2 + Math.floor(Math.random() * 3); // 2-4 strokes + + const stroke = (t, peak) => { + const osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.setValueAtTime(45 + Math.random() * 25, t); + osc.frequency.linearRampToValueAtTime(38, t + 0.3); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(80, t); + lp.Q.setValueAtTime(3, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 0.02); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.42); + osc.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + osc.start(t); + osc.stop(t + 0.45); + + // Mechanical piston wheeze + const wheezeBuf = this.am.createNoiseBuffer('pink', 0.3); + const wheeze = ctx.createBufferSource(); + wheeze.buffer = wheezeBuf; + const wheezeBp = ctx.createBiquadFilter(); + wheezeBp.type = 'bandpass'; + wheezeBp.frequency.setValueAtTime(380, t); + wheezeBp.Q.setValueAtTime(1.6, t); + const wheezeEnv = ctx.createGain(); + wheezeEnv.gain.setValueAtTime(0.001, t + 0.02); + wheezeEnv.gain.linearRampToValueAtTime(0.05, t + 0.08); + wheezeEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.3); + wheeze.connect(wheezeBp); + wheezeBp.connect(wheezeEnv); + wheezeEnv.connect(this.gainNode); + wheeze.start(t); + wheeze.stop(t + 0.32); + }; + + for (let i = 0; i < strokes; i++) { + stroke(now + i * (0.34 + Math.random() * 0.2), 0.3 - i * 0.04); + } + } + + /** + * Ambient Loop: Rhythmic diesel chug while the Bebop is engaged + */ + startBebopChug(minIntervalMs = 2200, maxIntervalMs = 4200) { + this._scheduleLoop('bebopChug', minIntervalMs, maxIntervalMs, () => { + this.synthesizeBebopChugStroke(); + }); + } + + /** + * Centrifugal Habitat Carousel Motor Groan (Military, audio_gaps.md #37) + * Deep rotational strain of a kilometer-long rotating drum: sub-audible + * 16 Hz rotational pair with bearing harmonics, modulated by a single + * ~1 RPM (0.0167 Hz) revolution LFO. One 58 s rotation pass. + */ + synthesizeCarouselGroanCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 58.0; + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.55, now + 8.0); + env.gain.linearRampToValueAtTime(0.5, now + 50.0); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + env.connect(this.gainNode); + + // One rotation per ~60 s: slow strain swell and release + const rotation = ctx.createOscillator(); + rotation.type = 'sine'; + rotation.frequency.setValueAtTime(1 / 60, now); + const rotationDepth = ctx.createGain(); + rotationDepth.gain.setValueAtTime(0.38, now); + rotation.connect(rotationDepth); + rotationDepth.connect(env.gain); + + const partials = [ + { freq: 16, amp: 0.55 }, + { freq: 16.04, amp: 0.32 }, // rotational beating pair + { freq: 48, amp: 0.22 }, // structural harmonic + { freq: 96, amp: 0.07 } // bearing hum harmonic + ]; + partials.forEach((partial) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(partial.freq, now); + const g = ctx.createGain(); + g.gain.setValueAtTime(partial.amp, now); + osc.connect(g); + g.connect(env); + osc.start(now); + osc.stop(now + duration + 0.1); + }); + + rotation.start(now); + rotation.stop(now + duration + 0.1); + } + + /** + * Ambient Loop: Carousel rotation groan while the Agamemnon is engaged + * (one 58 s pass every 60 s; the 2 s gap reads as a rotation splice) + */ + startCarouselGroan(intervalMs = 60000) { + this._scheduleLoop('carouselGroan', intervalMs, intervalMs, () => { + this.synthesizeCarouselGroanCycle(); + }); + } + + /** + * Slipstream Transition Surge (Military, audio_gaps.md #38) + * Ship tearing through an exotic slipstream: dual sweeping bandpass surges + * (200 -> 2400 Hz) with escalating resonance Q and a comb-filter shimmer + * tail. ~4.2 s. + */ + synthesizeSlipstreamSurge() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // Comb shimmer tail bus + const shimmer = ctx.createDelay(0.5); + shimmer.delayTime.setValueAtTime(0.12, now); + const shimmerFb = ctx.createGain(); + shimmerFb.gain.setValueAtTime(0.3, now); + const shimmerLp = ctx.createBiquadFilter(); + shimmerLp.type = 'lowpass'; + shimmerLp.frequency.setValueAtTime(4000, now); + shimmer.connect(shimmerFb); + shimmerFb.connect(shimmerLp); + shimmerLp.connect(shimmer); + const shimmerWet = ctx.createGain(); + shimmerWet.gain.setValueAtTime(0.5, now); + shimmer.connect(shimmerWet); + shimmerWet.connect(this.gainNode); + + const surge = (offset, peak) => { + const t = now + offset; + const osc = ctx.createOscillator(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(200, t); + osc.frequency.exponentialRampToValueAtTime(2400, t + 2.6); + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(200, t); + bp.frequency.exponentialRampToValueAtTime(2400, t + 2.6); + bp.Q.setValueAtTime(2, t); + bp.Q.linearRampToValueAtTime(10, t + 2.6); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(peak, t + 1.4); + env.gain.setValueAtTime(peak, t + 2.7); + env.gain.exponentialRampToValueAtTime(0.001, t + 3.8); + + osc.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + env.connect(shimmer); + osc.start(t); + osc.stop(t + 4.0); + }; + + surge(0, 0.22); + surge(0.3, 0.16); + } + + /** + * Viper Pilot Oxygen Regulator Demand Valve (Military, audio_gaps.md #39) + * Pulsing rebreather of a Viper pilot mid-combat: a sharp 15 ms mechanical + * diaphragm click preceding pulsed highpass-filtered white noise breath + * (1.2-4.5 kHz, ~1.1 s). One breath cycle. + */ + synthesizeOxygenRegulatorCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // Sharp mechanical diaphragm click + const click = ctx.createOscillator(); + click.type = 'triangle'; + click.frequency.setValueAtTime(1800, now); + click.frequency.exponentialRampToValueAtTime(600, now + 0.015); + const clickEnv = ctx.createGain(); + clickEnv.gain.setValueAtTime(0.3, now); + clickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.02); + click.connect(clickEnv); + clickEnv.connect(this.gainNode); + click.start(now); + click.stop(now + 0.025); + + // Regulated airflow breath + const noiseBuf = this.am.createNoiseBuffer('white', 1.3); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(1200, now + 0.02); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(4500, now + 0.02); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now + 0.02); + env.gain.linearRampToValueAtTime(0.13, now + 0.45); + env.gain.linearRampToValueAtTime(0.09, now + 0.62); + env.gain.exponentialRampToValueAtTime(0.001, now + 1.15); + noise.connect(hp); + hp.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + noise.start(now + 0.02); + noise.stop(now + 1.2); + } + + /** + * Ambient Loop: Rhythmic oxygen demand breathing while a Viper cockpit + * is engaged + */ + startOxygenRegulator(minIntervalMs = 3400, maxIntervalMs = 5200) { + this._scheduleLoop('oxygenRegulator', minIntervalMs, maxIntervalMs, () => { + this.synthesizeOxygenRegulatorCycle(); + }); + } + + /** + * Condensation Pipe Drip & Expansion Tick (Industrial, audio_gaps.md #40) + * Lonely water droplet pinging inside a kilometres-long cargo hauler: + * high-Q sine pings (1400-2600 Hz) with fast exponential decay on a + * randomized clock. One drip. + */ + synthesizePipeDrip() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const decay = 0.01 + Math.random() * 0.015; + + const osc = ctx.createOscillator(); + osc.type = 'sine'; + const freq = 1400 + Math.random() * 1200; + osc.frequency.setValueAtTime(freq, now); + osc.frequency.exponentialRampToValueAtTime(freq * 0.92, now + decay); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.1 + Math.random() * 0.2, now + 0.001); + env.gain.exponentialRampToValueAtTime(0.0001, now + decay + 0.01); + + osc.connect(env); + env.connect(this.gainNode); + osc.start(now); + osc.stop(now + decay + 0.03); + } + + /** + * Ambient Loop: Sporadic condensation drips while the Nostromo is engaged + */ + startPipeDrips(minIntervalMs = 500, maxIntervalMs = 3500) { + this._scheduleLoop('pipeDrips', minIntervalMs, maxIntervalMs, () => { + this.synthesizePipeDrip(); + }); + } + + /** + * High-Pressure Steam / Boiler Venting (Industrial, audio_gaps.md #41) + * Continuous hot-vent hiss of a steam-era mining vessel: bandpass-filtered + * white noise (800-2800 Hz) with a slow amplitude swell and random + * micro-flutter. One ~9 s vent pass. + */ + synthesizeSteamVentSwell() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 9.0; + + // Looping noise bed (short buffer, sustained vent) + const noiseBuf = this.am.createNoiseBuffer('white', 4.0); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + noise.loop = true; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(1500, now); + bp.Q.setValueAtTime(0.7, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.17, now + 3.0); + env.gain.linearRampToValueAtTime(0.15, now + 5.5); + env.gain.exponentialRampToValueAtTime(0.0001, now + duration); + + // Boiler pressure micro-flutter (~6-9 Hz jitter) + const flutter = ctx.createOscillator(); + flutter.type = 'sine'; + flutter.frequency.setValueAtTime(6 + Math.random() * 3, now); + const flutterDepth = ctx.createGain(); + flutterDepth.gain.setValueAtTime(0.04, now); + flutter.connect(flutterDepth); + flutterDepth.connect(env.gain); + + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + duration + 0.1); + flutter.start(now); + flutter.stop(now + duration + 0.1); + } + + /** + * Ambient Loop: Boiler vent passes while Serenity-class and Starbug + * presets are engaged + */ + startSteamVent(minIntervalMs = 6000, maxIntervalMs = 10000) { + this._scheduleLoop('steamVent', minIntervalMs, maxIntervalMs, () => { + this.synthesizeSteamVentSwell(); + }); + } + + /** + * Crash-Couch Hydraulic Gimbal Strain (Industrial, audio_gaps.md #42) + * Pilot's couch swinging into launch position: FM triangle carrier + * (140 Hz / 35 Hz modulator) through lowpass damping that tracks the + * acceleration strain. ~1.9 s. + */ + synthesizeCrashCouchGimbal() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 1.9; + + const layer = (offset, carrierFreq, modFreq) => { + const t = now + offset; + const carrier = ctx.createOscillator(); + carrier.type = 'triangle'; + carrier.frequency.setValueAtTime(carrierFreq, t); + + const mod = ctx.createOscillator(); + mod.type = 'sine'; + mod.frequency.setValueAtTime(modFreq, t); + const modDepth = ctx.createGain(); + modDepth.gain.setValueAtTime(carrierFreq * 4, t); + mod.connect(modDepth); + modDepth.connect(carrier.frequency); + + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(300, t); + lp.frequency.linearRampToValueAtTime(900, t + 0.5); + lp.frequency.exponentialRampToValueAtTime(350, t + duration - offset); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime(0.24, t + 0.55); + env.gain.linearRampToValueAtTime(0.2, t + 1.0); + env.gain.exponentialRampToValueAtTime(0.0001, t + duration - offset); + + carrier.connect(lp); + lp.connect(env); + env.connect(this.gainNode); + carrier.start(t); + carrier.stop(t + (duration - offset) + 0.05); + mod.start(t); + mod.stop(t + (duration - offset) + 0.05); + }; + + layer(0, 140, 35); + layer(0.42, 152, 38); + } + + /** + * HAL 9000 Breathing Loop (Retro Future, audio_gaps.md #43) + * Eerie slow respiration of a quiet, observant ship computer: rhythmic + * bandpass-filtered pink noise (450-1100 Hz) with a gentle 3.5 s + * inhalation/exhalation envelope. One breath. + */ + synthesizeHalBreathCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const inhale = 1.3; + const exhale = 2.2; + + const noiseBuf = this.am.createNoiseBuffer('pink', 4.0); + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuf; + noise.loop = true; + + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(700, now); + bp.Q.setValueAtTime(1.2, now); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.085, now + inhale); + env.gain.exponentialRampToValueAtTime(0.001, now + inhale + exhale); + + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(now); + noise.stop(now + inhale + exhale + 0.05); + } + + /** + * Ambient Loop: Continuous respiration while the Discovery One is engaged + */ + startHalBreathing(intervalMs = 3500) { + this._scheduleLoop('halBreathing', intervalMs, intervalMs, () => { + this.synthesizeHalBreathCycle(); + }); + } + + /** + * Death Blossom Energy Surge Ramp (Retro Future, audio_gaps.md #44) + * Fearsome gunstar overdrive: cascaded sawtooth exponential ramp + * (200 Hz -> 8000 Hz over 2.5 s) with rising overdrive, cutting off + * abruptly at full bloom. + */ + synthesizeDeathBlossomSurge() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const rampDur = 2.5; + + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(400, now); + hp.frequency.exponentialRampToValueAtTime(4000, now + rampDur); + + const hp2 = ctx.createBiquadFilter(); + hp2.type = 'highpass'; + hp2.frequency.setValueAtTime(200, now); + hp2.frequency.exponentialRampToValueAtTime(2000, now + rampDur); + + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, now); + env.gain.linearRampToValueAtTime(0.32, now + rampDur); + env.gain.setValueAtTime(0.32, now + rampDur + 0.03); + env.gain.exponentialRampToValueAtTime(0.001, now + rampDur + 0.4); + + hp.connect(hp2); + hp2.connect(env); + env.connect(this.gainNode); + + // Cascaded overdriven saws with staggered ignition + [0, 0.15, 0.3].forEach((offset, idx) => { + const t = now + offset; + const osc = ctx.createOscillator(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(200, t); + osc.frequency.exponentialRampToValueAtTime(8000, t + rampDur - offset); + const g = ctx.createGain(); + g.gain.setValueAtTime(0.33 - idx * 0.08, t); + osc.connect(g); + g.connect(hp); + osc.start(t); + osc.stop(t + (rampDur - offset) + 0.45); + }); + + // Abrupt bloom transient + const burstTime = now + rampDur; + const burstBuf = this.am.createNoiseBuffer('white', 0.25); + const burst = ctx.createBufferSource(); + burst.buffer = burstBuf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(3500, burstTime); + bp.Q.setValueAtTime(1.5, burstTime); + const burstEnv = ctx.createGain(); + burstEnv.gain.setValueAtTime(0.001, burstTime); + burstEnv.gain.linearRampToValueAtTime(0.3, burstTime + 0.01); + burstEnv.gain.exponentialRampToValueAtTime(0.001, burstTime + 0.22); + burst.connect(bp); + bp.connect(burstEnv); + burstEnv.connect(this.gainNode); + burst.start(burstTime); + burst.stop(burstTime + 0.25); + } + + /** + * Victorian Steam Piston Chug (Retro Future, audio_gaps.md #45) + * The steam-age skeleton of the Cygnus: lowpass-filtered noise bursts + * synchronized with heavy 40 Hz triangle thumps and a metallic slapback. + * One cluster of 2-3 stroke cycles. + */ + synthesizeCygnusPistonChug() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const strokes = 2 + Math.floor(Math.random() * 2); // 2-3 strokes + + const slap = ctx.createDelay(0.6); + slap.delayTime.setValueAtTime(0.35, now); + const slapFb = ctx.createGain(); + slapFb.gain.setValueAtTime(0.3, now); + const slapWet = ctx.createGain(); + slapWet.gain.setValueAtTime(0.4, now); + slap.connect(slapFb); + slapFb.connect(slap); + slap.connect(slapWet); + slapWet.connect(this.gainNode); + + const stroke = (t, peak) => { + // Heavy piston body thump + const thump = ctx.createOscillator(); + thump.type = 'triangle'; + thump.frequency.setValueAtTime(40, t); + thump.frequency.linearRampToValueAtTime(32, t + 0.4); + const lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(120, t); + const thumpEnv = ctx.createGain(); + thumpEnv.gain.setValueAtTime(0.001, t); + thumpEnv.gain.linearRampToValueAtTime(peak, t + 0.02); + thumpEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.45); + thump.connect(lp); + lp.connect(thumpEnv); + thumpEnv.connect(this.gainNode); + thumpEnv.connect(slap); + thump.start(t); + thump.stop(t + 0.5); + + // Synchronized steam exhaust puff + const puffBuf = this.am.createNoiseBuffer('white', 0.16); + const puff = ctx.createBufferSource(); + puff.buffer = puffBuf; + const puffLp = ctx.createBiquadFilter(); + puffLp.type = 'lowpass'; + puffLp.frequency.setValueAtTime(500, t); + const puffEnv = ctx.createGain(); + puffEnv.gain.setValueAtTime(0.001, t + 0.01); + puffEnv.gain.linearRampToValueAtTime(0.13, t + 0.03); + puffEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.15); + puff.connect(puffLp); + puffLp.connect(puffEnv); + puffEnv.connect(this.gainNode); + puff.start(t); + puff.stop(t + 0.17); + }; + + for (let i = 0; i < strokes; i++) { + stroke(now + i * (0.5 + Math.random() * 0.25), 0.26 - i * 0.03); + } + } + + /** + * Ambient Loop: Steam piston chug while the Cygnus is engaged + */ + startCygnusChug(minIntervalMs = 1800, maxIntervalMs = 3200) { + this._scheduleLoop('cygnusChug', minIntervalMs, maxIntervalMs, () => { + this.synthesizeCygnusPistonChug(); + }); + } + stopAllLoops() { this.stopMedicalMonitor(); this.stopStationSparks(); + Object.keys(this.loopTimers).forEach((key) => { + this._stopLoop(key); + }); } } diff --git a/js/config.js b/js/config.js index a467daa..78cfbe7 100644 --- a/js/config.js +++ b/js/config.js @@ -166,8 +166,10 @@ const UniverseRegistry = { { id: 'btn-door-swish', label: 'DOOR PNEUMATIC SWISH' }, { id: 'btn-bosun-whistle', label: "BOSUN'S PIPE WHISTLE" }, { id: 'btn-medical-monitor', label: 'SICKBAY ECG MONITOR' }, - { id: 'btn-chirp-single', label: 'LCARS SINGLE CHIRP' }, - { id: 'btn-chirp-ack', label: 'LCARS DATA ACK' } + { id: 'btn-cardassian-door', label: 'CARDASSIAN BULKHEAD DOOR' }, + { id: 'btn-turbolift', label: 'TURBOLIFT WHOOSH' }, + { id: 'btn-replicator', label: 'REPLICATOR SHIMMER' }, + { id: 'btn-chirp-single', label: 'LCARS SINGLE CHIRP' } ], presets: StarshipPresets }, @@ -194,7 +196,9 @@ const UniverseRegistry = { { id: 'btn-tardis-door', label: 'POLICE BOX WOODEN DOOR' }, { id: 'btn-fast-return', label: 'FAST-RETURN LEVER' }, { id: 'btn-demat-switch', label: 'DEMAT SWITCH' }, - { id: 'btn-telepathic', label: 'TELEPATHIC CHIME' } + { id: 'btn-telepathic', label: 'TELEPATHIC CHIME' }, + { id: 'btn-scanner-activate', label: 'SCANNER ACTIVATION' }, + { id: 'btn-type-clatter', label: 'CONSOLE TYPE CLATTER' } ], presets: { 'tardis-1963': { @@ -303,9 +307,9 @@ const UniverseRegistry = { soundboard: [ { id: 'btn-geiger', label: 'GEIGER COUNTER' }, { id: 'btn-dock-clamp', label: 'HYDRAULIC CLAMPS' }, - { id: 'btn-relay-click', label: 'M/TH/UR TELETYPE' }, - { id: 'btn-switch-pop', label: 'HEAVY SOLENOID' }, - { id: 'btn-pneumatic-door', label: 'PNEUMATIC SEAL PURGE' }, + { 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' } ], presets: { @@ -409,12 +413,12 @@ const UniverseRegistry = { { id: 'btn-bio-defense', label: 'BIO-DEFENSE BURST', type: 'biodefense' } ], soundboard: [ - { id: 'btn-telepathic-chime', label: 'NEURAL TWITCH' }, - { id: 'btn-fast-return', label: 'VASCULAR PUMP' }, - { id: 'btn-demat-switch', label: 'CHITIN CREAK' }, - { id: 'btn-chirp-sweep', label: 'BIOPLASMIC HISS' }, - { id: 'btn-sonic', label: 'SYMBIOTE VOCALIZATION' }, - { id: 'btn-relay-click', label: 'MEMBRANE FLUTTER' } + { id: 'btn-neural-bond', label: 'NEURAL-BOND SWELL' }, + { id: 'btn-fast-return', label: 'VASCULAR PUMP' }, + { id: 'btn-demat-switch', label: 'CHITIN CREAK' }, + { id: 'btn-chirp-sweep', label: 'BIOPLASMIC HISS' }, + { id: 'btn-vorlon-song', label: 'VORLON SINGING RESONANCE' }, + { id: 'btn-hull-regen', label: 'HULL REGEN TEXTURE' } ], presets: { 'bio-moya': { @@ -497,12 +501,12 @@ const UniverseRegistry = { { id: 'btn-hal-override', label: 'HAL 9000 OVERRIDE', type: 'hal' } ], soundboard: [ - { id: 'btn-hal-chime', label: 'HAL LOGIC CONFIRM' }, - { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' }, + { id: 'btn-hal-breath', label: 'HAL RESPIRATION' }, + { id: 'btn-death-blossom',label: 'DEATH BLOSSOM SURGE' }, + { id: 'btn-cygnus-chug', label: 'STEAM PISTON CHUG' }, { id: 'btn-chirp-single', label: 'ASTROGATOR BEAT' }, { id: 'btn-chirp-sweep', label: 'ANALOG GLISSANDO' }, - { id: 'btn-fast-return', label: 'MAGNETIC TAPE LATCH' }, - { id: 'btn-dock-clamp', label: 'PNEUMATIC SEAL PURGE' } + { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' } ], presets: { 'ret-jupiter-2': { @@ -605,12 +609,12 @@ const UniverseRegistry = { { id: 'btn-flak', label: 'FLAK BARRAGE', type: 'flak' } ], soundboard: [ - { id: 'btn-dradis-ping', label: 'DRADIS SCAN PING' }, - { id: 'btn-switch-pop', label: 'WEAPONS ARMED' }, - { id: 'btn-dock-clamp', label: 'BULKHEAD SEAL' }, - { id: 'btn-chirp-sweep', label: 'RADAR TARGET LOCK' }, - { id: 'btn-fast-return', label: 'TURBO-THRUSTER SWELL' }, - { id: 'btn-air-handler', label: 'RCS THRUSTER BURST' } + { id: 'btn-dradis-ping', label: 'DRADIS SCAN PING' }, + { id: 'btn-switch-pop', label: 'WEAPONS ARMED' }, + { 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' } ], presets: { 'mil-sulaco': { @@ -703,12 +707,12 @@ const UniverseRegistry = { { id: 'btn-solar-swell', label: 'SOLAR SHIELD SWELL', type: 'solar' } ], soundboard: [ - { id: 'btn-solar-roar', label: 'HEAT SHIELD ROAR' }, - { id: 'btn-cryo-sigh', label: 'CRYO POD SIGH' }, - { id: 'btn-orion-thump', label: 'ORION PULSE THUMP' }, - { id: 'btn-dock-clamp', label: 'HULL STRAIN MOAN' }, - { id: 'btn-telepathic-chime', label: 'VOID RESONANCE' }, - { id: 'btn-chirp-sweep', label: 'TACHYON SENSOR PING' } + { id: 'btn-solar-roar', label: 'HEAT SHIELD ROAR' }, + { id: 'btn-cryo-sigh', label: 'CRYO POD SIGH' }, + { id: 'btn-orion-thump', label: 'ORION PULSE THUMP' }, + { id: 'btn-hull-strain', label: 'TRUE HULL STRAIN MOAN' }, + { id: 'btn-void-whisper',label: 'PSYCHIC VOID WHISPER' }, + { id: 'btn-icarus-beacon', label: 'ICARUS I DISTRESS BEACON' } ], presets: { 'deep-avalon': { @@ -804,9 +808,9 @@ const UniverseRegistry = { { id: 'btn-tape-clunk', label: 'CASSETTE CLUNK & WHIRR' }, { id: 'btn-grappler-servo', label: 'GRAPPLER SERVO MOTOR' }, { id: 'btn-radio-static', label: 'RF SQUELCH STATIC BURST' }, - { id: 'btn-dock-clamp', label: 'FUEL LINE VALVE' }, - { id: 'btn-afterburner-snd',label: 'AFTERBURNER BOOST' }, - { id: 'btn-fast-return', label: 'TURBO JET BURN' } + { id: 'btn-repulsor-drone', label: 'REPULSORLIFT DRONE' }, + { id: 'btn-bebop-chug', label: 'MARINE DIESEL CHUG' }, + { id: 'btn-afterburner-snd',label: 'AFTERBURNER BOOST' } ], presets: { 'out-betty': { @@ -894,7 +898,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-dock-clamp', label: 'DOCKING CLAMP LATCH' } + { id: 'btn-belter-telemetry', label: 'BELTER JURY-RIG TELEMETRY', span: 2 } ], presets: { 'sta-babylon-5': { @@ -981,8 +985,8 @@ const UniverseRegistry = { { id: 'btn-tea-machine', label: 'TEA DISPENSER GURGLE' }, { id: 'btn-kettle-whistle', label: 'KETTLE STEAM WHISTLE' }, { id: 'btn-plaid-alarm', label: 'LUDICROUS PLAID ALARM' }, - { id: 'btn-beryllium-pulse',label: 'BERYLLIUM CORE PULSE' }, - { id: 'btn-chirp-ack', label: 'ORVILLE ACK CHIRP' } + { id: 'btn-beryllium-thrum',label: 'BERYLLIUM SPHERE THRUM' }, + { id: 'btn-omega-13', label: 'OMEGA-13 CAPACITOR WHINE' } ], presets: { 'com-orville': { diff --git a/sound_reference.md b/sound_reference.md index c8eb392..9c18d7a 100644 --- a/sound_reference.md +++ b/sound_reference.md @@ -294,6 +294,18 @@ Every Starfleet preset in `StarshipPresets` is audited against the audio engine: --- +## Star Trek Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #21–#23). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Cardassian bulkhead door | `synthesizeCardassianDoor` | `btn-cardassian-door` | — | +| Turbolift whoosh | `synthesizeTurboliftWhoosh` | `btn-turbolift` | — | +| Replicator shimmer | `synthesizeReplicatorShimmer` | `btn-replicator` | — | + +--- + ## Star Trek open questions Deliberately unresolved rather than guessed: @@ -486,6 +498,18 @@ Every TARDIS console room preset is audited against the audio engine: --- +## Whoniverse Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #24–#25). Both remain standalone +routines; `TelemetrySynth` `era: 'who'` routing is Phase 3 scope. + +| Element | Method (WhoniverseAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Scanner / monitor activation | `synthesizeScannerActivate` | `btn-scanner-activate` | — (flyback partial at 10.6 kHz) | +| Console "type" input telemetry | `synthesizeTypeClatter` + `startConsoleTypeTelemetry` | `btn-type-clatter` | All `tardis-*` rooms, 2.8–6.5 s clatter clusters | + +--- + ## Whoniverse open questions - **[GAP]** The cloister bell's real strike interval and who created the sound. @@ -721,6 +745,18 @@ Item 1 dwarfs the rest in the same way the Whoniverse telemetry gap did: a purpose-built organic discrete vocabulary — creaks, pulses, membrane flutters — would do more for this universe's authenticity than any single new one-shot. +## Bioships Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #26–#28). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Vorlon "singing" resonance | `synthesizeVorlonSingingSwell` + `startVorlonSong` | `btn-vorlon-song` | `bio-vorlon`, 11–18 s swell cadence | +| Neural-bond swell | `synthesizeNeuralBondSwell` | `btn-neural-bond` | Also fires from the NEURAL PULSE universe event | +| Hull regeneration texture | `synthesizeHullRegenBurst` + `startHullRegen` | `btn-hull-regen` | `bio-wraith`, 6–13 s cadence | + +--- + ## Bioships open questions - **[GAP]** No sound design production information was found for The Lexx, @@ -1194,6 +1230,17 @@ Ranked by impact and feasibility across the six station profiles: --- +## Space Stations Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (row #29). Delivered as a standalone +routine; `TelemetrySynth` `era: 'belter'` routing is Phase 3 scope. + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Belter jury-rigged telemetry | `synthesizeBelterTelemetryJitter` + `startBelterTelemetry` | `btn-belter-telemetry` (full-width) | `sta-ceres`, `sta-tycho`, 1.8–4.5 s jitter clusters | + +--- + ## Space Stations open questions - **[GAP]** Whether *Space: 1999*’s transition from Main Mission (Season 1) to @@ -1710,6 +1757,18 @@ Ranked by impact and feasibility across the seven vessel profiles: --- +## Deep Space Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #30–#32). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| True hull strain moan | `synthesizeHullStrainMoan` + `startHullStrainMoans` | `btn-hull-strain` | `deep-event-horizon`, `deep-icarus-ii`, 16–40 s cadence | +| Icarus I distress beacon | `synthesizeIcarusBeacon` + `startIcarusBeacon` | `btn-icarus-beacon` | `deep-icarus-ii`, 22–40 s cadence | +| Psychic void whisper | `synthesizeVoidWhisper` + `startVoidWhispers` | `btn-void-whisper` | `deep-event-horizon`, `deep-nightflyer`, 15–32 s cadence | + +--- + ## Deep Space open questions - **[GAP]** The specific synthesizer patches and filtering used by Glenn @@ -2126,6 +2185,17 @@ Ranked by impact and feasibility across the five starship profiles: --- +## Comedy & Adventure Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #33–#34). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Beryllium sphere resonant thrum | `synthesizeBerylliumThrumSwell` + `startBerylliumThrum` | `btn-beryllium-thrum` | `com-protector`, 9–16 s swell cadence | +| Omega-13 temporal capacitor whine | `synthesizeOmega13Whine` | `btn-omega-13` | — | + +--- + ## Comedy & Adventure open questions - **[GAP]** The specific synthesizer models and patch parameters used by Gary @@ -2582,6 +2652,17 @@ Ranked by impact and feasibility across the six starship profiles: --- +## Outlaw & Frontier Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #35–#36). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Repulsorlift engine drone | `synthesizeRepulsorliftDrone` + `startRepulsorliftDrone` | `btn-repulsor-drone` | `out-marauder`, 13 s wash cadence | +| Marine trawler engine chug | `synthesizeBebopChugStroke` + `startBebopChug` | `btn-bebop-chug` | `out-bebop`, 2.2–4.2 s stroke clusters | + +--- + ## Outlaw & Frontier open questions - **[GAP]** The specific mechanical source recorded by Christopher Boyes for @@ -3175,6 +3256,18 @@ that are currently simulated by existing approximations: --- +## Military Space Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #37–#39). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Carousel motor groan | `synthesizeCarouselGroanCycle` + `startCarouselGroan` | `btn-carousel-groan` | `mil-agamemnon`, 58 s rotation pass / 60 s | +| Slipstream transition surge | `synthesizeSlipstreamSurge` | `btn-slipstream` | — | +| O₂ demand valve | `synthesizeOxygenRegulatorCycle` + `startOxygenRegulator` | `btn-oxygen-reg` | `mil-viper`, 3.4–5.2 s breath cycles | + +--- + ## Military Space open questions - **[GAP]** The exact acoustic recording sources used by Don Sharpe for the @@ -3810,6 +3903,18 @@ production that are currently simulated by approximations: --- +## Industrial Space Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #40–#42). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| Condensation pipe drips | `synthesizePipeDrip` + `startPipeDrips` | `btn-pipe-drips` | `ind-nostromo`, 0.5–3.5 s drip clock | +| Steam / boiler venting | `synthesizeSteamVentSwell` + `startSteamVent` | `btn-steam-vent` | `ind-serenity`, `ind-starbug`, 6–10 s vent passes | +| Crash-couch hydraulic gimbal | `synthesizeCrashCouchGimbal` | `btn-crash-couch` | — | + +--- + ## Industrial Space open questions - **[GAP]** The exact mechanical typewriter and electromechanical relay sources @@ -4434,6 +4539,18 @@ that are currently simulated by existing approximations: --- +## Retro Future Phase 2 additions + +Implemented under audio_gaps.md Phase 2 (rows #43–#45). + +| Element | Method (ExpandedSciFiAudioSynth) | Soundboard | Ambient hook | +| --- | --- | --- | --- | +| HAL 9000 breathing loop | `synthesizeHalBreathCycle` + `startHalBreathing` | `btn-hal-breath` | `ret-discovery-one`, continuous 3.5 s breath cycle | +| Death Blossom energy surge | `synthesizeDeathBlossomSurge` | `btn-death-blossom` | — | +| Victorian steam piston chug | `synthesizeCygnusPistonChug` + `startCygnusChug` | `btn-cygnus-chug` | `ret-cygnus`, 1.8–3.2 s stroke clusters | + +--- + ## Retro Future open questions - **[GAP]** The exact electronic audio generators and tape echo units used by