Files
SciFi-XZBT/js/audio.js
T
ClaudeandClaude Haiku 4.5 97b0faa4a6 Add sound reference documentation and fix air-handler/docking-clamp duplicate
- Add sound_reference.md (937 lines): Real-world production reference covering Star Trek (TOS–ENT), Doctor Who/Whoniverse, Bioships (six ships across five franchises), and Space Stations (six stations across five sources).
- Add agents.md: Project architecture guide (file ownership, load order, conventions).
- Add telemetry_elements.md: Catalog of 10 telemetry elements with per-era selection weights and preset-by-preset routing.
- Add visual_elements.md: Canvas/SVG split architecture, layer declarations, per-theme casts.
- Fix duplicate soundboard mapping: Separate 'AIR HANDLER THUD' from 'DOCKING CLAMP LATCH'. Create ExpandedSciFiAudioSynth.synthesizeAirHandlerThud() (dull triangle-wave thump + sub-octave + slow airflow whoosh) distinct from synthesizeDockingClamp() (bright square-wave impact + pneumatic hiss). Update js/app.js to wire btn-air-handler to the new method.

Key findings:
* All 70 presets across 10 universes use Star Trek telemetry eras only — cross-universe borrowing is structural, not accidental.
* Doctor Who TARDIS demat correctly implements Brian Hodgson's 1963 technique (piano strings + tape feedback).
* Sevastopol Station's production sound design (Jeff van Dyck, Pinewood foley) is the best-documented non-Trek entry.
* The Expanse's "jury-rigged" Belter signature (Nelson Ferreira) is the single most actionable production detail found.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NbMozG2xjcgLBia8vrzTrr
2026-09-04 05:08:04 +00:00

2334 lines
72 KiB
JavaScript

