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