Implement Phase 3 audio gaps: complex multi-stage one-shots (transporter, flak, breech, docking, tape spooler, teletype)
This commit is contained in:
@@ -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;
|
||||
|
||||
+491
@@ -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();
|
||||
|
||||
+8
-3
@@ -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': {
|
||||
|
||||
Reference in New Issue
Block a user