class AudioManager {
constructor() {
this.ctx = null;
this.isInitialized = false;
this.isPlaying = false;
this.masterGain = null;
this.compressor = null;
this.analyser = null;
this.currentVolume = 0.75;
this.isMuted = false;
// Sleep Timer
this.timerId = null;
this.timerRemainingSeconds = 0;
this.onTimerTick = null;
this.onTimerComplete = null;
}
init() {
if (this.isInitialized) return;
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContextClass();
// Master Dynamics Compressor / Limiter for studio-quality mastering & anti-clipping
this.compressor = this.ctx.createDynamicsCompressor();
this.compressor.threshold.setValueAtTime(-12, this.ctx.currentTime);
this.compressor.knee.setValueAtTime(8, this.ctx.currentTime);
this.compressor.ratio.setValueAtTime(4, this.ctx.currentTime);
this.compressor.attack.setValueAtTime(0.003, this.ctx.currentTime);
this.compressor.release.setValueAtTime(0.25, this.ctx.currentTime);
// Master Gain
this.masterGain = this.ctx.createGain();
this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : this.currentVolume, this.ctx.currentTime);
// Master Analyser Node for Visualizers
this.analyser = this.ctx.createAnalyser();
this.analyser.fftSize = 512;
this.analyser.smoothingTimeConstant = 0.82;
// Route: Nodes -> Compressor -> MasterGain -> Analyser -> Destination
this.compressor.connect(this.masterGain);
this.masterGain.connect(this.analyser);
this.analyser.connect(this.ctx.destination);
this.isInitialized = true;
}
async resume() {
if (!this.isInitialized) this.init();
if (this.ctx.state === 'suspended') {
await this.ctx.resume();
}
}
setMasterVolume(val, smoothTime = 0.05) {
const clamped = Math.max(0, Math.min(1, val));
this.currentVolume = clamped;
if (this.masterGain && this.ctx) {
const target = this.isMuted ? 0 : clamped;
const now = this.ctx.currentTime;
this.masterGain.gain.cancelScheduledValues(now);
this.masterGain.gain.linearRampToValueAtTime(target, now + smoothTime);
}
}
getMasterVolume() {
return this.currentVolume;
}
toggleMute() {
return this.setMute(!this.isMuted);
}
setMute(muted) {
this.isMuted = !!muted;
if (this.masterGain && this.ctx) {
const target = this.isMuted ? 0 : this.currentVolume;
const now = this.ctx.currentTime;
this.masterGain.gain.cancelScheduledValues(now);
this.masterGain.gain.linearRampToValueAtTime(target, now + 0.05);
}
return this.isMuted;
}
// Noise Buffer Helper (White, Pink, Brown)
createNoiseBuffer(type = 'pink', durationSeconds = 5) {
if (!this.ctx) this.init();
const sampleRate = this.ctx.sampleRate;
const bufferSize = sampleRate * durationSeconds;
const buffer = this.ctx.createBuffer(2, bufferSize, sampleRate);
const left = buffer.getChannelData(0);
const right = buffer.getChannelData(1);
if (type === 'white') {
for (let i = 0; i < bufferSize; i++) {
left[i] = Math.random() * 2 - 1;
right[i] = Math.random() * 2 - 1;
}
} else if (type === 'pink') {
let b0L = 0, b1L = 0, b2L = 0, b3L = 0, b4L = 0, b5L = 0, b6L = 0;
let b0R = 0, b1R = 0, b2R = 0, b3R = 0, b4R = 0, b5R = 0, b6R = 0;
for (let i = 0; i < bufferSize; i++) {
const whiteL = Math.random() * 2 - 1;
b0L = 0.99886 * b0L + whiteL * 0.0555179;
b1L = 0.99332 * b1L + whiteL * 0.0750759;
b2L = 0.96900 * b2L + whiteL * 0.1538520;
b3L = 0.86650 * b3L + whiteL * 0.3104856;
b4L = 0.55000 * b4L + whiteL * 0.5329522;
b5L = -0.7616 * b5L - whiteL * 0.0168980;
left[i] = (b0L + b1L + b2L + b3L + b4L + b5L + b6L + whiteL * 0.5362) * 0.11;
b6L = whiteL * 0.115926;
const whiteR = Math.random() * 2 - 1;
b0R = 0.99886 * b0R + whiteR * 0.0555179;
b1R = 0.99332 * b1R + whiteR * 0.0750759;
b2R = 0.96900 * b2R + whiteR * 0.1538520;
b3R = 0.86650 * b3R + whiteR * 0.3104856;
b4R = 0.55000 * b4R + whiteR * 0.5329522;
b5R = -0.7616 * b5R - whiteR * 0.0168980;
right[i] = (b0R + b1R + b2R + b3R + b4R + b5R + b6R + whiteR * 0.5362) * 0.11;
b6R = whiteR * 0.115926;
}
} else if (type === 'brown') {
let lastOutL = 0.0;
let lastOutR = 0.0;
for (let i = 0; i < bufferSize; i++) {
const whiteL = Math.random() * 2 - 1;
lastOutL = (lastOutL + 0.02 * whiteL) / 1.02;
left[i] = lastOutL * 3.5;
const whiteR = Math.random() * 2 - 1;
lastOutR = (lastOutR + 0.02 * whiteR) / 1.02;
right[i] = lastOutR * 3.5;
}
}
return buffer;
}
// Sleep Timer System
startSleepTimer(minutes, onTick, onComplete) {
this.stopSleepTimer();
this.timerRemainingSeconds = Math.round(minutes * 60);
this.onTimerTick = onTick;
this.onTimerComplete = onComplete;
if (this.onTimerTick) this.onTimerTick(this.timerRemainingSeconds);
this.timerId = setInterval(() => {
this.timerRemainingSeconds--;
if (this.onTimerTick) this.onTimerTick(this.timerRemainingSeconds);
// Begin exponential smooth fadeout during final 30 seconds
if (this.timerRemainingSeconds <= 30 && this.timerRemainingSeconds > 0) {
const factor = this.timerRemainingSeconds / 30;
if (this.masterGain && this.ctx) {
const targetVol = this.getMasterVolume() * factor;
this.masterGain.gain.setValueAtTime(Math.max(0, targetVol), this.ctx.currentTime);
}
}
if (this.timerRemainingSeconds <= 0) {
this.stopSleepTimer();
if (this.onTimerComplete) this.onTimerComplete();
}
}, 1000);
}
stopSleepTimer() {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
this.timerRemainingSeconds = 0;
}
}
}
window.AudioManager = AudioManager;
/**
* Hull Drone & Environmental Sub-Bass Synthesizer
* Generates organic, continuous low-frequency starship structural vibration & room tone.
*/
class HullDroneSynth {
constructor(audioManager) {
this.am = audioManager;
this.nodes = [];
this.gainNode = null;
this.filterNode = null;
this.subOsc1 = null;
this.subOsc2 = null;
this.noiseSource = null;
this.isMuted = false;
// Default configuration parameters
this.params = {
volume: 0.7,
baseFreq: 50, // Fundamental frequency (e.g. 50Hz for TNG bridge)
filterCutoff: 110, // Lowpass filter cutoff
resonance: 2.5, // Filter Q / resonance peak
noiseMix: 0.45, // Brown noise texture mix
harmonicSpread: 1.02 // Slight frequency detune between sub-oscillators for phasing
};
}
start() {
this.stop();
const ctx = this.am.ctx;
if (!ctx) return;
// Channel Gain Node
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(this.isMuted ? 0 : this.params.volume, ctx.currentTime);
// Steep Lowpass Filter (24dB/oct via 2 cascading biquads)
this.filterNode = ctx.createBiquadFilter();
this.filterNode.type = 'lowpass';
this.filterNode.frequency.setValueAtTime(this.params.filterCutoff, ctx.currentTime);
this.filterNode.Q.setValueAtTime(this.params.resonance, ctx.currentTime);
const filterStage2 = ctx.createBiquadFilter();
filterStage2.type = 'lowpass';
filterStage2.frequency.setValueAtTime(this.params.filterCutoff * 1.5, ctx.currentTime);
filterStage2.Q.setValueAtTime(1.0, ctx.currentTime);
// Sub-bass Oscillator 1 (Sine)
this.subOsc1 = ctx.createOscillator();
this.subOsc1.type = 'sine';
this.subOsc1.frequency.setValueAtTime(this.params.baseFreq, ctx.currentTime);
const osc1Gain = ctx.createGain();
osc1Gain.gain.setValueAtTime(0.5, ctx.currentTime);
this.subOsc1.connect(osc1Gain);
osc1Gain.connect(this.filterNode);
// Sub-bass Oscillator 2 (Triangle/Sine detuned for slow, natural phase beating)
this.subOsc2 = ctx.createOscillator();
this.subOsc2.type = 'triangle';
this.subOsc2.frequency.setValueAtTime(this.params.baseFreq * this.params.harmonicSpread, ctx.currentTime);
const osc2Gain = ctx.createGain();
osc2Gain.gain.setValueAtTime(0.3, ctx.currentTime);
this.subOsc2.connect(osc2Gain);
osc2Gain.connect(this.filterNode);
// Brown Noise Structural Rumble Layer
const brownBuffer = this.am.createNoiseBuffer('brown', 6);
this.noiseSource = ctx.createBufferSource();
this.noiseSource.buffer = brownBuffer;
this.noiseSource.loop = true;
const noiseGain = ctx.createGain();
noiseGain.gain.setValueAtTime(this.params.noiseMix * 0.7, ctx.currentTime);
this.noiseSource.connect(noiseGain);
noiseGain.connect(this.filterNode);
// Slow LFO for organic drifting movement
const lfo = ctx.createOscillator();
lfo.type = 'sine';
lfo.frequency.setValueAtTime(0.1, ctx.currentTime); // 10 second cycle
const lfoGain = ctx.createGain();
lfoGain.gain.setValueAtTime(12, ctx.currentTime); // Modulate cutoff by ±12Hz
lfo.connect(lfoGain);
lfoGain.connect(this.filterNode.frequency);
// Connect Graph
this.filterNode.connect(filterStage2);
filterStage2.connect(this.gainNode);
this.gainNode.connect(this.am.compressor);
// Start Sources
this.subOsc1.start();
this.subOsc2.start();
this.noiseSource.start();
lfo.start();
this.nodes = [this.subOsc1, this.subOsc2, this.noiseSource, lfo, osc1Gain, osc2Gain, noiseGain, lfoGain, this.filterNode, filterStage2, this.gainNode];
}
stop() {
if (this.nodes.length > 0) {
try {
if (this.subOsc1) this.subOsc1.stop();
if (this.subOsc2) this.subOsc2.stop();
if (this.noiseSource) this.noiseSource.stop();
} catch (e) {
// Ignore if already stopped
}
this.nodes.forEach(node => {
try { node.disconnect(); } catch (e) {}
});
this.nodes = [];
}
}
setVolume(val) {
this.params.volume = Math.max(0, Math.min(1, val));
if (this.gainNode && this.am.ctx && !this.isMuted) {
const now = this.am.ctx.currentTime;
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05);
}
}
setBaseFreq(freq) {
this.params.baseFreq = freq;
if (this.subOsc1 && this.subOsc2 && this.am.ctx) {
const now = this.am.ctx.currentTime;
this.subOsc1.frequency.linearRampToValueAtTime(freq, now + 0.1);
this.subOsc2.frequency.linearRampToValueAtTime(freq * this.params.harmonicSpread, now + 0.1);
}
}
setFilterCutoff(cutoff) {
this.params.filterCutoff = cutoff;
if (this.filterNode && this.am.ctx) {
const now = this.am.ctx.currentTime;
this.filterNode.frequency.linearRampToValueAtTime(cutoff, now + 0.1);
}
}
applyPreset(config) {
if (config.volume !== undefined) this.params.volume = config.volume;
if (config.baseFreq !== undefined) this.setBaseFreq(config.baseFreq);
if (config.filterCutoff !== undefined) this.setFilterCutoff(config.filterCutoff);
if (config.resonance !== undefined) this.params.resonance = config.resonance;
if (config.noiseMix !== undefined) this.params.noiseMix = config.noiseMix;
if (config.harmonicSpread !== undefined) this.params.harmonicSpread = config.harmonicSpread;
this.setVolume(this.params.volume);
}
}
window.HullDroneSynth = HullDroneSynth;
/**
* Warp Core & Reactor Pulse Synthesizer
* Generates the iconic pulsating magnetic intermix thrum of Star Trek warp cores.
*/
class WarpCoreSynth {
constructor(audioManager) {
this.am = audioManager;
this.nodes = [];
this.gainNode = null;
this.isMuted = false;
// Pulse parameters
this.params = {
volume: 0.8,
bpm: 48, // Pulse rate (TNG is ~46-52 BPM, Voyager is ~68-75 BPM)
carrierFreq: 58, // Fundamental carrier pitch (Hz)
modFreqRatio: 2.0, // FM modulation frequency multiplier
modIndex: 40, // FM modulation depth
filterCutoff: 180, // Lowpass filter cutoff
pulseShape: 'tng', // 'tng', 'voyager', 'tos', 'defiant', 'nx'
resonance: 3.0,
swirlMix: 0.35 // Stereo phase swirl
};
this.pulseInterval = null;
this.pulsePhase = 0;
this.onPulse = null; // Callback for UI visualizer pulse animation!
}
start() {
this.stop();
const ctx = this.am.ctx;
if (!ctx) return;
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(this.isMuted ? 0 : this.params.volume, ctx.currentTime);
// Filter Node
this.filterNode = ctx.createBiquadFilter();
this.filterNode.type = 'lowpass';
this.filterNode.frequency.setValueAtTime(this.params.filterCutoff, ctx.currentTime);
this.filterNode.Q.setValueAtTime(this.params.resonance, ctx.currentTime);
// Stereo Panner for magnetic swirl
this.panner = ctx.createStereoPanner ? ctx.createStereoPanner() : null;
// Carrier & Modulator Oscillators (FM Engine)
this.carrier = ctx.createOscillator();
this.carrier.type = this.params.pulseShape === 'tos' ? 'sawtooth' : (this.params.pulseShape === 'nx' ? 'triangle' : 'sine');
this.carrier.frequency.setValueAtTime(this.params.carrierFreq, ctx.currentTime);
// Sub-harmonic oscillator for massive bottom end
this.subOsc = ctx.createOscillator();
this.subOsc.type = 'sine';
this.subOsc.frequency.setValueAtTime(this.params.carrierFreq * 0.5, ctx.currentTime);
const subGain = ctx.createGain();
subGain.gain.setValueAtTime(0.6, ctx.currentTime);
this.subOsc.connect(subGain);
subGain.connect(this.filterNode);
// Pulse Envelope Modulator Gain Node
this.pulseGain = ctx.createGain();
this.pulseGain.gain.setValueAtTime(0.2, ctx.currentTime);
// Connect Carrier -> PulseGain -> Filter -> Panner -> ChannelGain -> Master
this.carrier.connect(this.pulseGain);
this.pulseGain.connect(this.filterNode);
if (this.panner) {
this.filterNode.connect(this.panner);
this.panner.connect(this.gainNode);
} else {
this.filterNode.connect(this.gainNode);
}
this.gainNode.connect(this.am.compressor);
this.carrier.start();
this.subOsc.start();
this.nodes = [this.carrier, this.subOsc, subGain, this.pulseGain, this.filterNode, this.gainNode];
if (this.panner) this.nodes.push(this.panner);
// Start precision pulse scheduler
this.startPulseLoop();
}
startPulseLoop() {
if (this.pulseInterval) clearInterval(this.pulseInterval);
const intervalMs = (60 / this.params.bpm) * 1000;
this.scheduleNextPulse();
this.pulseInterval = setInterval(() => {
this.scheduleNextPulse();
}, intervalMs);
}
scheduleNextPulse() {
const ctx = this.am.ctx;
if (!ctx || !this.pulseGain || !this.filterNode) return;
const now = ctx.currentTime;
const pulseDuration = (60 / this.params.bpm);
this.pulsePhase = (this.pulsePhase + 1) % 4;
// Trigger visualizer callback
if (this.onPulse) {
this.onPulse(this.pulsePhase, pulseDuration);
}
// Dynamic envelope shaping based on ship era
if (this.params.pulseShape === 'tng') {
// Iconic 4-stage Galaxy-class magnetic warp pulse
// Soft attack, deep swelling peak, secondary reverberant harmonic bloom, smooth decay
const peakTime = now + pulseDuration * 0.28;
const secondPeak = now + pulseDuration * 0.58;
this.pulseGain.gain.cancelScheduledValues(now);
this.pulseGain.gain.setValueAtTime(0.18, now);
this.pulseGain.gain.linearRampToValueAtTime(0.95, peakTime);
this.pulseGain.gain.exponentialRampToValueAtTime(0.45, now + pulseDuration * 0.42);
this.pulseGain.gain.linearRampToValueAtTime(0.65, secondPeak);
this.pulseGain.gain.exponentialRampToValueAtTime(0.18, now + pulseDuration * 0.95);
// Modulate filter cutoff in sync with the pulse
this.filterNode.frequency.cancelScheduledValues(now);
this.filterNode.frequency.setValueAtTime(this.params.filterCutoff * 0.7, now);
this.filterNode.frequency.exponentialRampToValueAtTime(this.params.filterCutoff * 1.6, peakTime);
this.filterNode.frequency.exponentialRampToValueAtTime(this.params.filterCutoff * 0.7, now + pulseDuration * 0.95);
} else if (this.params.pulseShape === 'voyager') {
// Faster, sharper, higher-resonance Class 9 warp core
const peakTime = now + pulseDuration * 0.2;
this.pulseGain.gain.cancelScheduledValues(now);
this.pulseGain.gain.setValueAtTime(0.25, now);
this.pulseGain.gain.linearRampToValueAtTime(1.0, peakTime);
this.pulseGain.gain.exponentialRampToValueAtTime(0.25, now + pulseDuration * 0.85);
this.filterNode.frequency.cancelScheduledValues(now);
this.filterNode.frequency.linearRampToValueAtTime(this.params.filterCutoff * 1.8, peakTime);
this.filterNode.frequency.linearRampToValueAtTime(this.params.filterCutoff * 0.8, now + pulseDuration * 0.85);
} else if (this.params.pulseShape === 'tos') {
// TOS Electromechanical oscillating engine thrum
const halfTime = now + pulseDuration * 0.5;
this.pulseGain.gain.cancelScheduledValues(now);
this.pulseGain.gain.setValueAtTime(0.4, now);
this.pulseGain.gain.linearRampToValueAtTime(0.9, halfTime);
this.pulseGain.gain.linearRampToValueAtTime(0.4, now + pulseDuration);
} else if (this.params.pulseShape === 'defiant') {
// Defiant: tight, aggressive pulse with rapid decay
const peakTime = now + pulseDuration * 0.15;
this.pulseGain.gain.cancelScheduledValues(now);
this.pulseGain.gain.setValueAtTime(0.3, now);
this.pulseGain.gain.linearRampToValueAtTime(1.0, peakTime);
this.pulseGain.gain.exponentialRampToValueAtTime(0.3, now + pulseDuration * 0.75);
} else {
// NX / Industrial reactor chug
const peakTime = now + pulseDuration * 0.35;
this.pulseGain.gain.cancelScheduledValues(now);
this.pulseGain.gain.setValueAtTime(0.2, now);
this.pulseGain.gain.linearRampToValueAtTime(0.85, peakTime);
this.pulseGain.gain.linearRampToValueAtTime(0.2, now + pulseDuration);
}
// Subtle stereo panning drift
if (this.panner && this.params.swirlMix > 0) {
const panTarget = Math.sin(this.pulsePhase * Math.PI * 0.5) * this.params.swirlMix;
this.panner.pan.linearRampToValueAtTime(panTarget, now + pulseDuration * 0.5);
}
}
setBpm(bpm) {
this.params.bpm = Math.max(20, Math.min(160, bpm));
if (this.nodes.length > 0) {
this.startPulseLoop();
}
}
setVolume(val) {
this.params.volume = Math.max(0, Math.min(1, val));
if (this.gainNode && this.am.ctx && !this.isMuted) {
const now = this.am.ctx.currentTime;
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05);
}
}
setCarrierFreq(freq) {
this.params.carrierFreq = freq;
if (this.carrier && this.subOsc && this.am.ctx) {
const now = this.am.ctx.currentTime;
this.carrier.frequency.linearRampToValueAtTime(freq, now + 0.1);
this.subOsc.frequency.linearRampToValueAtTime(freq * 0.5, now + 0.1);
}
}
stop() {
if (this.pulseInterval) {
clearInterval(this.pulseInterval);
this.pulseInterval = null;
}
if (this.nodes.length > 0) {
try {
if (this.carrier) this.carrier.stop();
if (this.subOsc) this.subOsc.stop();
} catch (e) {}
this.nodes.forEach(node => {
try { node.disconnect(); } catch (e) {}
});
this.nodes = [];
}
}
applyPreset(config) {
if (config.volume !== undefined) this.params.volume = config.volume;
if (config.bpm !== undefined) this.setBpm(config.bpm);
if (config.carrierFreq !== undefined) this.setCarrierFreq(config.carrierFreq);
if (config.filterCutoff !== undefined) this.params.filterCutoff = config.filterCutoff;
if (config.pulseShape !== undefined) this.params.pulseShape = config.pulseShape;
if (config.resonance !== undefined) this.params.resonance = config.resonance;
if (config.swirlMix !== undefined) this.params.swirlMix = config.swirlMix;
this.setVolume(this.params.volume);
}
}
window.WarpCoreSynth = WarpCoreSynth;
/**
* Environmental Life Support & Airflow Synthesizer
* Generates continuous ventilation airflow, atmospheric hiss, and room acoustic damping.
*/
class LifeSupportSynth {
constructor(audioManager) {
this.am = audioManager;
this.nodes = [];
this.gainNode = null;
this.isMuted = false;
this.params = {
volume: 0.5,
noiseType: 'pink', // 'pink' (warm TNG), 'white' (crisp Voyager), 'brown' (heavy NX-01)
highpassFreq: 180, // Cuts extreme sub rumble to isolate air movement
lowpassFreq: 1800, // Gentle top-end rolloff
airflowModSpeed: 0.15, // Subtle breathing movement of the environmental airflow
airflowModDepth: 0.12 // Depth of airflow intensity modulation
};
}
start() {
this.stop();
const ctx = this.am.ctx;
if (!ctx) return;
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(this.isMuted ? 0 : this.params.volume, ctx.currentTime);
// Highpass filter (cuts muddy lows)
const hpFilter = ctx.createBiquadFilter();
hpFilter.type = 'highpass';
hpFilter.frequency.setValueAtTime(this.params.highpassFreq, ctx.currentTime);
// Lowpass filter (shapes the crispness vs warmth of the air)
this.lpFilter = ctx.createBiquadFilter();
this.lpFilter.type = 'lowpass';
this.lpFilter.frequency.setValueAtTime(this.params.lowpassFreq, ctx.currentTime);
this.lpFilter.Q.setValueAtTime(0.7, ctx.currentTime);
// Noise buffer source
const noiseBuffer = this.am.createNoiseBuffer(this.params.noiseType, 6);
this.noiseSource = ctx.createBufferSource();
this.noiseSource.buffer = noiseBuffer;
this.noiseSource.loop = true;
// Slow airflow modulation LFO for organic breath
const airflowLfo = ctx.createOscillator();
airflowLfo.type = 'sine';
airflowLfo.frequency.setValueAtTime(this.params.airflowModSpeed, ctx.currentTime);
const lfoGain = ctx.createGain();
lfoGain.gain.setValueAtTime(this.params.airflowModDepth, ctx.currentTime);
const modGain = ctx.createGain();
modGain.gain.setValueAtTime(0.8, ctx.currentTime);
airflowLfo.connect(lfoGain);
lfoGain.connect(modGain.gain);
// Stereo widener using delay
const splitter = ctx.createChannelSplitter(2);
const merger = ctx.createChannelMerger(2);
const delayRight = ctx.createDelay();
delayRight.delayTime.setValueAtTime(0.018, ctx.currentTime); // 18ms Haas effect widening
// Graph: Noise -> ModGain -> HP -> LP -> Splitter -> (Left direct, Right delay) -> Merger -> Gain -> Compressor
this.noiseSource.connect(modGain);
modGain.connect(hpFilter);
hpFilter.connect(this.lpFilter);
this.lpFilter.connect(splitter);
splitter.connect(merger, 0, 0); // Left channel
splitter.connect(delayRight, 1);
delayRight.connect(merger, 0, 1); // Right delayed channel
merger.connect(this.gainNode);
this.gainNode.connect(this.am.compressor);
this.noiseSource.start();
airflowLfo.start();
this.nodes = [
this.noiseSource, airflowLfo, lfoGain, modGain,
hpFilter, this.lpFilter, splitter, delayRight, merger, this.gainNode
];
}
stop() {
if (this.nodes.length > 0) {
try {
if (this.noiseSource) this.noiseSource.stop();
} catch (e) {}
this.nodes.forEach(node => {
try { node.disconnect(); } catch (e) {}
});
this.nodes = [];
}
}
setVolume(val) {
this.params.volume = Math.max(0, Math.min(1, val));
if (this.gainNode && this.am.ctx && !this.isMuted) {
const now = this.am.ctx.currentTime;
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05);
}
}
setFilterCutoff(freq) {
this.params.lowpassFreq = freq;
if (this.lpFilter && this.am.ctx) {
const now = this.am.ctx.currentTime;
this.lpFilter.frequency.linearRampToValueAtTime(freq, now + 0.1);
}
}
applyPreset(config) {
if (config.volume !== undefined) this.params.volume = config.volume;
if (config.noiseType !== undefined) this.params.noiseType = config.noiseType;
if (config.highpassFreq !== undefined) this.params.highpassFreq = config.highpassFreq;
if (config.lowpassFreq !== undefined) this.setFilterCutoff(config.lowpassFreq);
if (config.airflowModSpeed !== undefined) this.params.airflowModSpeed = config.airflowModSpeed;
if (config.airflowModDepth !== undefined) this.params.airflowModDepth = config.airflowModDepth;
this.setVolume(this.params.volume);
}
}
window.LifeSupportSynth = LifeSupportSynth;
/**
* Procedural Starship Telemetry, LCARS Chirps, Beeps & Console Synthesizer
* 100% synthesized programmatically via Web Audio API oscillators, FM synthesis, and envelopes.
* Zero stored audio samples.
*/
class TelemetrySynth {
constructor(audioManager) {
this.am = audioManager;
this.gainNode = null;
this.isMuted = false;
this.schedulerTimer = null;
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'
reverbMix: 0.25
};
// Musical pitch frequencies for authentic LCARS musical intervals (major/minor pentatonic & perfect 4ths/5ths)
this.lcarsPitches = [
880, 987.77, 1046.50, 1174.66, 1318.51, 1396.91, 1567.98, 1760, 1975.53, 2093.00, 2349.32, 2637.02
];
// TOS Bridge oscillator warble frequencies
this.tosFrequencies = [
440, 554.37, 659.25, 830.61, 880, 1108.73, 1318.51, 1661.22, 2217.46
];
}
start() {
this.stop();
const ctx = this.am.ctx;
if (!ctx) return;
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(this.isMuted ? 0 : this.params.volume, ctx.currentTime);
this.gainNode.connect(this.am.compressor);
this.startAutoTelemetryScheduler();
}
stop() {
if (this.schedulerTimer) {
clearTimeout(this.schedulerTimer);
this.schedulerTimer = null;
}
}
setVolume(val) {
this.params.volume = Math.max(0, Math.min(1, val));
if (this.gainNode && this.am.ctx && !this.isMuted) {
const now = this.am.ctx.currentTime;
this.gainNode.gain.cancelScheduledValues(now);
this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05);
}
}
setDensity(val) {
this.params.density = Math.max(0, Math.min(1, val));
}
startAutoTelemetryScheduler() {
if (this.schedulerTimer) clearTimeout(this.schedulerTimer);
if (this.params.density <= 0.01) return;
// Calculate delay inversely proportional to density (2s to 12s)
const baseDelay = 12000 * (1.05 - this.params.density);
const jitter = Math.random() * 4000;
const nextInterval = Math.max(800, baseDelay + jitter);
this.schedulerTimer = setTimeout(() => {
this.playRandomTelemetrySound();
this.startAutoTelemetryScheduler();
}, nextInterval);
}
playRandomTelemetrySound() {
if (this.isMuted || this.params.volume <= 0.01 || !this.am.ctx) return;
switch (this.params.era) {
case 'tos':
Math.random() > 0.4 ? this.synthesizeTOSWarble() : this.synthesizeTOSRelayClick();
break;
case 'ds9':
Math.random() > 0.5 ? this.synthesizeCardassianSensor() : this.synthesizeLCARSSingleChirp();
break;
case 'voyager':
Math.random() > 0.4 ? this.synthesizeLCARSDoubleChirp() : this.synthesizeSensorSweep();
break;
case 'nx':
Math.random() > 0.5 ? this.synthesizeNXRelay() : this.synthesizeNXIndicatorBeep();
break;
case 'tng':
default:
const r = Math.random();
if (r < 0.45) this.synthesizeLCARSSingleChirp();
else if (r < 0.75) this.synthesizeLCARSDoubleChirp();
else if (r < 0.90) this.synthesizeLCARSSequence();
else this.synthesizeSensorSweep();
break;
}
// OBSERVATION intentionally treats telemetry as an abstract activity pulse,
// not as a claim that a specific fictional beep means a specific thing.
window.dispatchEvent(new CustomEvent('scifi-telemetry-activity', {
detail: {
era: this.params.era,
density: this.params.density,
firedAt: performance.now()
}
}));
}
/**
* TNG/Voyager Single LCARS Touch Tone (Soft sine with gentle attack and rapid exponential decay)
*/
synthesizeLCARSSingleChirp(pitch = null) {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const freq = pitch || this.lcarsPitches[Math.floor(Math.random() * this.lcarsPitches.length)];
const duration = 0.09;
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now);
// Subtle downward micro-pitch glide (12Hz) for that warm capacitive touch feel
osc.frequency.exponentialRampToValueAtTime(freq * 0.98, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.35, now + 0.008);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
// Filter to eliminate any click
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(3200, now);
osc.connect(env);
env.connect(filter);
filter.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* TNG LCARS Double Chirp (Iconic standard confirmation tone)
*/
synthesizeLCARSDoubleChirp() {
const ctx = this.am.ctx;
if (!ctx) return;
const idx = Math.floor(Math.random() * (this.lcarsPitches.length - 2));
const p1 = this.lcarsPitches[idx];
const p2 = this.lcarsPitches[idx + 2]; // Minor third or fourth higher
this.synthesizeLCARSSingleChirp(p1);
setTimeout(() => {
this.synthesizeLCARSSingleChirp(p2);
}, 65);
}
/**
* TNG LCARS Multi-Tone Data Acknowledgment Sequence
*/
synthesizeLCARSSequence() {
const ctx = this.am.ctx;
if (!ctx) return;
const notes = [
this.lcarsPitches[Math.floor(Math.random() * 4) + 4],
this.lcarsPitches[Math.floor(Math.random() * 4) + 6],
this.lcarsPitches[Math.floor(Math.random() * 4) + 2]
];
notes.forEach((freq, i) => {
setTimeout(() => {
this.synthesizeLCARSSingleChirp(freq);
}, i * 75);
});
}
/**
* High-tech Sensor Sweep Tone (Voyager/TNG Long-Range Sensor telemetry)
*/
synthesizeSensorSweep() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.38;
const startFreq = 1200 + Math.random() * 800;
const endFreq = startFreq * (Math.random() > 0.5 ? 1.6 : 0.65);
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(startFreq, now);
osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.18, now + 0.05);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* TOS Original Series Bridge Electronic Computer Warble
* Two detuned square/triangle oscillators modulated by high-speed vibrato LFO
*/
synthesizeTOSWarble() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.45;
const baseFreq = this.tosFrequencies[Math.floor(Math.random() * this.tosFrequencies.length)];
const osc1 = ctx.createOscillator();
osc1.type = 'triangle';
osc1.frequency.setValueAtTime(baseFreq, now);
const osc2 = ctx.createOscillator();
osc2.type = 'sawtooth';
osc2.frequency.setValueAtTime(baseFreq * 1.5, now);
// Fast Vibrato LFO
const lfo = ctx.createOscillator();
lfo.type = 'sine';
lfo.frequency.setValueAtTime(14 + Math.random() * 8, now); // 14-22 Hz warble
const lfoGain = ctx.createGain();
lfoGain.gain.setValueAtTime(35, now);
lfo.connect(lfoGain);
lfoGain.connect(osc1.frequency);
lfoGain.connect(osc2.frequency);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.22, now + 0.04);
env.gain.setValueAtTime(0.22, now + duration * 0.7);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
// Bandpass filter to create that vintage analog 1960s telephone/relay resonance
const filter = ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(baseFreq * 1.2, now);
filter.Q.setValueAtTime(3.5, now);
osc1.connect(env);
osc2.connect(env);
env.connect(filter);
filter.connect(this.gainNode);
osc1.start(now);
osc2.start(now);
lfo.start(now);
osc1.stop(now + duration);
osc2.stop(now + duration);
lfo.stop(now + duration);
}
/**
* TOS Mechanical Relay Solenoid Click
*/
synthesizeTOSRelayClick() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.025;
const osc = ctx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(1400, now);
osc.frequency.exponentialRampToValueAtTime(300, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.3, now);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* DS9 / Cardassian Cavernous Sensor Tone (Resonant metallic ring)
*/
synthesizeCardassianSensor() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.55;
const freq = 420 + Math.random() * 200;
const osc1 = ctx.createOscillator();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(freq, now);
const osc2 = ctx.createOscillator();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(freq * 1.414, now); // Tritone metallic dissonance
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.2, now + 0.015);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc1.connect(env);
osc2.connect(env);
env.connect(this.gainNode);
osc1.start(now);
osc2.start(now);
osc1.stop(now + duration);
osc2.stop(now + duration);
}
/**
* NX-01 Industrial Hydraulic Relay Click
*/
synthesizeNXRelay() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.04;
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(750, now);
osc.frequency.exponentialRampToValueAtTime(120, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.25, now);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* NX-01 Indicator Beep (Early 22nd century industrial tone)
*/
synthesizeNXIndicatorBeep() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.08;
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(950, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.2, now + 0.005);
env.gain.setValueAtTime(0.2, now + duration * 0.8);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Iconic TNG 2-Tone Door Chime ("Come in")
*/
synthesizeDoorChime() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const f1 = 880; // A5
const f2 = 1174.66; // D6 (Up a fourth)
const osc1 = ctx.createOscillator();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(f1, now);
const env1 = ctx.createGain();
env1.gain.setValueAtTime(0.001, now);
env1.gain.linearRampToValueAtTime(0.35, now + 0.015);
env1.gain.exponentialRampToValueAtTime(0.001, now + 0.45);
osc1.connect(env1);
env1.connect(this.gainNode);
osc1.start(now);
osc1.stop(now + 0.45);
// Second tone starts at 0.16s
const osc2 = ctx.createOscillator();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(f2, now + 0.16);
const env2 = ctx.createGain();
env2.gain.setValueAtTime(0.001, now + 0.16);
env2.gain.linearRampToValueAtTime(0.4, now + 0.175);
env2.gain.exponentialRampToValueAtTime(0.001, now + 0.7);
osc2.connect(env2);
env2.connect(this.gainNode);
osc2.start(now + 0.16);
osc2.stop(now + 0.7);
}
applyPreset(config) {
if (config.volume !== undefined) this.params.volume = config.volume;
if (config.density !== undefined) this.setDensity(config.density);
if (config.era !== undefined) this.params.era = config.era;
this.setVolume(this.params.volume);
this.startAutoTelemetryScheduler();
}
}
window.TelemetrySynth = TelemetrySynth;
/**
* Procedural Starship Alert & Event Synthesizer
* 100% synthesized programmatically in Web Audio API.
* Includes TNG Red Alert (3-tone), TOS Red Alert (hooter buzzer), Movie-era descending klaxon,
* Yellow Alert chime, and dynamic Warp Drive Throttle swell.
*/
class AlertSynth {
constructor(audioManager) {
this.am = audioManager;
this.gainNode = null;
this.activeAlert = null; // 'red', 'yellow', null
this.alertTimer = null;
this.alertType = 'tng'; // 'tng', 'tos', 'movie'
this.params = {
volume: 0.6
};
}
init() {
if (this.gainNode || !this.am.ctx) return;
const ctx = this.am.ctx;
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(this.params.volume, ctx.currentTime);
this.gainNode.connect(this.am.compressor);
}
setVolume(val) {
this.params.volume = Math.max(0, Math.min(1, val));
if (this.gainNode && this.am.ctx) {
const now = this.am.ctx.currentTime;
this.gainNode.gain.linearRampToValueAtTime(this.params.volume, now + 0.05);
}
}
triggerRedAlert(type = 'tng') {
this.init();
this.stopAlert();
this.activeAlert = 'red';
this.alertType = type;
const playLoop = () => {
if (this.activeAlert !== 'red') return;
let loopDuration = 1.35;
if (this.alertType === 'tng') {
this.synthesizeTNGRedAlertCycle();
loopDuration = 1.35;
} else if (this.alertType === 'tos') {
this.synthesizeTOSRedAlertCycle();
loopDuration = 1.1;
} else {
this.synthesizeMovieRedAlertCycle();
loopDuration = 1.4;
}
this.alertTimer = setTimeout(playLoop, loopDuration * 1000);
};
playLoop();
}
triggerYellowAlert() {
this.init();
this.stopAlert();
this.activeAlert = 'yellow';
const playLoop = () => {
if (this.activeAlert !== 'yellow') return;
this.synthesizeYellowAlertCycle();
this.alertTimer = setTimeout(playLoop, 2200);
};
playLoop();
}
stopAlert() {
this.activeAlert = null;
if (this.alertTimer) {
clearTimeout(this.alertTimer);
this.alertTimer = null;
}
}
/**
* TNG Red Alert Klaxon (3-tone rising & cascading electronic horn with resonant envelope)
*/
synthesizeTNGRedAlertCycle() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
// Frequencies for the iconic TNG 3-tone klaxon chord: F5 (698.46Hz), Ab5 (830.61Hz), C6 (1046.50Hz)
const freqs = [698.46, 830.61, 1046.50];
freqs.forEach((freq, idx) => {
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(freq * 0.94, now);
// Fast upward swoop on trigger
osc.frequency.exponentialRampToValueAtTime(freq, now + 0.12);
// Lowpass filter to give that brassy starship horn acoustic resonance
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1600, now);
filter.Q.setValueAtTime(4.0, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.25 / freqs.length, now + 0.08);
env.gain.setValueAtTime(0.25 / freqs.length, now + 0.45);
env.gain.exponentialRampToValueAtTime(0.0001, now + 0.85);
osc.connect(filter);
filter.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + 0.86);
});
}
/**
* TOS Red Alert Buzzer / Hooter Siren (Pulsing 2-tone frequency modulation)
*/
synthesizeTOSRedAlertCycle() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.85;
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(540, now);
osc.frequency.linearRampToValueAtTime(920, now + duration * 0.5);
osc.frequency.linearRampToValueAtTime(540, now + duration);
const filter = ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(800, now);
filter.Q.setValueAtTime(2.2, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.01, now);
env.gain.linearRampToValueAtTime(0.28, now + 0.05);
env.gain.setValueAtTime(0.28, now + duration * 0.85);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(filter);
filter.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Star Trek Movie Era Refit Descending Red Alert Klaxon
*/
synthesizeMovieRedAlertCycle() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 1.05;
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(1350, now);
osc.frequency.exponentialRampToValueAtTime(420, now + duration * 0.9);
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(2200, now);
filter.Q.setValueAtTime(3.5, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.01, now);
env.gain.linearRampToValueAtTime(0.3, now + 0.06);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(filter);
filter.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Yellow Alert Pulsing Warning Chime
*/
synthesizeYellowAlertCycle() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const f1 = 660; // E5
const f2 = 880; // A5
[0, 0.22].forEach((offset, idx) => {
const freq = idx === 0 ? f1 : f2;
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + offset);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now + offset);
env.gain.linearRampToValueAtTime(0.32, now + offset + 0.015);
env.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.5);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now + offset);
osc.stop(now + offset + 0.52);
});
}
/**
* Procedural Warp Drive Acceleration Swell ("Engage!")
* Synthesizes rising plasma induction whine + deep bass detonation
*/
synthesizeWarpJump() {
this.init();
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 2.8;
// 1. Rising High Induction Whine
const whineOsc = ctx.createOscillator();
whineOsc.type = 'sawtooth';
whineOsc.frequency.setValueAtTime(80, now);
whineOsc.frequency.exponentialRampToValueAtTime(3800, now + 1.8);
whineOsc.frequency.exponentialRampToValueAtTime(14000, now + 2.5);
const whineFilter = ctx.createBiquadFilter();
whineFilter.type = 'bandpass';
whineFilter.frequency.setValueAtTime(200, now);
whineFilter.frequency.exponentialRampToValueAtTime(4500, now + 1.8);
whineFilter.Q.setValueAtTime(5.0, now);
const whineEnv = ctx.createGain();
whineEnv.gain.setValueAtTime(0.01, now);
whineEnv.gain.linearRampToValueAtTime(0.35, now + 1.6);
whineEnv.gain.exponentialRampToValueAtTime(0.001, now + 2.7);
whineOsc.connect(whineFilter);
whineFilter.connect(whineEnv);
whineEnv.connect(this.gainNode);
// 2. Sub-bass Matter-Antimatter Boom
const subOsc = ctx.createOscillator();
subOsc.type = 'sine';
subOsc.frequency.setValueAtTime(140, now + 1.4);
subOsc.frequency.exponentialRampToValueAtTime(32, now + 2.6);
const subEnv = ctx.createGain();
subEnv.gain.setValueAtTime(0.001, now + 1.4);
subEnv.gain.linearRampToValueAtTime(0.7, now + 1.7);
subEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration);
subOsc.connect(subEnv);
subEnv.connect(this.gainNode);
whineOsc.start(now);
whineOsc.stop(now + 2.7);
subOsc.start(now + 1.4);
subOsc.stop(now + duration);
}
}
window.AlertSynth = AlertSynth;
/**
* Procedural Doctor Who & TARDIS Sound Synthesizer
* 100% synthesized programmatically via Web Audio API.
* Includes Dematerialization Wheeze-Groan, Cloister Bell, Sonic Screwdriver, and TARDIS Console Foley.
*/
class WhoniverseAudioSynth {
constructor(audioManager) {
this.am = audioManager;
this.gainNode = null;
this.activeCloister = false;
this.cloisterTimer = null;
}
init() {
if (this.gainNode || !this.am.ctx) return;
const ctx = this.am.ctx;
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(0.7, ctx.currentTime);
this.gainNode.connect(this.am.compressor);
}
/**
* Procedural TARDIS Materialization / Dematerialization ("Wheeze-Groan")
* Modeled after Brian Hodgson's 1963 BBC Radiophonic technique:
* Dragging keys on piano bass strings -> reverse playback -> slow tape speed -> feedback loop.
*/
synthesizeDematCycle(cycles = 4) {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
for (let c = 0; c < cycles; c++) {
const cycleStart = ctx.currentTime + c * 1.85;
this.synthesizeSingleDematSwell(cycleStart, c, cycles);
}
}
synthesizeSingleDematSwell(startTime, cycleIndex, totalCycles) {
const ctx = this.am.ctx;
const duration = 1.75;
// Intensity fades slightly on later cycles
const intensity = 1.0 - (cycleIndex / totalCycles) * 0.35;
// 1. Friction Scrape Carrier (Sawtooth through resonant highpass/bandpass with frequency glide)
const frictionOsc = ctx.createOscillator();
frictionOsc.type = 'sawtooth';
// Frequency glides up then groans down
frictionOsc.frequency.setValueAtTime(120, startTime);
frictionOsc.frequency.exponentialRampToValueAtTime(840, startTime + 0.65);
frictionOsc.frequency.exponentialRampToValueAtTime(95, startTime + duration);
// Filter modeling the piano soundboard metallic scraping resonance
const frictionFilter = ctx.createBiquadFilter();
frictionFilter.type = 'bandpass';
frictionFilter.frequency.setValueAtTime(320, startTime);
frictionFilter.frequency.exponentialRampToValueAtTime(1450, startTime + 0.65);
frictionFilter.frequency.exponentialRampToValueAtTime(220, startTime + duration);
frictionFilter.Q.setValueAtTime(4.5, startTime);
const frictionGain = ctx.createGain();
frictionGain.gain.setValueAtTime(0.001, startTime);
frictionGain.gain.linearRampToValueAtTime(0.4 * intensity, startTime + 0.45);
frictionGain.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
// 2. Sub-Vortex Resonant Groan (FM synthesis for the deep cosmic groaning undertone)
const groanCarrier = ctx.createOscillator();
groanCarrier.type = 'triangle';
groanCarrier.frequency.setValueAtTime(55, startTime);
groanCarrier.frequency.linearRampToValueAtTime(138, startTime + 0.55);
groanCarrier.frequency.exponentialRampToValueAtTime(48, startTime + duration);
const groanMod = ctx.createOscillator();
groanMod.type = 'sine';
groanMod.frequency.setValueAtTime(28, startTime); // Phasing FM modulator
groanMod.frequency.linearRampToValueAtTime(65, startTime + 0.6);
const groanModGain = ctx.createGain();
groanModGain.gain.setValueAtTime(45, startTime);
groanMod.connect(groanModGain);
groanModGain.connect(groanCarrier.frequency);
const groanGain = ctx.createGain();
groanGain.gain.setValueAtTime(0.001, startTime);
groanGain.gain.linearRampToValueAtTime(0.6 * intensity, startTime + 0.5);
groanGain.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
// 3. Phasing Flutter / Swell (Tape-flange simulation via slow LFO)
const lfo = ctx.createOscillator();
lfo.type = 'sine';
lfo.frequency.setValueAtTime(5.5, startTime); // 5.5 Hz flanging flutter
const lfoDepth = ctx.createGain();
lfoDepth.gain.setValueAtTime(0.25, startTime);
lfo.connect(lfoDepth);
lfoDepth.connect(frictionGain.gain);
// Connect Graph
frictionOsc.connect(frictionFilter);
frictionFilter.connect(frictionGain);
frictionGain.connect(this.gainNode);
groanCarrier.connect(groanGain);
groanGain.connect(this.gainNode);
// Trigger Nodes
frictionOsc.start(startTime);
frictionOsc.stop(startTime + duration);
groanCarrier.start(startTime);
groanCarrier.stop(startTime + duration);
groanMod.start(startTime);
groanMod.stop(startTime + duration);
lfo.start(startTime);
lfo.stop(startTime + duration);
}
/**
* Procedural Cloister Bell (Deep, ominous bronze cathedral bell)
*/
triggerCloisterBell() {
this.init();
this.stopCloisterBell();
this.activeCloister = true;
const ringLoop = () => {
if (!this.activeCloister) return;
this.synthesizeCloisterStrike();
this.cloisterTimer = setTimeout(ringLoop, 3200); // Canonical cloister bell repetition rate
};
ringLoop();
}
stopCloisterBell() {
this.activeCloister = false;
if (this.cloisterTimer) {
clearTimeout(this.cloisterTimer);
this.cloisterTimer = null;
}
}
synthesizeCloisterStrike() {
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 4.2;
// Authentic bell inharmonic partial ratios: Fundamental, Minor 3rd, 5th, Octave, Major 7th
const bellPartials = [
{ freqRatio: 1.0, gain: 0.65, decay: 4.2 }, // Fundamental ~108 Hz
{ freqRatio: 1.19, gain: 0.45, decay: 3.6 }, // Minor third
{ freqRatio: 1.51, gain: 0.40, decay: 3.1 }, // Fifth
{ freqRatio: 2.01, gain: 0.30, decay: 2.4 }, // Octave
{ freqRatio: 2.74, gain: 0.22, decay: 1.8 }, // Upper strike tone
{ freqRatio: 3.42, gain: 0.15, decay: 1.2 } // High strike transient
];
const basePitch = 108.0; // Deep bronze bell pitch
bellPartials.forEach(p => {
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(basePitch * p.freqRatio, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(p.gain * 0.35, now + 0.012); // Sharp hammer impact
env.gain.exponentialRampToValueAtTime(0.0001, now + p.decay);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + p.decay + 0.05);
});
}
/**
* Procedural Sonic Screwdriver (High-frequency modulated sweep & vibrato warble)
*/
synthesizeSonicScrewdriver(durationSeconds = 1.2) {
this.init();
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = durationSeconds;
// Dual square/saw oscillators
const osc1 = ctx.createOscillator();
osc1.type = 'square';
osc1.frequency.setValueAtTime(2350, now);
osc1.frequency.linearRampToValueAtTime(2650, now + duration * 0.5);
osc1.frequency.linearRampToValueAtTime(2350, now + duration);
const osc2 = ctx.createOscillator();
osc2.type = 'sawtooth';
osc2.frequency.setValueAtTime(2362, now); // 12Hz natural phase beat
// Rapid Vibrato LFO
const vibrato = ctx.createOscillator();
vibrato.type = 'sine';
vibrato.frequency.setValueAtTime(32, now); // 32 Hz warble rate
const vibGain = ctx.createGain();
vibGain.gain.setValueAtTime(140, now);
vibrato.connect(vibGain);
vibGain.connect(osc1.frequency);
vibGain.connect(osc2.frequency);
// Bandpass filter for metallic resonance
const filter = ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(2500, now);
filter.Q.setValueAtTime(4.0, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.28, now + 0.03);
env.gain.setValueAtTime(0.28, now + duration * 0.85);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc1.connect(filter);
osc2.connect(filter);
filter.connect(env);
env.connect(this.gainNode);
osc1.start(now);
osc2.start(now);
vibrato.start(now);
osc1.stop(now + duration);
osc2.stop(now + duration);
vibrato.stop(now + duration);
}
/**
* Fast-Return Spring Lever (Heavy spring recoil clack + resonant ring)
*/
synthesizeFastReturn() {
this.init();
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.35;
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(620, now);
osc.frequency.exponentialRampToValueAtTime(95, now + 0.08);
const env = ctx.createGain();
env.gain.setValueAtTime(0.45, now);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* TARDIS Demat Switch / Relay Solenoid
*/
synthesizeDematSwitch() {
this.init();
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 0.06;
const osc = ctx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(850, now);
osc.frequency.exponentialRampToValueAtTime(140, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.35, now);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Telepathic Circuit Chime (Glassy, mystical resonance)
*/
synthesizeTelepathicChime() {
this.init();
const ctx = this.am.ctx;
if (!ctx || !this.gainNode) return;
const now = ctx.currentTime;
const duration = 1.4;
const notes = [1046.50, 1318.51, 1567.98, 2093.00]; // C Major arpeggio shimmer
notes.forEach((freq, idx) => {
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + idx * 0.08);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now + idx * 0.08);
env.gain.linearRampToValueAtTime(0.18, now + idx * 0.08 + 0.02);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now + idx * 0.08);
osc.stop(now + duration + 0.05);
});
}
}
window.WhoniverseAudioSynth = WhoniverseAudioSynth;
/**
* Procedural Audio Synthesizers for Expanded Sci-Fi Universes
* Generates Epstein drives, Bio-ship neural pulses, DRADIS sonar, Singularity drives,
* Retro analog tone glides, and Ludicrous speed reality shifts via Web Audio API.
*/
class ExpandedSciFiAudioSynth {
constructor(audioManager) {
this.am = audioManager;
this.gainNode = null;
}
init() {
if (this.gainNode || !this.am.ctx) return;
const ctx = this.am.ctx;
this.gainNode = ctx.createGain();
this.gainNode.gain.setValueAtTime(0.7, ctx.currentTime);
this.gainNode.connect(this.am.compressor);
}
/**
* Epstein Drive Fusion Torch Burn (The Expanse / Industrial Space)
* Tremendous raw fusion thrust with high-pressure magnetic plasma acceleration
*/
synthesizeEpsteinBurn() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 3.5;
// 1. High-frequency plasma induction whine
const whineOsc = ctx.createOscillator();
whineOsc.type = 'sawtooth';
whineOsc.frequency.setValueAtTime(140, now);
whineOsc.frequency.exponentialRampToValueAtTime(1800, now + 1.2);
whineOsc.frequency.exponentialRampToValueAtTime(3200, now + 2.5);
const whineFilter = ctx.createBiquadFilter();
whineFilter.type = 'bandpass';
whineFilter.frequency.setValueAtTime(280, now);
whineFilter.frequency.exponentialRampToValueAtTime(2600, now + 2.0);
whineFilter.Q.setValueAtTime(4.0, now);
const whineGain = ctx.createGain();
whineGain.gain.setValueAtTime(0.001, now);
whineGain.gain.linearRampToValueAtTime(0.35, now + 1.0);
whineGain.gain.exponentialRampToValueAtTime(0.001, now + duration);
whineOsc.connect(whineFilter);
whineFilter.connect(whineGain);
whineGain.connect(this.gainNode);
// 2. Colossal fusion blast roar (filtered noise)
const roarBuffer = this.am.createNoiseBuffer('brown', 4);
const roarSource = ctx.createBufferSource();
roarSource.buffer = roarBuffer;
const roarFilter = ctx.createBiquadFilter();
roarFilter.type = 'lowpass';
roarFilter.frequency.setValueAtTime(180, now);
roarFilter.frequency.linearRampToValueAtTime(550, now + 1.2);
roarFilter.frequency.exponentialRampToValueAtTime(120, now + duration);
const roarGain = ctx.createGain();
roarGain.gain.setValueAtTime(0.001, now);
roarGain.gain.linearRampToValueAtTime(0.7, now + 1.2);
roarGain.gain.exponentialRampToValueAtTime(0.001, now + duration);
roarSource.connect(roarFilter);
roarFilter.connect(roarGain);
roarGain.connect(this.gainNode);
whineOsc.start(now);
whineOsc.stop(now + duration);
roarSource.start(now);
roarSource.stop(now + duration);
}
/**
* Bio-Ship Starburst / Neural Pulse (Farscape Moya & Bioships)
* Organic vocalized dimensional fold and vascular wave
*/
synthesizeStarburst() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 2.8;
const osc1 = ctx.createOscillator();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(85, now);
osc1.frequency.exponentialRampToValueAtTime(940, now + 1.4);
osc1.frequency.exponentialRampToValueAtTime(45, now + duration);
const osc2 = ctx.createOscillator();
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(125, now);
osc2.frequency.exponentialRampToValueAtTime(1420, now + 1.4);
osc2.frequency.exponentialRampToValueAtTime(65, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.45, now + 1.3);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc1.connect(env);
osc2.connect(env);
env.connect(this.gainNode);
osc1.start(now);
osc2.start(now);
osc1.stop(now + duration);
osc2.stop(now + duration);
}
/**
* Battlestar Galactica DRADIS Sonar Ping (Military Space)
* The iconic tactical combat contact echo
*/
synthesizeDradisPing() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 1.4;
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(1860, now);
osc.frequency.exponentialRampToValueAtTime(1540, now + 0.08);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.35, now + 0.01);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* FTL Jump Thunderclap (BSG / Military Space)
* Sudden vacuum displacement shockwave
*/
synthesizeFtlJump() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 2.2;
const noiseBuffer = this.am.createNoiseBuffer('brown', 2.5);
const noise = ctx.createBufferSource();
noise.buffer = noiseBuffer;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, now);
filter.frequency.exponentialRampToValueAtTime(45, now + 1.8);
const env = ctx.createGain();
env.gain.setValueAtTime(0.8, now);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
noise.connect(filter);
filter.connect(env);
env.connect(this.gainNode);
noise.start(now);
noise.stop(now + duration);
}
/**
* Retro Astrogator Tone Glide (Jupiter 2 / Retro Future)
* 1960s Theremin / electronic oscillator glissando
*/
synthesizeRetroAstrogator() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 1.6;
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.linearRampToValueAtTime(1180, now + 0.6);
osc.frequency.linearRampToValueAtTime(320, now + 1.1);
osc.frequency.linearRampToValueAtTime(660, now + duration);
const vibrato = ctx.createOscillator();
vibrato.type = 'sine';
vibrato.frequency.setValueAtTime(8, now);
const vibGain = ctx.createGain();
vibGain.gain.setValueAtTime(25, now);
vibrato.connect(vibGain);
vibGain.connect(osc.frequency);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.3, now + 0.1);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
vibrato.start(now);
osc.stop(now + duration);
vibrato.stop(now + duration);
}
/**
* HAL 9000 Logic Confirmation Chime (Discovery One)
*/
synthesizeHalChime() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const f1 = 784; // G5
const f2 = 523; // C5
[0, 0.14].forEach((offset, idx) => {
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(idx === 0 ? f1 : f2, now + offset);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now + offset);
env.gain.linearRampToValueAtTime(0.25, now + offset + 0.01);
env.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.5);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now + offset);
osc.stop(now + offset + 0.52);
});
}
/**
* Event Horizon Gravity Singularity Pulse (Deep Space)
* Deep sub-bass dimensional warping thrum
*/
synthesizeSingularityEngage() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 3.2;
const sub = ctx.createOscillator();
sub.type = 'sine';
sub.frequency.setValueAtTime(95, now);
sub.frequency.exponentialRampToValueAtTime(28, now + 2.2);
const mod = ctx.createOscillator();
mod.type = 'triangle';
mod.frequency.setValueAtTime(14, now);
mod.frequency.linearRampToValueAtTime(45, now + 1.8);
const modGain = ctx.createGain();
modGain.gain.setValueAtTime(60, now);
mod.connect(modGain);
modGain.connect(sub.frequency);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.65, now + 1.5);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
sub.connect(env);
env.connect(this.gainNode);
sub.start(now);
mod.start(now);
sub.stop(now + duration);
mod.stop(now + duration);
}
/**
* Outlaw Afterburner Thruster Surge (Cowboy Bebop / Milano)
*/
synthesizeAfterburner() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 2.4;
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(80, now);
osc.frequency.exponentialRampToValueAtTime(850, now + 0.8);
osc.frequency.linearRampToValueAtTime(620, now + duration);
const filter = ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(350, now);
filter.frequency.exponentialRampToValueAtTime(1400, now + 0.8);
filter.Q.setValueAtTime(3.0, now);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.5, now + 0.6);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(filter);
filter.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Space Station Docking Clamp Latch & Airlock Purge
*/
synthesizeDockingClamp() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
// Heavy mechanical solenoid impact
const osc = ctx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(380, now);
osc.frequency.exponentialRampToValueAtTime(65, now + 0.12);
const env = ctx.createGain();
env.gain.setValueAtTime(0.5, now);
env.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + 0.25);
// Followed by pneumatic seal hiss
setTimeout(() => {
if (!this.am.ctx) return;
const t = this.am.ctx.currentTime;
const hissBuf = this.am.createNoiseBuffer('white', 1.2);
const hiss = this.am.ctx.createBufferSource();
hiss.buffer = hissBuf;
const hFilter = this.am.ctx.createBiquadFilter();
hFilter.type = 'bandpass';
hFilter.frequency.setValueAtTime(2200, t);
hFilter.Q.setValueAtTime(2.5, t);
const hEnv = this.am.ctx.createGain();
hEnv.gain.setValueAtTime(0.001, t);
hEnv.gain.linearRampToValueAtTime(0.25, t + 0.05);
hEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.9);
hiss.connect(hFilter);
hFilter.connect(hEnv);
hEnv.connect(this.gainNode);
hiss.start(t);
hiss.stop(t + 0.9);
}, 180);
}
/**
* Space Station Air Handler Thud (large HVAC unit cycling on)
* Distinct from the docking clamp latch above: a dull low-frequency thump
* (no bright metallic impact) followed by a slow-building airflow whoosh
* rather than a short pneumatic hiss. A clamp is a single hard mechanical
* event; an air handler is a big soft one that keeps breathing after it.
*/
synthesizeAirHandlerThud() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
// Dull low-frequency thump -- triangle, not square, and no bright impact
// transient, so it reads as a heavy fan housing rather than a latch.
const thump = ctx.createOscillator();
thump.type = 'triangle';
thump.frequency.setValueAtTime(95, now);
thump.frequency.exponentialRampToValueAtTime(38, now + 0.22);
const thumpEnv = ctx.createGain();
thumpEnv.gain.setValueAtTime(0.001, now);
thumpEnv.gain.linearRampToValueAtTime(0.42, now + 0.02);
thumpEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
// Sub-octave body for HVAC housing weight
const sub = ctx.createOscillator();
sub.type = 'sine';
sub.frequency.setValueAtTime(46, now);
const subEnv = ctx.createGain();
subEnv.gain.setValueAtTime(0.001, now);
subEnv.gain.linearRampToValueAtTime(0.25, now + 0.03);
subEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.45);
thump.connect(thumpEnv);
thumpEnv.connect(this.gainNode);
sub.connect(subEnv);
subEnv.connect(this.gainNode);
thump.start(now);
thump.stop(now + 0.4);
sub.start(now);
sub.stop(now + 0.45);
// Slow-building airflow whoosh -- lowpass rather than the clamp's
// bandpass, and a much slower attack, so it reads as a fan spinning up
// rather than a sharp seal-release hiss.
setTimeout(() => {
if (!this.am.ctx) return;
const t = this.am.ctx.currentTime;
const airBuf = this.am.createNoiseBuffer('pink', 1.8);
const air = this.am.ctx.createBufferSource();
air.buffer = airBuf;
const airFilter = this.am.ctx.createBiquadFilter();
airFilter.type = 'lowpass';
airFilter.frequency.setValueAtTime(420, t);
airFilter.Q.setValueAtTime(0.7, t);
const airEnv = this.am.ctx.createGain();
airEnv.gain.setValueAtTime(0.001, t);
airEnv.gain.linearRampToValueAtTime(0.22, t + 0.35);
airEnv.gain.exponentialRampToValueAtTime(0.001, t + 1.6);
air.connect(airFilter);
airFilter.connect(airEnv);
airEnv.connect(this.gainNode);
air.start(t);
air.stop(t + 1.6);
}, 140);
}
/**
* Ludicrous Speed Accelerator (Spaceball One / Comedy)
*/
synthesizeLudicrousSpeed() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 3.2;
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(60, now);
osc.frequency.exponentialRampToValueAtTime(5400, now + 2.2);
const env = ctx.createGain();
env.gain.setValueAtTime(0.01, now);
env.gain.linearRampToValueAtTime(0.4, now + 1.8);
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Infinite Improbability Reality-Warp Flip (Heart of Gold)
*/
synthesizeImprobabilityFlip() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 1.8;
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(1400, now);
osc.frequency.exponentialRampToValueAtTime(180, now + 0.7);
osc.frequency.exponentialRampToValueAtTime(2200, now + 1.3);
osc.frequency.exponentialRampToValueAtTime(440, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.35, now + 0.1);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
/**
* Geiger Counter Click Burst (Nostromo / Mining)
*/
synthesizeGeigerBurst() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const clicks = 8 + Math.floor(Math.random() * 8);
for (let i = 0; i < clicks; i++) {
const clickTime = now + (i * 0.04) + (Math.random() * 0.03);
const osc = ctx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(2800 + Math.random() * 800, clickTime);
const env = ctx.createGain();
env.gain.setValueAtTime(0.18, clickTime);
env.gain.exponentialRampToValueAtTime(0.001, clickTime + 0.015);
osc.connect(env);
env.connect(this.gainNode);
osc.start(clickTime);
osc.stop(clickTime + 0.016);
}
}
/**
* Cheerful Door Sigh (Heart of Gold / Sirius Cybernetics Corp)
*/
synthesizeCheerfulDoor() {
this.init();
const ctx = this.am.ctx;
if (!ctx) return;
const now = ctx.currentTime;
const duration = 0.9;
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(620, now);
osc.frequency.linearRampToValueAtTime(840, now + 0.35);
osc.frequency.linearRampToValueAtTime(520, now + duration);
const env = ctx.createGain();
env.gain.setValueAtTime(0.001, now);
env.gain.linearRampToValueAtTime(0.25, now + 0.15);
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
osc.connect(env);
env.connect(this.gainNode);
osc.start(now);
osc.stop(now + duration);
}
}
window.ExpandedSciFiAudioSynth = ExpandedSciFiAudioSynth;
/**
* Canonical Starship & Location Sound Profiles Matrix
* Each preset defines the exact parameters for Hull Drone, Warp Core, Life Support, and Telemetry.
*/