diff --git a/js/app.js b/js/app.js index 84d4751..9215868 100644 --- a/js/app.js +++ b/js/app.js @@ -669,10 +669,12 @@ document.addEventListener('DOMContentLoaded', () => { expandedAudio.synthesizeHalChime(); break; case 'ftl-jump': - case 'flak': expandedAudio.synthesizeFtlJump(); visualizer.spawnWarpPulses(); break; + case 'flak': + expandedAudio.synthesizeFlakBarrageBurst(); + break; case 'singularity': case 'gravity': case 'rotation': @@ -917,6 +919,23 @@ document.addEventListener('DOMContentLoaded', () => { expandedAudio.synthesizeCygnusPistonChug(); break; + // Phase 3 (audio_gaps.md #48-#53) + case 'btn-transporter': + expandedAudio.synthesizeTransporterCycle(); + break; + case 'btn-breech-clank': + expandedAudio.synthesizeBreechClank(); + break; + case 'btn-dock-groan': + expandedAudio.synthesizeDockingGroan(); + break; + case 'btn-tape-spooler': + expandedAudio.synthesizeTapeSpooler(); + break; + case 'btn-teletype': + expandedAudio.synthesizeTeletypeChatterPhrase(); + break; + default: telemetry.synthesizeLCARSSingleChirp(); break; diff --git a/js/audio.js b/js/audio.js index 7800281..df09d51 100644 --- a/js/audio.js +++ b/js/audio.js @@ -5228,6 +5228,497 @@ class ExpandedSciFiAudioSynth { }); } + /** + * Transporter Dematerialization / Rematerialization Cycle (audio_gaps.md #48) + * The classic shimmering beam-up wash: an additive array of 5 detuned sines + * (330-990 Hz over a 660 Hz base) each interlocked with its own 5-11 Hz AM + * LFO, plus a high bandpass noise shimmer gated by a 30 Hz LFO, all under a + * slow rise / mid dip / release master envelope. ~3.0 s. + */ + synthesizeTransporterCycle() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 3.0; + + // Tonal shimmer master envelope (dip mid-cycle reads as the demat/remat split) + const masterEnv = ctx.createGain(); + masterEnv.gain.setValueAtTime(0.001, now); + masterEnv.gain.linearRampToValueAtTime(0.15, now + 0.9); + masterEnv.gain.setValueAtTime(0.1, now + 1.35); + masterEnv.gain.linearRampToValueAtTime(0.17, now + 1.95); + masterEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + masterEnv.connect(this.gainNode); + + const ratios = [0.5, 0.75, 1.0, 1.25, 1.5]; + ratios.forEach((ratio, idx) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(660 * ratio, now); + osc.detune.setValueAtTime((idx % 2 ? 1 : -1) * (1.5 + Math.random() * 4.5), now); + + // Per-voice interlocking amplitude modulation (staggered 5-11 Hz LFOs) + const amBase = 0.5 + Math.random() * 0.2; + const voiceEnv = ctx.createGain(); + voiceEnv.gain.setValueAtTime(amBase, now); + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(5 + idx * 1.5 + Math.random(), now); + const amDepth = ctx.createGain(); + amDepth.gain.setValueAtTime(amBase * 0.7, now); + lfo.connect(amDepth); + amDepth.connect(voiceEnv.gain); + + osc.connect(voiceEnv); + voiceEnv.connect(masterEnv); + osc.start(now); + osc.stop(now + duration + 0.1); + lfo.start(now); + lfo.stop(now + duration + 0.1); + }); + + // High-frequency energy shimmer (4-9 kHz) following a parallel envelope + const shimmerEnv = ctx.createGain(); + shimmerEnv.gain.setValueAtTime(0.001, now); + shimmerEnv.gain.linearRampToValueAtTime(0.05, now + 0.9); + shimmerEnv.gain.setValueAtTime(0.035, now + 1.35); + shimmerEnv.gain.linearRampToValueAtTime(0.055, now + 1.95); + shimmerEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + shimmerEnv.connect(this.gainNode); + + const shimmerBuf = this.am.createNoiseBuffer('white', duration + 0.2); + const shimmer = ctx.createBufferSource(); + shimmer.buffer = shimmerBuf; + [4500, 7800].forEach((center) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(center, now); + bp.Q.setValueAtTime(1.2, now); + const amEnv = ctx.createGain(); + amEnv.gain.setValueAtTime(center === 4500 ? 0.6 : 0.4, now); + const lfo = ctx.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.setValueAtTime(30, now); + const lfoDepth = ctx.createGain(); + lfoDepth.gain.setValueAtTime(amEnv.gain.value * 0.9, now); + lfo.connect(lfoDepth); + lfoDepth.connect(amEnv.gain); + shimmer.connect(bp); + bp.connect(amEnv); + amEnv.connect(shimmerEnv); + lfo.start(now); + lfo.stop(now + duration + 0.1); + }); + shimmer.start(now); + shimmer.stop(now + duration + 0.05); + } + + /** + * Point-Defense Flak Barrage Burst (audio_gaps.md #49) + * Staggered concussive clusters of 4-8 noise transients 10-30 ms apart over + * a 40 Hz sub-bass punch, with a couple of high shell-crack transients per + * volley. Each hit self-releases; whole burst ~0.9-1.4 s. + */ + synthesizeFlakBarrageBurst() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 0.9 + Math.random() * 0.5; + + // 40 Hz sub-bass punch at volley start + const sub = ctx.createOscillator(); + sub.type = 'triangle'; + sub.frequency.setValueAtTime(40, now); + sub.frequency.linearRampToValueAtTime(32, now + 0.25); + const subEnv = ctx.createGain(); + subEnv.gain.setValueAtTime(0.001, now); + subEnv.gain.linearRampToValueAtTime(0.3, now + 0.008); + subEnv.gain.exponentialRampToValueAtTime(0.0001, now + 0.3); + sub.connect(subEnv); + subEnv.connect(this.gainNode); + sub.start(now); + sub.stop(now + 0.32); + + const hitCount = 4 + Math.floor(Math.random() * 5); // 4-8 hits + let cursor = 0.05; + for (let i = 0; i < hitCount; i++) { + const t = now + cursor; + const alternate = i % 2 ? 0.75 : 1.0; // mono proxy for spread + const isCrack = i % 3 === 1; + const buf = this.am.createNoiseBuffer('white', 0.12); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + if (isCrack) { + bp.frequency.setValueAtTime(2500 + Math.random() * 1500, t); + bp.Q.setValueAtTime(8 + Math.random() * 4, t); + } else { + bp.frequency.setValueAtTime(300 + Math.random() * 1300, t); + bp.Q.setValueAtTime(2 + Math.random() * 3, t); + } + const env = ctx.createGain(); + env.gain.setValueAtTime(0.001, t); + env.gain.linearRampToValueAtTime((isCrack ? 0.1 : 0.17 + Math.random() * 0.12) * alternate, t + 0.004); + env.gain.exponentialRampToValueAtTime(0.001, t + (isCrack ? 0.015 : 0.03 + Math.random() * 0.04)); + noise.connect(bp); + bp.connect(env); + env.connect(this.gainNode); + noise.start(t); + noise.stop(t + 0.13); + cursor += 0.01 + Math.random() * 0.02; // 10-30 ms stagger + } + + // Second soft sub thump mid-volley (distant batteries) + const sub2 = ctx.createOscillator(); + sub2.type = 'triangle'; + sub2.frequency.setValueAtTime(38, now + 0.42); + sub2.frequency.linearRampToValueAtTime(30, now + 0.6); + const sub2Env = ctx.createGain(); + sub2Env.gain.setValueAtTime(0.001, now + 0.42); + sub2Env.gain.linearRampToValueAtTime(0.16, now + 0.43); + sub2Env.gain.exponentialRampToValueAtTime(0.0001, now + 0.72); + sub2.connect(sub2Env); + sub2Env.connect(this.gainNode); + sub2.start(now + 0.42); + sub2.stop(now + 0.74); + } + + /** + * Heavy Weapons Breech Locking Clank (audio_gags.md #50) + * Three-stage loading cycle: 80 ms pre-charge pneumatic hiss, 60 Hz square + * breech slam into a lowpass body, and a high-Q steel ringoff with damped + * inharmonic partials. ~1.6 s. + */ + synthesizeBreechClank() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + + // Stage 1: pneumatic pre-charge hiss (80 ms, highpass 2.5 kHz) + const hissBuf = this.am.createNoiseBuffer('white', 0.14); + const hiss = ctx.createBufferSource(); + hiss.buffer = hissBuf; + const hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.setValueAtTime(2500, now); + hp.Q.setValueAtTime(0.7, now); + const hissEnv = ctx.createGain(); + hissEnv.gain.setValueAtTime(0.001, now); + hissEnv.gain.linearRampToValueAtTime(0.14, now + 0.005); + hissEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.08); + hiss.connect(hp); + hp.connect(hissEnv); + hissEnv.connect(this.gainNode); + hiss.start(now); + hiss.stop(now + 0.1); + + // Stage 2: breech slam - 60 Hz square impact into a lowpass body + const slam = ctx.createOscillator(); + slam.type = 'square'; + slam.frequency.setValueAtTime(60, now + 0.08); + slam.frequency.linearRampToValueAtTime(48, now + 0.2); + const slamLp = ctx.createBiquadFilter(); + slamLp.type = 'lowpass'; + slamLp.frequency.setValueAtTime(300, now + 0.08); + const slamEnv = ctx.createGain(); + slamEnv.gain.setValueAtTime(0.001, now + 0.08); + slamEnv.gain.linearRampToValueAtTime(0.4, now + 0.086); + slamEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.28); + slam.connect(slamLp); + slamLp.connect(slamEnv); + slamEnv.connect(this.gainNode); + slam.start(now + 0.08); + slam.stop(now + 0.3); + + // Stage 3: high-Q steel ringoff with damped inharmonic partials + const ringBuf = this.am.createNoiseBuffer('white', 0.45); + const ring = ctx.createBufferSource(); + ring.buffer = ringBuf; + const ringBp = ctx.createBiquadFilter(); + ringBp.type = 'bandpass'; + ringBp.frequency.setValueAtTime(1800 + Math.random() * 1600, now + 0.09); + ringBp.Q.setValueAtTime(18 + Math.random() * 7, now + 0.09); + const ringEnv = ctx.createGain(); + ringEnv.gain.setValueAtTime(0.001, now + 0.09); + ringEnv.gain.linearRampToValueAtTime(0.16, now + 0.095); + ringEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.5); + ring.connect(ringBp); + ringBp.connect(ringEnv); + ringEnv.connect(this.gainNode); + ring.start(now + 0.09); + ring.stop(now + 0.52); + + [240, 630, 1120].forEach((freq, idx) => { + const partial = ctx.createOscillator(); + partial.type = 'sine'; + partial.frequency.setValueAtTime(freq, now + 0.09); + const pEnv = ctx.createGain(); + pEnv.gain.setValueAtTime(0.001, now + 0.09); + pEnv.gain.linearRampToValueAtTime([0.11, 0.07, 0.045][idx], now + 0.095); + pEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.09 + [0.9, 0.6, 0.35][idx]); + partial.connect(pEnv); + pEnv.connect(this.gainNode); + partial.start(now + 0.09); + partial.stop(now + [1.0, 0.7, 0.45][idx]); + }); + } + + /** + * Heavy Hydraulic Docking Groan & Mechanical Clang (audio_gaps.md #52) + * A slow ~2.4 s structural stress groan: brown noise through two parallel + * resonant bandpasses sweeping 38->62 Hz and 55->85 Hz under an asymmetric + * groaning envelope, resolving into a metallic anvil transient with an + * inharmonic sine stack at the moment the clamp seats. ~4.0 s. + */ + synthesizeDockingGroan() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const groanDur = 3.9; + + // Brown-noise stress bed through two sweeping resonant bandpasses + const bedBuf = this.am.createNoiseBuffer('brown', groanDur + 0.2); + const bed = ctx.createBufferSource(); + bed.buffer = bedBuf; + const groanEnv = ctx.createGain(); + groanEnv.gain.setValueAtTime(0.0001, now); + groanEnv.gain.linearRampToValueAtTime(0.001, now + 0.01); + groanEnv.gain.linearRampToValueAtTime(0.22, now + 1.1); // groan up + groanEnv.gain.setValueAtTime(0.1, now + 1.6); // stress dip + groanEnv.gain.linearRampToValueAtTime(0.26, now + 2.3); // groan peaks + groanEnv.gain.exponentialRampToValueAtTime(0.0001, now + groanDur); + groanEnv.connect(this.gainNode); + + [38, 55].forEach((startFreq, idx) => { + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(startFreq, now); + bp.frequency.linearRampToValueAtTime(idx === 0 ? 62 : 85, now + 2.4); + bp.Q.setValueAtTime(idx === 0 ? 5.5 : 4.5, now); + bed.connect(bp); + bp.connect(groanEnv); + }); + bed.start(now); + bed.stop(now + groanDur + 0.05); + + // Metallic anvil clang as the groan resolves (~2.7 s in) + const clangT = now + 2.7; + const ringBuf = this.am.createNoiseBuffer('white', 0.4); + const ring = ctx.createBufferSource(); + ring.buffer = ringBuf; + const ringBp = ctx.createBiquadFilter(); + ringBp.type = 'bandpass'; + ringBp.frequency.setValueAtTime(2000, clangT); + ringBp.Q.setValueAtTime(20, clangT); + const ringEnv = ctx.createGain(); + ringEnv.gain.setValueAtTime(0.001, clangT); + ringEnv.gain.linearRampToValueAtTime(0.16, clangT + 0.004); + ringEnv.gain.exponentialRampToValueAtTime(0.001, clangT + 0.35); + ring.connect(ringBp); + ringBp.connect(ringEnv); + ringEnv.connect(this.gainNode); + ring.start(clangT); + ring.stop(clangT + 0.4); + + [420, 880, 1600].forEach((freq, idx) => { + const partial = ctx.createOscillator(); + partial.type = 'sine'; + partial.frequency.setValueAtTime(freq, clangT); + const pEnv = ctx.createGain(); + pEnv.gain.setValueAtTime(0.001, clangT); + pEnv.gain.linearRampToValueAtTime([0.08, 0.06, 0.04][idx], clangT + 0.005); + pEnv.gain.exponentialRampToValueAtTime(0.001, clangT + [0.8, 0.55, 0.3][idx]); + partial.connect(pEnv); + pEnv.connect(this.gainNode); + partial.start(clangT); + partial.stop(clangT + [0.85, 0.6, 0.35][idx]); + }); + } + + /** + * Dual-Reel Magnetic Tape Spooler & Pinch Roller Clank (audio_gaps.md #53) + * Solenoid pinch-roller engagement (110 Hz impulse), a fluttering pink tape + * bed (15 Hz AM on a bandpass noise loop) and a faint 3200 Hz capstan hum + * with spool-up glide, ending in an optional fast-forward whine tail. + * ~2.6-3.5 s. + */ + synthesizeTapeSpooler() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const duration = 2.6 + Math.random() * 0.9; + + // Solenoid clamp impulse (110 Hz) with a tiny mechanical tick + const solenoid = ctx.createOscillator(); + solenoid.type = 'square'; + solenoid.frequency.setValueAtTime(110, now); + solenoid.frequency.linearRampToValueAtTime(95, now + 0.05); + const solEnv = ctx.createGain(); + solEnv.gain.setValueAtTime(0.001, now); + solEnv.gain.linearRampToValueAtTime(0.22, now + 0.004); + solEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.07); + solenoid.connect(solEnv); + solEnv.connect(this.gainNode); + solenoid.start(now); + solenoid.stop(now + 0.09); + + const tickBuf = this.am.createNoiseBuffer('white', 0.03); + const tick = ctx.createBufferSource(); + tick.buffer = tickBuf; + const tickBp = ctx.createBiquadFilter(); + tickBp.type = 'bandpass'; + tickBp.frequency.setValueAtTime(1800, now + 0.02); + tickBp.Q.setValueAtTime(10, now + 0.02); + const tickEnv = ctx.createGain(); + tickEnv.gain.setValueAtTime(0.08, now + 0.02); + tickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.045); + tick.connect(tickBp); + tickBp.connect(tickEnv); + tickEnv.connect(this.gainNode); + tick.start(now + 0.02); + tick.stop(now + 0.06); + + // Fluttering pink-noise tape bed (15 Hz flutter AM) + const tapeBuf = this.am.createNoiseBuffer('pink', duration + 0.3); + const tape = ctx.createBufferSource(); + tape.buffer = tapeBuf; + tape.loop = true; + const tapeBp = ctx.createBiquadFilter(); + tapeBp.type = 'bandpass'; + tapeBp.frequency.setValueAtTime(500 + Math.random() * 1000, now); + tapeBp.Q.setValueAtTime(1, now); + const tapeEnv = ctx.createGain(); + tapeEnv.gain.setValueAtTime(0.0001, now); + tapeEnv.gain.linearRampToValueAtTime(0.05, now + 0.15); + tapeEnv.gain.setValueAtTime(0.03, now + duration * 0.45); + tapeEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + const flutter = ctx.createOscillator(); + flutter.type = 'sine'; + flutter.frequency.setValueAtTime(15, now); + const flutterDepth = ctx.createGain(); + flutterDepth.gain.setValueAtTime(0.035, now); + flutter.connect(flutterDepth); + flutterDepth.connect(tapeEnv.gain); + tape.connect(tapeBp); + tapeBp.connect(tapeEnv); + tapeEnv.connect(this.gainNode); + tape.start(now); + tape.stop(now + duration + 0.05); + flutter.start(now); + flutter.stop(now + duration + 0.05); + + // Capstan hum: faint 3200 Hz sine with a slow spool-up glide and wow + const capstan = ctx.createOscillator(); + capstan.type = 'sine'; + capstan.frequency.setValueAtTime(3136, now); + capstan.frequency.exponentialRampToValueAtTime(3264, now + 1.5); + const capstanEnv = ctx.createGain(); + capstanEnv.gain.setValueAtTime(0.0001, now); + capstanEnv.gain.linearRampToValueAtTime(0.016, now + 0.3); + capstanEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration); + const wow = ctx.createOscillator(); + wow.type = 'sine'; + wow.frequency.setValueAtTime(8, now); + const wowDepth = ctx.createGain(); + wowDepth.gain.setValueAtTime(12, now); + wow.connect(wowDepth); + wowDepth.connect(capstan.frequency); + capstan.connect(capstanEnv); + capstanEnv.connect(this.gainNode); + capstan.start(now); + capstan.stop(now + duration + 0.1); + wow.start(now); + wow.stop(now + duration + 0.1); + + // Occasional fast-forward whine tail + if (Math.random() < 0.3) { + const whineT = now + duration - 0.55; + const whine = ctx.createOscillator(); + whine.type = 'sine'; + whine.frequency.setValueAtTime(1400, whineT); + whine.frequency.exponentialRampToValueAtTime(2300, whineT + 0.5); + const whineEnv = ctx.createGain(); + whineEnv.gain.setValueAtTime(0.0001, whineT); + whineEnv.gain.linearRampToValueAtTime(0.02, whineT + 0.08); + whineEnv.gain.exponentialRampToValueAtTime(0.0001, whineT + 0.5); + whine.connect(whineEnv); + whineEnv.connect(this.gainNode); + whine.start(whineT); + whine.stop(whineT + 0.55); + } + } + + /** + * Dot-Matrix / Teletype Chatter Phrase (audio_gaps.md #51, one phrase unit) + * A burst of 6-16 pseudo-random high-Q ~4 kHz impact spikes with periodic + * 12 Hz stepper clicks, phrased with a soft attack/release. The continuous + * Nostromo terminal loop is startTeletypeChatter() below. + */ + synthesizeTeletypeChatterPhrase() { + this.init(); + const ctx = this.am.ctx; + if (!ctx || !this.gainNode) return; + + const now = ctx.currentTime; + const phraseDur = 0.6 + Math.random() * 0.8; + + const masterEnv = ctx.createGain(); + masterEnv.gain.setValueAtTime(0.0001, now); + masterEnv.gain.linearRampToValueAtTime(1, now + 0.03); // soft phrase in + masterEnv.gain.setValueAtTime(1, now + phraseDur - 0.06); + masterEnv.gain.exponentialRampToValueAtTime(0.0001, now + phraseDur + 0.05); + masterEnv.connect(this.gainNode); + + const strikes = 6 + Math.floor(Math.random() * 11); // 6-16 spikes + let cursor = 0.02; + for (let i = 0; i < strikes; i++) { + if (cursor >= phraseDur - 0.1) break; // never schedule past the phrase out + const t = now + cursor; + const buf = this.am.createNoiseBuffer('white', 0.03); + const noise = ctx.createBufferSource(); + noise.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.setValueAtTime(3400 + Math.random() * 1200, t); + bp.Q.setValueAtTime(10 + Math.random() * 6, t); + const env = ctx.createGain(); + env.gain.setValueAtTime(0.1 + Math.random() * 0.1, t); + env.gain.exponentialRampToValueAtTime(0.001, t + 0.012 + Math.random() * 0.01); + noise.connect(bp); + bp.connect(env); + env.connect(masterEnv); + noise.start(t); + noise.stop(t + 0.035); + + // Periodic 12 Hz carriage stepper click + if (i % 3 === 2) { + const stepper = ctx.createOscillator(); + stepper.type = 'square'; + stepper.frequency.setValueAtTime(90, t); + stepper.frequency.linearRampToValueAtTime(70, t + 0.02); + const sEnv = ctx.createGain(); + sEnv.gain.setValueAtTime(0.06, t); + sEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.025); + stepper.connect(sEnv); + sEnv.connect(masterEnv); + stepper.start(t); + stepper.stop(t + 0.03); + } + cursor += 0.045 + Math.random() * 0.095; // jittered 45-140 ms strikes + } + } + stopAllLoops() { this.stopMedicalMonitor(); this.stopStationSparks(); diff --git a/js/config.js b/js/config.js index cee6abb..26aa46a 100644 --- a/js/config.js +++ b/js/config.js @@ -169,6 +169,7 @@ const UniverseRegistry = { { id: 'btn-cardassian-door', label: 'CARDASSIAN BULKHEAD DOOR' }, { id: 'btn-turbolift', label: 'TURBOLIFT WHOOSH' }, { id: 'btn-replicator', label: 'REPLICATOR SHIMMER' }, + { id: 'btn-transporter', label: 'TRANSPORTER CYCLE' }, { id: 'btn-chirp-single', label: 'LCARS SINGLE CHIRP' } ], presets: StarshipPresets @@ -310,7 +311,9 @@ const UniverseRegistry = { { id: 'btn-pipe-drips', label: 'CONDENSATION DRIPS' }, { id: 'btn-steam-vent', label: 'BOILER VENT HISS' }, { id: 'btn-crash-couch', label: 'CRASH-COUCH GIMBAL' }, - { id: 'btn-air-handler', label: 'HVAC AIR HANDLER' } + { id: 'btn-air-handler', label: 'HVAC AIR HANDLER' }, + { id: 'btn-dock-groan', label: 'DOCKING GROAN & CLANG' }, + { id: 'btn-teletype', label: 'DOT-MATRIX TELETYPE' } ], presets: { 'ind-nostromo': { @@ -506,7 +509,8 @@ const UniverseRegistry = { { id: 'btn-cygnus-chug', label: 'STEAM PISTON CHUG' }, { id: 'btn-chirp-single', label: 'ASTROGATOR BEAT' }, { id: 'btn-chirp-sweep', label: 'ANALOG GLISSANDO' }, - { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' } + { id: 'btn-relay-click', label: 'TAPE REEL SPOOL' }, + { id: 'btn-tape-spooler', label: 'DUAL-REEL TAPE SPOOLER' } ], presets: { 'ret-jupiter-2': { @@ -614,7 +618,8 @@ const UniverseRegistry = { { id: 'btn-dock-clamp', label: 'BULKHEAD SEAL' }, { id: 'btn-carousel-groan', label: 'CAROUSEL MOTOR GROAN' }, { id: 'btn-slipstream', label: 'SLIPSTREAM SURGE' }, - { id: 'btn-oxygen-reg', label: 'O2 DEMAND VALVE' } + { id: 'btn-oxygen-reg', label: 'O2 DEMAND VALVE' }, + { id: 'btn-breech-clank', label: 'WEAPONS BREECH CLANK' } ], presets: { 'mil-sulaco': {