6352 lines
215 KiB
JavaScript
6352 lines
215 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(rampDuration = 0) {
|
||
this.stop();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx) return;
|
||
|
||
// Channel Gain Node
|
||
this.gainNode = ctx.createGain();
|
||
const targetVol = this.isMuted ? 0 : this.params.volume;
|
||
if (rampDuration > 0) {
|
||
this.gainNode.gain.setValueAtTime(0.0001, ctx.currentTime);
|
||
this.gainNode.gain.linearRampToValueAtTime(targetVol, ctx.currentTime + rampDuration);
|
||
} else {
|
||
this.gainNode.gain.setValueAtTime(targetVol, 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(fadeDuration = 0) {
|
||
if (fadeDuration > 0 && this.nodes.length > 0 && this.gainNode && this.am.ctx) {
|
||
const now = this.am.ctx.currentTime;
|
||
this.gainNode.gain.cancelScheduledValues(now);
|
||
this.gainNode.gain.setValueAtTime(Math.max(0.0001, this.gainNode.gain.value), now);
|
||
this.gainNode.gain.linearRampToValueAtTime(0.0001, now + fadeDuration);
|
||
if (this.stoppingTimeout) clearTimeout(this.stoppingTimeout);
|
||
this.stoppingTimeout = setTimeout(() => {
|
||
this.stop(0);
|
||
}, fadeDuration * 1000);
|
||
return;
|
||
}
|
||
if (this.stoppingTimeout) {
|
||
clearTimeout(this.stoppingTimeout);
|
||
this.stoppingTimeout = null;
|
||
}
|
||
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 = [];
|
||
this.subOsc1 = null;
|
||
this.subOsc2 = null;
|
||
this.noiseSource = null;
|
||
this.gainNode = 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);
|
||
}
|
||
}
|
||
|
||
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(rampDuration = 0) {
|
||
this.stop();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx) return;
|
||
|
||
this.gainNode = ctx.createGain();
|
||
const targetVol = this.isMuted ? 0 : this.params.volume;
|
||
if (rampDuration > 0) {
|
||
this.gainNode.gain.setValueAtTime(0.0001, ctx.currentTime);
|
||
this.gainNode.gain.linearRampToValueAtTime(targetVol, ctx.currentTime + rampDuration);
|
||
} else {
|
||
this.gainNode.gain.setValueAtTime(targetVol, 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');
|
||
if (rampDuration > 0) {
|
||
this.carrier.frequency.setValueAtTime(Math.max(20, this.params.carrierFreq * 0.45), ctx.currentTime);
|
||
this.carrier.frequency.exponentialRampToValueAtTime(this.params.carrierFreq, ctx.currentTime + rampDuration);
|
||
} else {
|
||
this.carrier.frequency.setValueAtTime(this.params.carrierFreq, ctx.currentTime);
|
||
}
|
||
|
||
// Sub-harmonic oscillator for massive bottom end
|
||
this.subOsc = ctx.createOscillator();
|
||
this.subOsc.type = 'sine';
|
||
if (rampDuration > 0) {
|
||
this.subOsc.frequency.setValueAtTime(Math.max(12, this.params.carrierFreq * 0.22), ctx.currentTime);
|
||
this.subOsc.frequency.exponentialRampToValueAtTime(this.params.carrierFreq * 0.5, ctx.currentTime + rampDuration);
|
||
} else {
|
||
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(fadeDuration = 0) {
|
||
if (fadeDuration > 0 && this.nodes.length > 0 && this.gainNode && this.am.ctx) {
|
||
const now = this.am.ctx.currentTime;
|
||
this.gainNode.gain.cancelScheduledValues(now);
|
||
this.gainNode.gain.setValueAtTime(Math.max(0.0001, this.gainNode.gain.value), now);
|
||
this.gainNode.gain.linearRampToValueAtTime(0.0001, now + fadeDuration);
|
||
if (this.carrier) {
|
||
try {
|
||
this.carrier.frequency.cancelScheduledValues(now);
|
||
this.carrier.frequency.setValueAtTime(this.carrier.frequency.value, now);
|
||
this.carrier.frequency.linearRampToValueAtTime(Math.max(20, this.params.carrierFreq * 0.35), now + fadeDuration);
|
||
} catch (e) {}
|
||
}
|
||
if (this.stoppingTimeout) clearTimeout(this.stoppingTimeout);
|
||
this.stoppingTimeout = setTimeout(() => {
|
||
this.stop(0);
|
||
}, fadeDuration * 1000);
|
||
return;
|
||
}
|
||
if (this.stoppingTimeout) {
|
||
clearTimeout(this.stoppingTimeout);
|
||
this.stoppingTimeout = null;
|
||
}
|
||
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 = [];
|
||
this.carrier = null;
|
||
this.subOsc = null;
|
||
this.gainNode = null;
|
||
}
|
||
}
|
||
|
||
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(rampDuration = 0) {
|
||
this.stop();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx) return;
|
||
|
||
this.gainNode = ctx.createGain();
|
||
const targetVol = this.isMuted ? 0 : this.params.volume;
|
||
if (rampDuration > 0) {
|
||
this.gainNode.gain.setValueAtTime(0.0001, ctx.currentTime);
|
||
this.gainNode.gain.linearRampToValueAtTime(targetVol, ctx.currentTime + rampDuration);
|
||
} else {
|
||
this.gainNode.gain.setValueAtTime(targetVol, 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(fadeDuration = 0) {
|
||
if (fadeDuration > 0 && this.nodes.length > 0 && this.gainNode && this.am.ctx) {
|
||
const now = this.am.ctx.currentTime;
|
||
this.gainNode.gain.cancelScheduledValues(now);
|
||
this.gainNode.gain.setValueAtTime(Math.max(0.0001, this.gainNode.gain.value), now);
|
||
this.gainNode.gain.linearRampToValueAtTime(0.0001, now + fadeDuration);
|
||
if (this.stoppingTimeout) clearTimeout(this.stoppingTimeout);
|
||
this.stoppingTimeout = setTimeout(() => {
|
||
this.stop(0);
|
||
}, fadeDuration * 1000);
|
||
return;
|
||
}
|
||
if (this.stoppingTimeout) {
|
||
clearTimeout(this.stoppingTimeout);
|
||
this.stoppingTimeout = null;
|
||
}
|
||
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 = [];
|
||
this.noiseSource = null;
|
||
this.gainNode = 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);
|
||
}
|
||
}
|
||
|
||
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', 'who', 'bioship', 'belter'
|
||
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 '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;
|
||
case 'ds9':
|
||
Math.random() > 0.5 ? this.synthesizeCardassianSensor() : this.synthesizeLCARSSingleChirp();
|
||
break;
|
||
case 'voyager': {
|
||
const r = Math.random();
|
||
if (r < 0.12) this.synthesizeCommBadge();
|
||
else if (r < 0.55) this.synthesizeLCARSDoubleChirp();
|
||
else this.synthesizeSensorSweep();
|
||
break;
|
||
}
|
||
case 'nx':
|
||
Math.random() > 0.5 ? this.synthesizeNXRelay() : this.synthesizeNXIndicatorBeep();
|
||
break;
|
||
case 'tng':
|
||
default: {
|
||
const r = Math.random();
|
||
if (r < 0.10) this.synthesizeCommBadge();
|
||
else if (r < 0.45) this.synthesizeLCARSSingleChirp();
|
||
else if (r < 0.72) this.synthesizeLCARSDoubleChirp();
|
||
else if (r < 0.88) 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()
|
||
}
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Star Trek Comm Badge Confirmation Chirp
|
||
*/
|
||
synthesizeCommBadge() {
|
||
if (window.expandedAudio) {
|
||
window.expandedAudio.synthesizeCommBadge();
|
||
return;
|
||
}
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
const now = ctx.currentTime;
|
||
[784, 1396.91].forEach((freq, idx) => {
|
||
const t = now + idx * 0.038;
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(freq, t);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(0.35, t + 0.005);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + (idx === 0 ? 0.045 : 0.09));
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(t);
|
||
osc.stop(t + (idx === 0 ? 0.05 : 0.095));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
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);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* TARDIS Lever-Throw Clunk (Tactile mechanical switch)
|
||
* Hard square transient click coupled to a dull highpass noise thump
|
||
*/
|
||
synthesizeLeverClunk() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Hard square transient click
|
||
const clickOsc = ctx.createOscillator();
|
||
clickOsc.type = 'square';
|
||
clickOsc.frequency.setValueAtTime(80, now);
|
||
clickOsc.frequency.exponentialRampToValueAtTime(30, now + 0.035);
|
||
|
||
const clickEnv = ctx.createGain();
|
||
clickEnv.gain.setValueAtTime(0.42, now);
|
||
clickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
|
||
|
||
clickOsc.connect(clickEnv);
|
||
clickEnv.connect(this.gainNode);
|
||
clickOsc.start(now);
|
||
clickOsc.stop(now + 0.042);
|
||
|
||
// Dull highpass noise thump
|
||
const noiseBuf = this.am.createNoiseBuffer('pink', 0.15);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const filter = ctx.createBiquadFilter();
|
||
filter.type = 'highpass';
|
||
filter.frequency.setValueAtTime(320, now);
|
||
|
||
const noiseEnv = ctx.createGain();
|
||
noiseEnv.gain.setValueAtTime(0.3, now);
|
||
noiseEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.06);
|
||
|
||
noise.connect(filter);
|
||
filter.connect(noiseEnv);
|
||
noiseEnv.connect(this.gainNode);
|
||
noise.start(now);
|
||
noise.stop(now + 0.065);
|
||
}
|
||
|
||
/**
|
||
* TARDIS Police Box Exterior Door Open / Close (Wood creak + mortise latch)
|
||
* Modulated bandpass friction noise layered over a sharp metallic dual-click transient
|
||
*/
|
||
synthesizeTardisDoor() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.75;
|
||
|
||
// 1. Modulated bandpass friction noise (wood creak)
|
||
const creakBuf = this.am.createNoiseBuffer('brown', duration);
|
||
const creak = ctx.createBufferSource();
|
||
creak.buffer = creakBuf;
|
||
|
||
const creakFilter = ctx.createBiquadFilter();
|
||
creakFilter.type = 'bandpass';
|
||
creakFilter.frequency.setValueAtTime(260, now);
|
||
creakFilter.frequency.linearRampToValueAtTime(540, now + 0.35);
|
||
creakFilter.frequency.exponentialRampToValueAtTime(310, now + 0.65);
|
||
creakFilter.Q.setValueAtTime(6.0, now);
|
||
|
||
const creakEnv = ctx.createGain();
|
||
creakEnv.gain.setValueAtTime(0.001, now);
|
||
creakEnv.gain.linearRampToValueAtTime(0.38, now + 0.15);
|
||
creakEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.68);
|
||
|
||
creak.connect(creakFilter);
|
||
creakFilter.connect(creakEnv);
|
||
creakEnv.connect(this.gainNode);
|
||
creak.start(now);
|
||
creak.stop(now + 0.7);
|
||
|
||
// 2. Iron mortise latch dual-click transient
|
||
[0.54, 0.62].forEach((offset, idx) => {
|
||
const click = ctx.createOscillator();
|
||
click.type = 'triangle';
|
||
click.frequency.setValueAtTime(idx === 0 ? 1100 : 750, now + offset);
|
||
click.frequency.exponentialRampToValueAtTime(180, now + offset + 0.03);
|
||
|
||
const cEnv = ctx.createGain();
|
||
cEnv.gain.setValueAtTime(0.28, now + offset);
|
||
cEnv.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.035);
|
||
|
||
click.connect(cEnv);
|
||
cEnv.connect(this.gainNode);
|
||
click.start(now + offset);
|
||
click.stop(now + offset + 0.04);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Scanner / Monitor Screen Activation (Whoniverse, audio_gaps.md #24)
|
||
* Rapid upward sine sweep (200 Hz -> 1600 Hz over 120 ms) terminating in a
|
||
* CRT flyback whine. The 15.6 kHz spec tail is compromised to ~10.6 kHz:
|
||
* near/above Nyquist on low-rate contexts and ear-fatiguing at audible levels.
|
||
*/
|
||
synthesizeScannerActivate() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// 1. Cathode-ray startup pitch sweep
|
||
const sweep = ctx.createOscillator();
|
||
sweep.type = 'sine';
|
||
sweep.frequency.setValueAtTime(200, now);
|
||
sweep.frequency.exponentialRampToValueAtTime(1600, now + 0.12);
|
||
|
||
const sweepEnv = ctx.createGain();
|
||
sweepEnv.gain.setValueAtTime(0.001, now);
|
||
sweepEnv.gain.linearRampToValueAtTime(0.32, now + 0.03);
|
||
sweepEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.16);
|
||
|
||
sweep.connect(sweepEnv);
|
||
sweepEnv.connect(this.gainNode);
|
||
sweep.start(now);
|
||
sweep.stop(now + 0.17);
|
||
|
||
// 2. CRT flyback whine partial (10.6 kHz compromise, low amplitude)
|
||
const whine = ctx.createOscillator();
|
||
whine.type = 'sine';
|
||
whine.frequency.setValueAtTime(10600, now + 0.1);
|
||
|
||
const wobble = ctx.createOscillator();
|
||
wobble.type = 'sine';
|
||
wobble.frequency.setValueAtTime(1.0, now + 0.1);
|
||
const wobbleDepth = ctx.createGain();
|
||
wobbleDepth.gain.setValueAtTime(8, now + 0.1);
|
||
|
||
wobble.connect(wobbleDepth);
|
||
wobbleDepth.connect(whine.frequency);
|
||
|
||
const whineEnv = ctx.createGain();
|
||
whineEnv.gain.setValueAtTime(0.001, now + 0.1);
|
||
whineEnv.gain.linearRampToValueAtTime(0.03, now + 0.16);
|
||
whineEnv.gain.exponentialRampToValueAtTime(0.0001, now + 0.85);
|
||
|
||
whine.connect(whineEnv);
|
||
whineEnv.connect(this.gainNode);
|
||
whine.start(now + 0.1);
|
||
whine.stop(now + 0.87);
|
||
wobble.start(now + 0.1);
|
||
wobble.stop(now + 0.87);
|
||
}
|
||
|
||
/**
|
||
* Console "Type" Input Clatter Cluster (Whoniverse, audio_gaps.md #25)
|
||
* Rhythmic mechanical clatter of toggle switches, spring-loaded buttons and
|
||
* tumbler relays: clustered wooden/plastic clicks and solenoid snaps rather
|
||
* than tonal beeps. Produces a single 0.3-0.6 s cluster of 3-7 transients.
|
||
*/
|
||
synthesizeTypeClatter() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const transientCount = 3 + Math.floor(Math.random() * 5); // 3-7 strikes
|
||
let cursor = 0;
|
||
|
||
const strike = (kind, offset, peak) => {
|
||
const t = now + offset;
|
||
if (kind === 'toggle') {
|
||
// Wooden relay toggle: short bandpass pink-noise burst with downward snap
|
||
const buf = this.am.createNoiseBuffer('pink', 0.1);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = buf;
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(700 + Math.random() * 700, t);
|
||
bp.frequency.exponentialRampToValueAtTime(240, t + 0.03);
|
||
bp.Q.setValueAtTime(8 + Math.random() * 4, t);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(peak, t + 0.005);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.045);
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(t);
|
||
noise.stop(t + 0.05);
|
||
} else if (kind === 'button') {
|
||
// Spring-loaded plastic button: fast square click collapsing in pitch
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(900, t);
|
||
osc.frequency.exponentialRampToValueAtTime(200, t + 0.012);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(peak * 0.8, t);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.022);
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(t);
|
||
osc.stop(t + 0.025);
|
||
} else {
|
||
// Heavy solenoid snap: dull low square thump
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(120, t);
|
||
osc.frequency.exponentialRampToValueAtTime(55, t + 0.025);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(peak, t);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.035);
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(t);
|
||
osc.stop(t + 0.04);
|
||
}
|
||
};
|
||
|
||
for (let i = 0; i < transientCount; i++) {
|
||
const kinds = ['toggle', 'toggle', 'button', 'button', 'solenoid'];
|
||
const kind = kinds[Math.floor(Math.random() * kinds.length)];
|
||
cursor += 0.015 + Math.random() * 0.075;
|
||
strike(kind, cursor, 0.07 + Math.random() * 0.07);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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() {
|
||
}
|
||
}
|
||
|
||
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;
|
||
this.medicalTimer = null;
|
||
this.sparkTimer = null;
|
||
this.loopTimers = {};
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* Star Trek Comm Badge Confirmation Chirp
|
||
* Iconic bright two-tone pulse in rapid sequence (784 Hz to 1397 Hz)
|
||
*/
|
||
synthesizeCommBadge() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const tone1Freq = 784; // G5
|
||
const tone2Freq = 1396.91; // F6
|
||
|
||
// Pulse 1
|
||
const osc1 = ctx.createOscillator();
|
||
osc1.type = 'sine';
|
||
osc1.frequency.setValueAtTime(tone1Freq, now);
|
||
|
||
const env1 = ctx.createGain();
|
||
env1.gain.setValueAtTime(0.001, now);
|
||
env1.gain.linearRampToValueAtTime(0.38, now + 0.005);
|
||
env1.gain.exponentialRampToValueAtTime(0.001, now + 0.045);
|
||
|
||
osc1.connect(env1);
|
||
env1.connect(this.gainNode);
|
||
osc1.start(now);
|
||
osc1.stop(now + 0.048);
|
||
|
||
// Pulse 2
|
||
const t2 = now + 0.038;
|
||
const osc2 = ctx.createOscillator();
|
||
osc2.type = 'sine';
|
||
osc2.frequency.setValueAtTime(tone2Freq, t2);
|
||
|
||
const env2 = ctx.createGain();
|
||
env2.gain.setValueAtTime(0.001, t2);
|
||
env2.gain.linearRampToValueAtTime(0.42, t2 + 0.006);
|
||
env2.gain.exponentialRampToValueAtTime(0.001, t2 + 0.095);
|
||
|
||
osc2.connect(env2);
|
||
env2.connect(this.gainNode);
|
||
osc2.start(t2);
|
||
osc2.stop(t2 + 0.1);
|
||
}
|
||
|
||
/**
|
||
* Starfleet Pneumatic Door Swish (Four Era Variants)
|
||
* Bandpass-filtered white noise burst shaped per era
|
||
*/
|
||
synthesizeDoorSwish(era = 'tng') {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
let centerFreq = 950;
|
||
let qVal = 1.8;
|
||
let duration = 0.45;
|
||
let noiseType = 'white';
|
||
|
||
switch (era) {
|
||
case 'tos':
|
||
centerFreq = 720;
|
||
qVal = 1.4;
|
||
duration = 0.38;
|
||
break;
|
||
case 'voyager':
|
||
centerFreq = 1250;
|
||
qVal = 2.4;
|
||
duration = 0.40;
|
||
break;
|
||
case 'nx':
|
||
centerFreq = 650;
|
||
qVal = 2.0;
|
||
duration = 0.55;
|
||
noiseType = 'pink';
|
||
break;
|
||
case 'tng':
|
||
default:
|
||
centerFreq = 950;
|
||
qVal = 1.8;
|
||
duration = 0.45;
|
||
break;
|
||
}
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer(noiseType, duration + 0.1);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(centerFreq * 0.8, now);
|
||
bp.frequency.linearRampToValueAtTime(centerFreq * 1.15, now + duration * 0.4);
|
||
bp.frequency.exponentialRampToValueAtTime(centerFreq * 0.7, now + duration);
|
||
bp.Q.setValueAtTime(qVal, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.45, now + 0.03);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.02);
|
||
}
|
||
|
||
/**
|
||
* Starfleet Bosun's Pipe Whistle (TOS Command Announce)
|
||
* Two-tone sine glide (1800 Hz -> 2400 Hz -> 1850 Hz) with gentle tremolo
|
||
*/
|
||
synthesizeBosunWhistle() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.85;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(1800, now);
|
||
osc.frequency.linearRampToValueAtTime(2400, now + 0.22);
|
||
osc.frequency.setValueAtTime(2400, now + 0.48);
|
||
osc.frequency.linearRampToValueAtTime(1850, now + 0.75);
|
||
|
||
const tremolo = ctx.createOscillator();
|
||
tremolo.type = 'sine';
|
||
tremolo.frequency.setValueAtTime(6.5, now);
|
||
|
||
const tremGain = ctx.createGain();
|
||
tremGain.gain.setValueAtTime(0.12, now);
|
||
tremolo.connect(tremGain);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.28, now + 0.05);
|
||
env.gain.setValueAtTime(0.28, now + 0.65);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
tremGain.connect(env.gain);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
tremolo.start(now);
|
||
osc.stop(now + duration);
|
||
tremolo.stop(now + duration);
|
||
}
|
||
|
||
/**
|
||
* Starfleet Sickbay Medical Monitor ECG Ping
|
||
* Calm, periodic rhythmic vital signs pulse (1080 Hz sine, 40 ms decay)
|
||
*/
|
||
synthesizeMedicalMonitor() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.045;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(1080, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.32, now + 0.003);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.005);
|
||
}
|
||
|
||
/**
|
||
* Sevastopol Electrical Conduit Spark Transient
|
||
* High-voltage electrical discharge arcs across damaged station bulkheads
|
||
*/
|
||
synthesizeSevastopolSpark() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const bursts = 2 + Math.floor(Math.random() * 2);
|
||
|
||
for (let i = 0; i < bursts; i++) {
|
||
const offset = i * (0.015 + Math.random() * 0.02);
|
||
const t = now + offset;
|
||
const dur = 0.008 + Math.random() * 0.008;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('white', 0.05);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const hp = ctx.createBiquadFilter();
|
||
hp.type = 'highpass';
|
||
hp.frequency.setValueAtTime(3600 + Math.random() * 800, t);
|
||
hp.Q.setValueAtTime(4.5, t);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.45, t);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||
|
||
noise.connect(hp);
|
||
hp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(t);
|
||
noise.stop(t + dur + 0.005);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Moonbase Alpha Commlock Calling Tone (Space: 1999)
|
||
* Dual square wave pulse sequence (1100 Hz / 1500 Hz)
|
||
*/
|
||
synthesizeCommlockTone() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Beep 1: 1100 Hz, 60 ms
|
||
const osc1 = ctx.createOscillator();
|
||
osc1.type = 'square';
|
||
osc1.frequency.setValueAtTime(1100, now);
|
||
|
||
const env1 = ctx.createGain();
|
||
env1.gain.setValueAtTime(0.001, now);
|
||
env1.gain.linearRampToValueAtTime(0.24, now + 0.004);
|
||
env1.gain.setValueAtTime(0.24, now + 0.055);
|
||
env1.gain.exponentialRampToValueAtTime(0.001, now + 0.062);
|
||
|
||
osc1.connect(env1);
|
||
env1.connect(this.gainNode);
|
||
osc1.start(now);
|
||
osc1.stop(now + 0.065);
|
||
|
||
// Beep 2: 1500 Hz, 75 ms starting at +0.082s
|
||
const t2 = now + 0.082;
|
||
const osc2 = ctx.createOscillator();
|
||
osc2.type = 'square';
|
||
osc2.frequency.setValueAtTime(1500, t2);
|
||
|
||
const env2 = ctx.createGain();
|
||
env2.gain.setValueAtTime(0.001, t2);
|
||
env2.gain.linearRampToValueAtTime(0.24, t2 + 0.004);
|
||
env2.gain.setValueAtTime(0.24, t2 + 0.07);
|
||
env2.gain.exponentialRampToValueAtTime(0.001, t2 + 0.078);
|
||
|
||
osc2.connect(env2);
|
||
env2.connect(this.gainNode);
|
||
osc2.start(t2);
|
||
osc2.stop(t2 + 0.082);
|
||
}
|
||
|
||
/**
|
||
* Gateway Station Medical Vital Telemetry Pip (Aliens)
|
||
* Clinical sterile 950 Hz pure sine pip (35 ms decay)
|
||
*/
|
||
synthesizeStationMedicalPing() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.038;
|
||
|
||
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.3, now + 0.003);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.005);
|
||
}
|
||
|
||
/**
|
||
* Solar Radiation / Heat Shield Roar (Icarus II / Sunshine)
|
||
* Terrifying lowpass brown noise roar with amplitude swell and crackle impulses
|
||
*/
|
||
synthesizeSolarRoar() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 3.6;
|
||
|
||
// Lowpass brown noise body
|
||
const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.2);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(120, now);
|
||
lp.frequency.linearRampToValueAtTime(180, now + 1.5);
|
||
lp.frequency.exponentialRampToValueAtTime(90, now + duration);
|
||
lp.Q.setValueAtTime(2.2, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.72, now + 1.4);
|
||
env.gain.setValueAtTime(0.70, now + 2.2);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
noise.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.05);
|
||
|
||
// Highpass solar crackle layer
|
||
const crackleBuf = this.am.createNoiseBuffer('pink', duration);
|
||
const crackle = ctx.createBufferSource();
|
||
crackle.buffer = crackleBuf;
|
||
|
||
const hp = ctx.createBiquadFilter();
|
||
hp.type = 'highpass';
|
||
hp.frequency.setValueAtTime(3200, now);
|
||
hp.Q.setValueAtTime(4.0, now);
|
||
|
||
const crackleEnv = ctx.createGain();
|
||
crackleEnv.gain.setValueAtTime(0.001, now);
|
||
crackleEnv.gain.linearRampToValueAtTime(0.08, now + 1.2);
|
||
crackleEnv.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
crackle.connect(hp);
|
||
hp.connect(crackleEnv);
|
||
crackleEnv.connect(this.gainNode);
|
||
|
||
crackle.start(now);
|
||
crackle.stop(now + duration);
|
||
}
|
||
|
||
/**
|
||
* Cryogenic Pod Depressurization Sigh (Avalon / Ark One)
|
||
* Gas pressure relief with exponential filter decay followed by 205 Hz seal hum
|
||
*/
|
||
synthesizeCryoDepressurize() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 1.35;
|
||
|
||
// Pneumatic gas depressurization sigh
|
||
const noiseBuf = this.am.createNoiseBuffer('white', 1.4);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(1800, now);
|
||
lp.frequency.exponentialRampToValueAtTime(280, now + 1.1);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.38, now + 0.06);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + 1.15);
|
||
|
||
noise.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(now);
|
||
noise.stop(now + 1.2);
|
||
|
||
// Quiet motorized seal hum
|
||
const hum = ctx.createOscillator();
|
||
hum.type = 'sine';
|
||
hum.frequency.setValueAtTime(205, now + 0.35);
|
||
|
||
const humEnv = ctx.createGain();
|
||
humEnv.gain.setValueAtTime(0.001, now + 0.35);
|
||
humEnv.gain.linearRampToValueAtTime(0.18, now + 0.55);
|
||
humEnv.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
hum.connect(humEnv);
|
||
humEnv.connect(this.gainNode);
|
||
|
||
hum.start(now + 0.35);
|
||
hum.stop(now + duration + 0.02);
|
||
}
|
||
|
||
/**
|
||
* Mid-Century Orion Nuclear Pulse Thump (USS Ascension)
|
||
* Heavy 45 Hz lowpass square impulse with sub-bass body resonance
|
||
*/
|
||
synthesizeOrionPulseThump() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.22;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(45, now);
|
||
osc.frequency.exponentialRampToValueAtTime(24, now + 0.09);
|
||
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(140, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.65, now);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + 0.085);
|
||
|
||
osc.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
osc.stop(now + 0.09);
|
||
|
||
const sub = ctx.createOscillator();
|
||
sub.type = 'sine';
|
||
sub.frequency.setValueAtTime(36, now);
|
||
|
||
const subEnv = ctx.createGain();
|
||
subEnv.gain.setValueAtTime(0.001, now);
|
||
subEnv.gain.linearRampToValueAtTime(0.55, now + 0.01);
|
||
subEnv.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
|
||
sub.connect(subEnv);
|
||
subEnv.connect(this.gainNode);
|
||
|
||
sub.start(now);
|
||
sub.stop(now + duration);
|
||
}
|
||
|
||
/**
|
||
* Nutri-Matic Dedicated Tea Dispenser Gurgle (Heart of Gold)
|
||
* Bandpass bubbling noise hops (500–1000 Hz) terminating in a short steam hiss
|
||
*/
|
||
synthesizeTeaDispenser() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Bubbling hops
|
||
const bubbleFreqs = [520, 840, 610, 960, 480, 730];
|
||
bubbleFreqs.forEach((freq, i) => {
|
||
const t = now + i * 0.11;
|
||
const bDur = 0.09;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('pink', 0.12);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(freq, t);
|
||
bp.Q.setValueAtTime(6.0, t);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(0.35, t + 0.015);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + bDur);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(t);
|
||
noise.stop(t + bDur + 0.01);
|
||
});
|
||
|
||
// Steam hiss
|
||
const steamTime = now + 0.65;
|
||
const steamBuf = this.am.createNoiseBuffer('white', 0.45);
|
||
const steam = ctx.createBufferSource();
|
||
steam.buffer = steamBuf;
|
||
|
||
const steamFilter = ctx.createBiquadFilter();
|
||
steamFilter.type = 'bandpass';
|
||
steamFilter.frequency.setValueAtTime(2400, steamTime);
|
||
steamFilter.Q.setValueAtTime(3.0, steamTime);
|
||
|
||
const steamEnv = ctx.createGain();
|
||
steamEnv.gain.setValueAtTime(0.001, steamTime);
|
||
steamEnv.gain.linearRampToValueAtTime(0.28, steamTime + 0.04);
|
||
steamEnv.gain.exponentialRampToValueAtTime(0.001, steamTime + 0.42);
|
||
|
||
steam.connect(steamFilter);
|
||
steamFilter.connect(steamEnv);
|
||
steamEnv.connect(this.gainNode);
|
||
|
||
steam.start(steamTime);
|
||
steam.stop(steamTime + 0.45);
|
||
}
|
||
|
||
/**
|
||
* British Electric Kettle Steam Whistle (HMS Camden Lock / Hyperdrive)
|
||
* Narrow bandpass sine sweeping 1820 Hz to 2380 Hz with steady tremolo
|
||
*/
|
||
synthesizeKettleWhistle() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 1.6;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(1820, now);
|
||
osc.frequency.exponentialRampToValueAtTime(2380, now + duration * 0.7);
|
||
osc.frequency.linearRampToValueAtTime(2320, now + duration);
|
||
|
||
const tremolo = ctx.createOscillator();
|
||
tremolo.type = 'sine';
|
||
tremolo.frequency.setValueAtTime(5.2, now);
|
||
|
||
const tremGain = ctx.createGain();
|
||
tremGain.gain.setValueAtTime(0.15, now);
|
||
tremolo.connect(tremGain);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.26, now + 0.25);
|
||
env.gain.setValueAtTime(0.26, now + 1.2);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
tremGain.connect(env.gain);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
tremolo.start(now);
|
||
osc.stop(now + duration);
|
||
tremolo.stop(now + duration);
|
||
}
|
||
|
||
/**
|
||
* Ludicrous Speed Plaid Alarm (Spaceball One)
|
||
* Two-tone alternating square wave siren (440 Hz / 880 Hz)
|
||
*/
|
||
synthesizePlaidAlarm() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const steps = 6;
|
||
const stepDur = 0.12;
|
||
|
||
for (let i = 0; i < steps; i++) {
|
||
const t = now + i * stepDur;
|
||
const freq = (i % 2 === 0) ? 440 : 880;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(freq, t);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(0.28, t + 0.006);
|
||
env.gain.setValueAtTime(0.28, t + stepDur - 0.01);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + stepDur);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(t);
|
||
osc.stop(t + stepDur + 0.005);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Dedicated Cassette Transport Clunk & Whirr (Milano / Bebop)
|
||
* Dual square transient click (120 Hz / 360 Hz) followed by 2200 Hz capstan tone with flutter
|
||
*/
|
||
synthesizeCassetteTransport() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Dual square mechanical button clunk
|
||
[120, 360].forEach((freq) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(freq, now);
|
||
osc.frequency.exponentialRampToValueAtTime(40, now + 0.028);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.35, now);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + 0.03);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
osc.stop(now + 0.032);
|
||
});
|
||
|
||
// 12V capstan motor tone + tape flutter
|
||
const motorTime = now + 0.04;
|
||
const motorDur = 0.95;
|
||
|
||
const motor = ctx.createOscillator();
|
||
motor.type = 'sine';
|
||
motor.frequency.setValueAtTime(2200, motorTime);
|
||
|
||
const flutter = ctx.createOscillator();
|
||
flutter.type = 'sine';
|
||
flutter.frequency.setValueAtTime(8.2, motorTime);
|
||
|
||
const flutGain = ctx.createGain();
|
||
flutGain.gain.setValueAtTime(35, motorTime);
|
||
flutter.connect(flutGain);
|
||
flutGain.connect(motor.frequency);
|
||
|
||
const motorEnv = ctx.createGain();
|
||
motorEnv.gain.setValueAtTime(0.001, motorTime);
|
||
motorEnv.gain.linearRampToValueAtTime(0.12, motorTime + 0.08);
|
||
motorEnv.gain.exponentialRampToValueAtTime(0.001, motorTime + motorDur);
|
||
|
||
motor.connect(motorEnv);
|
||
motorEnv.connect(this.gainNode);
|
||
|
||
motor.start(motorTime);
|
||
flutter.start(motorTime);
|
||
motor.stop(motorTime + motorDur);
|
||
flutter.stop(motorTime + motorDur);
|
||
|
||
// Tape noise bed
|
||
const tapeBuf = this.am.createNoiseBuffer('pink', motorDur);
|
||
const tape = ctx.createBufferSource();
|
||
tape.buffer = tapeBuf;
|
||
|
||
const tapeFilter = ctx.createBiquadFilter();
|
||
tapeFilter.type = 'bandpass';
|
||
tapeFilter.frequency.setValueAtTime(3200, motorTime);
|
||
tapeFilter.Q.setValueAtTime(2.0, motorTime);
|
||
|
||
const tapeEnv = ctx.createGain();
|
||
tapeEnv.gain.setValueAtTime(0.001, motorTime);
|
||
tapeEnv.gain.linearRampToValueAtTime(0.08, motorTime + 0.05);
|
||
tapeEnv.gain.exponentialRampToValueAtTime(0.001, motorTime + motorDur);
|
||
|
||
tape.connect(tapeFilter);
|
||
tapeFilter.connect(tapeEnv);
|
||
tapeEnv.connect(this.gainNode);
|
||
|
||
tape.start(motorTime);
|
||
tape.stop(motorTime + motorDur);
|
||
}
|
||
|
||
/**
|
||
* Grappler Arm Servo Motor Whine (Outlaw Star / Bebop)
|
||
* Swept triangle wave (220 Hz -> 680 Hz) through resonant bandpass filter
|
||
*/
|
||
synthesizeGrapplerServo() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.44;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'triangle';
|
||
osc.frequency.setValueAtTime(220, now);
|
||
osc.frequency.exponentialRampToValueAtTime(680, now + duration * 0.7);
|
||
osc.frequency.linearRampToValueAtTime(540, now + duration);
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(320, now);
|
||
bp.frequency.exponentialRampToValueAtTime(820, now + duration * 0.7);
|
||
bp.Q.setValueAtTime(3.2, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.38, now + 0.04);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
osc.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.01);
|
||
}
|
||
|
||
/**
|
||
* True RF Radio Static Burst (The Betty / The Marauder)
|
||
* Bandpass-filtered pink/white noise burst (300–3200 Hz) with hard-knee square gate
|
||
*/
|
||
synthesizeRadioStatic() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 0.18;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('pink', 0.25);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(1400, now);
|
||
bp.Q.setValueAtTime(1.1, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.42, now);
|
||
env.gain.setValueAtTime(0.40, now + duration - 0.02);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.01);
|
||
}
|
||
|
||
/**
|
||
* Magnetic Boot ("Mag-Boot") Latch (Ceres / Tycho / The Expanse)
|
||
* Low-frequency impact thump (80 Hz click) followed by 120 Hz inductive clamp buzz
|
||
*/
|
||
synthesizeMagBootLatch() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Stage 1: Impact thump
|
||
const thump = ctx.createOscillator();
|
||
thump.type = 'triangle';
|
||
thump.frequency.setValueAtTime(80, now);
|
||
thump.frequency.exponentialRampToValueAtTime(32, now + 0.03);
|
||
|
||
const thumpEnv = ctx.createGain();
|
||
thumpEnv.gain.setValueAtTime(0.5, now);
|
||
thumpEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.035);
|
||
|
||
thump.connect(thumpEnv);
|
||
thumpEnv.connect(this.gainNode);
|
||
thump.start(now);
|
||
thump.stop(now + 0.038);
|
||
|
||
// Stage 2: 120 Hz inductive clamping buzz
|
||
const buzzTime = now + 0.025;
|
||
const buzz = ctx.createOscillator();
|
||
buzz.type = 'square';
|
||
buzz.frequency.setValueAtTime(120, buzzTime);
|
||
|
||
const buzzFilter = ctx.createBiquadFilter();
|
||
buzzFilter.type = 'lowpass';
|
||
buzzFilter.frequency.setValueAtTime(420, buzzTime);
|
||
|
||
const buzzEnv = ctx.createGain();
|
||
buzzEnv.gain.setValueAtTime(0.001, buzzTime);
|
||
buzzEnv.gain.linearRampToValueAtTime(0.32, buzzTime + 0.01);
|
||
buzzEnv.gain.exponentialRampToValueAtTime(0.001, buzzTime + 0.16);
|
||
|
||
buzz.connect(buzzFilter);
|
||
buzzFilter.connect(buzzEnv);
|
||
buzzEnv.connect(this.gainNode);
|
||
|
||
buzz.start(buzzTime);
|
||
buzz.stop(buzzTime + 0.17);
|
||
}
|
||
|
||
/**
|
||
* Background Loop: Periodic Medical Vital Signs Monitor (ECG)
|
||
*/
|
||
startMedicalMonitor(intervalSeconds = 1.1) {
|
||
this.stopMedicalMonitor();
|
||
const tick = () => {
|
||
this.synthesizeMedicalMonitor();
|
||
this.medicalTimer = setTimeout(tick, intervalSeconds * 1000);
|
||
};
|
||
tick();
|
||
}
|
||
|
||
stopMedicalMonitor() {
|
||
if (this.medicalTimer) {
|
||
clearTimeout(this.medicalTimer);
|
||
this.medicalTimer = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Background Loop: Random Electrical Sparks on Damaged Stations
|
||
*/
|
||
startStationSparks(minIntervalMs = 4000, maxIntervalMs = 12000) {
|
||
this.stopStationSparks();
|
||
const scheduleNext = () => {
|
||
const delay = minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs);
|
||
this.sparkTimer = setTimeout(() => {
|
||
this.synthesizeSevastopolSpark();
|
||
scheduleNext();
|
||
}, delay);
|
||
};
|
||
scheduleNext();
|
||
}
|
||
|
||
stopStationSparks() {
|
||
if (this.sparkTimer) {
|
||
clearTimeout(this.sparkTimer);
|
||
this.sparkTimer = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop Registry (Phase 2)
|
||
* Generic recursive-timeout scheduler keyed by name. A start call always
|
||
* kills any previous instance of the same key first. If min === max the
|
||
* cadence is fixed; otherwise it is random within [min, max].
|
||
*/
|
||
_scheduleLoop(key, minIntervalMs, maxIntervalMs, fn) {
|
||
this._stopLoop(key);
|
||
const scheduleNext = () => {
|
||
const delay = maxIntervalMs === minIntervalMs
|
||
? minIntervalMs
|
||
: minIntervalMs + Math.random() * (maxIntervalMs - minIntervalMs);
|
||
this.loopTimers[key] = setTimeout(() => {
|
||
fn();
|
||
scheduleNext();
|
||
}, delay);
|
||
};
|
||
scheduleNext();
|
||
}
|
||
|
||
_stopLoop(key) {
|
||
if (this.loopTimers[key]) {
|
||
clearTimeout(this.loopTimers[key]);
|
||
delete this.loopTimers[key];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Cardassian Bulkhead Door Grind (Star Trek DS9, audio_gaps.md #21)
|
||
* Oppressive pneumatic bulkhead grinding open: resonant bandpass sweep
|
||
* across brown noise (120 Hz -> 650 Hz) layered with a scraping metallic
|
||
* saw undertone.
|
||
*/
|
||
synthesizeCardassianDoor() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 1.4;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.2);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(120, now);
|
||
bp.frequency.exponentialRampToValueAtTime(650, now + 1.1);
|
||
bp.Q.setValueAtTime(3.5, now);
|
||
|
||
// Scraping metallic saw undertone through the same sweeping bandpass
|
||
const saw = ctx.createOscillator();
|
||
saw.type = 'sawtooth';
|
||
saw.frequency.setValueAtTime(110, now);
|
||
saw.frequency.exponentialRampToValueAtTime(70, now + duration);
|
||
const sawGain = ctx.createGain();
|
||
sawGain.gain.setValueAtTime(0.001, now);
|
||
sawGain.gain.linearRampToValueAtTime(0.09, now + 0.6);
|
||
sawGain.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.5, now + 0.4);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
noise.connect(bp);
|
||
saw.connect(sawGain);
|
||
sawGain.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.05);
|
||
saw.start(now);
|
||
saw.stop(now + duration + 0.05);
|
||
}
|
||
|
||
/**
|
||
* Turbolift Pass Whoosh (Star Trek TNG/VOY, audio_gaps.md #22)
|
||
* Bandpass-filtered white noise pitch sweep (180 -> 450 Hz and back) with
|
||
* resonant boost and soft stereo panning drift past the listener.
|
||
*/
|
||
synthesizeTurboliftWhoosh() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 1.6;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(180, now);
|
||
bp.frequency.exponentialRampToValueAtTime(450, now + 0.55);
|
||
bp.frequency.exponentialRampToValueAtTime(180, now + 1.4);
|
||
bp.Q.setValueAtTime(2.2, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.42, now + 0.5);
|
||
env.gain.linearRampToValueAtTime(0.3, now + 0.8);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
let panOut;
|
||
if (ctx.createStereoPanner) {
|
||
const panner = ctx.createStereoPanner();
|
||
panner.pan.setValueAtTime(-0.45, now);
|
||
panner.pan.linearRampToValueAtTime(0.45, now + duration);
|
||
panOut = panner;
|
||
}
|
||
if (panOut) {
|
||
env.connect(panOut);
|
||
panOut.connect(this.gainNode);
|
||
} else {
|
||
env.connect(this.gainNode);
|
||
}
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.02);
|
||
}
|
||
|
||
/**
|
||
* Replicator Materialization Shimmer (Star Trek TNG/VOY, audio_gaps.md #23)
|
||
* High-frequency white-noise shimmer (2.5-8 kHz) amplitude-modulated by a
|
||
* fast 30 Hz sine LFO with a soft ramp decay.
|
||
*/
|
||
synthesizeReplicatorShimmer() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 2.0;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.15);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const masterEnv = ctx.createGain();
|
||
masterEnv.gain.setValueAtTime(0.001, now);
|
||
masterEnv.gain.linearRampToValueAtTime(0.3, now + 0.5);
|
||
masterEnv.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
masterEnv.connect(this.gainNode);
|
||
|
||
// Two parallel shimmer bands (2.5-4 kHz and 5-8 kHz)
|
||
const bands = [
|
||
{ center: 3200, q: 1.3, depth: 0.22 },
|
||
{ center: 6400, q: 1.2, depth: 0.16 }
|
||
];
|
||
bands.forEach((band) => {
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(band.center, now);
|
||
bp.Q.setValueAtTime(band.q, now);
|
||
|
||
const amEnv = ctx.createGain();
|
||
amEnv.gain.setValueAtTime(band.depth, now);
|
||
|
||
// 30 Hz shimmer AM
|
||
const lfo = ctx.createOscillator();
|
||
lfo.type = 'sine';
|
||
lfo.frequency.setValueAtTime(30, now);
|
||
const lfoDepth = ctx.createGain();
|
||
lfoDepth.gain.setValueAtTime(band.depth * 0.9, now);
|
||
lfo.connect(lfoDepth);
|
||
lfoDepth.connect(amEnv.gain);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(amEnv);
|
||
amEnv.connect(masterEnv);
|
||
|
||
lfo.start(now);
|
||
lfo.stop(now + duration + 0.05);
|
||
});
|
||
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.02);
|
||
}
|
||
|
||
/**
|
||
* Vorlon Crystal "Singing" Resonance (Bioships, audio_gaps.md #26)
|
||
* Ethereal telepathic crystal harmonics: dual detuned sines (528/532 Hz)
|
||
* through a narrow bandpass with a slow 0.2 Hz undulating tremolo.
|
||
* One 8.5 s swell phrasing.
|
||
*/
|
||
synthesizeVorlonSingingSwell() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 8.5;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(530, now);
|
||
bp.Q.setValueAtTime(22, now);
|
||
|
||
const tremolo = ctx.createGain();
|
||
tremolo.gain.setValueAtTime(0.5, now);
|
||
|
||
const lfo = ctx.createOscillator();
|
||
lfo.type = 'sine';
|
||
lfo.frequency.setValueAtTime(0.2, now);
|
||
const lfoDepth = ctx.createGain();
|
||
lfoDepth.gain.setValueAtTime(0.24, now);
|
||
lfo.connect(lfoDepth);
|
||
lfoDepth.connect(tremolo.gain);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.16, now + 3.0);
|
||
env.gain.linearRampToValueAtTime(0.13, now + 5.5);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
env.connect(this.gainNode);
|
||
|
||
[528, 532].forEach((freq, idx) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(freq, now);
|
||
if (idx === 1) osc.detune.setValueAtTime(3, now);
|
||
osc.connect(bp);
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.05);
|
||
});
|
||
|
||
bp.connect(tremolo);
|
||
tremolo.connect(env);
|
||
|
||
lfo.start(now);
|
||
lfo.stop(now + duration + 0.05);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Vorlon singing resonance while the Vorlon cruiser is engaged
|
||
*/
|
||
startVorlonSong(minIntervalMs = 11000, maxIntervalMs = 18000) {
|
||
this._scheduleLoop('vorlonSong', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeVorlonSingingSwell();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Neural-Bond Swell (Bioships, audio_gaps.md #27)
|
||
* Symbiotic neural link between pilot and bioship swelling with emotion:
|
||
* deep 55 Hz triangle sweeping into a resonant vowel-formant filter
|
||
* (350-850 Hz) over 2.5 seconds.
|
||
*/
|
||
synthesizeNeuralBondSwell() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 3.4;
|
||
|
||
// Sub-bass carrier
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'triangle';
|
||
osc.frequency.setValueAtTime(55, now);
|
||
osc.frequency.linearRampToValueAtTime(52, now + duration);
|
||
|
||
// Formant sweep adding vowel-like harmonics
|
||
const formant = ctx.createBiquadFilter();
|
||
formant.type = 'bandpass';
|
||
formant.frequency.setValueAtTime(350, now);
|
||
formant.frequency.linearRampToValueAtTime(850, now + 2.5);
|
||
formant.Q.setValueAtTime(4.5, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.34, now + 0.9);
|
||
env.gain.linearRampToValueAtTime(0.42, now + 1.9);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
osc.connect(formant);
|
||
formant.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.05);
|
||
}
|
||
|
||
/**
|
||
* Hull Self-Repair / Regeneration Texture (Bioships, audio_gaps.md #28)
|
||
* Microscopic organic tissue knit and chitin regrowth: granular
|
||
* amplitude-modulated pink noise (8-24 Hz modulation) with wet low
|
||
* regenerative pops. One ~1.8 s regeneration burst.
|
||
*/
|
||
synthesizeHullRegenBurst() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 1.8;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('pink', duration + 0.1);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(650, now);
|
||
bp.frequency.linearRampToValueAtTime(1600, now + duration);
|
||
bp.Q.setValueAtTime(2.0, now);
|
||
|
||
const modEnv = ctx.createGain();
|
||
modEnv.gain.setValueAtTime(0.16, now);
|
||
|
||
// Rapid scabbing modulation (~13 Hz) over the wet tissue bed
|
||
const lfo = ctx.createOscillator();
|
||
lfo.type = 'square';
|
||
lfo.frequency.setValueAtTime(9 + Math.random() * 8, now);
|
||
const lfoDepth = ctx.createGain();
|
||
lfoDepth.gain.setValueAtTime(0.12, now);
|
||
lfo.connect(lfoDepth);
|
||
lfoDepth.connect(modEnv.gain);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.2, now + 0.25);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(modEnv);
|
||
modEnv.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.05);
|
||
lfo.start(now);
|
||
lfo.stop(now + duration + 0.05);
|
||
|
||
// Wet bioplasmic pops (chitin knit)
|
||
const popCount = 2 + Math.floor(Math.random() * 3);
|
||
for (let i = 0; i < popCount; i++) {
|
||
const popTime = now + 0.25 + Math.random() * (duration - 0.6);
|
||
const pop = ctx.createOscillator();
|
||
pop.type = 'sine';
|
||
pop.frequency.setValueAtTime(110 + Math.random() * 60, popTime);
|
||
pop.frequency.exponentialRampToValueAtTime(60, popTime + 0.06);
|
||
const popEnv = ctx.createGain();
|
||
popEnv.gain.setValueAtTime(0.001, popTime);
|
||
popEnv.gain.linearRampToValueAtTime(0.14, popTime + 0.008);
|
||
popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.07);
|
||
pop.connect(popEnv);
|
||
popEnv.connect(this.gainNode);
|
||
pop.start(popTime);
|
||
pop.stop(popTime + 0.08);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Periodic hull regeneration texture on Wraith hives
|
||
*/
|
||
startHullRegen(minIntervalMs = 6000, maxIntervalMs = 13000) {
|
||
this._scheduleLoop('hullRegen', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeHullRegenBurst();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Belter Jury-Rigged Telemetry Jitter (Space Stations, audio_gaps.md #29)
|
||
* Rattle of loose relays, worn copper contactors and erratic voltage drops:
|
||
* noisy square relay pulses with micro-dropouts, contact-bounce double
|
||
* clicks and harsh metallic ticks on a jittered clock. One ~1.1 s cluster.
|
||
*/
|
||
synthesizeBelterTelemetryJitter() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const eventCount = 3 + Math.floor(Math.random() * 5); // 3-7 events
|
||
let cursor = now + 0.05 + Math.random() * 0.2;
|
||
|
||
const relayPulse = (t) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
const baseFreq = 60 + Math.random() * 60;
|
||
osc.frequency.setValueAtTime(baseFreq, t);
|
||
osc.frequency.linearRampToValueAtTime(baseFreq * 0.8, t + 0.06);
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(420, t);
|
||
const env = ctx.createGain();
|
||
// Erratic voltage: randomized micro-dropouts in the pulse body
|
||
env.gain.setValueAtTime(0.17, t);
|
||
env.gain.setValueAtTime(0.001, t + 0.012 + Math.random() * 0.015);
|
||
env.gain.setValueAtTime(0.14, t + 0.026 + Math.random() * 0.02);
|
||
env.gain.setValueAtTime(0.001, t + 0.05 + Math.random() * 0.025);
|
||
env.gain.setValueAtTime(0.1, t + 0.07);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
|
||
osc.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(t);
|
||
osc.stop(t + 0.11);
|
||
};
|
||
|
||
const contactBounce = (t) => {
|
||
// Worn contactor: double/triple click 1-3 ms apart
|
||
const clicks = 2 + (Math.random() < 0.35 ? 1 : 0);
|
||
for (let i = 0; i < clicks; i++) {
|
||
const ct = t + i * (0.001 + Math.random() * 0.002);
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(500 + Math.random() * 500, ct);
|
||
osc.frequency.exponentialRampToValueAtTime(120, ct + 0.008);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.14, ct);
|
||
env.gain.exponentialRampToValueAtTime(0.001, ct + 0.012);
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(ct);
|
||
osc.stop(ct + 0.015);
|
||
}
|
||
};
|
||
|
||
const metallicTick = (t) => {
|
||
const buf = this.am.createNoiseBuffer('white', 0.05);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = buf;
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(400 + Math.random() * 1400, t);
|
||
bp.Q.setValueAtTime(6, t);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.09, t);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.02);
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(t);
|
||
noise.stop(t + 0.025);
|
||
};
|
||
|
||
for (let i = 0; i < eventCount; i++) {
|
||
const kind = Math.random();
|
||
if (kind < 0.4) relayPulse(cursor);
|
||
else if (kind < 0.75) contactBounce(cursor);
|
||
else metallicTick(cursor);
|
||
cursor += 0.08 + Math.random() * 0.22; // jittered clock drift
|
||
}
|
||
}
|
||
|
||
/**
|
||
* True Hull Strain Moan (Deep Space, audio_gaps.md #30)
|
||
* Massive structural groan of stressed bulkheads flexing under gravitational
|
||
* shear: resonant bandpass slowly sweeping 40-220 Hz (high Q) over shaped
|
||
* brown noise with an asymmetric attack/decay. ~5.5 s.
|
||
*/
|
||
synthesizeHullStrainMoan() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 5.5;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('brown', duration + 0.3);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(48, now);
|
||
bp.frequency.exponentialRampToValueAtTime(210, now + 3.4);
|
||
bp.Q.setValueAtTime(10, now);
|
||
|
||
// Asymmetric envelope: slow menacing rise, long creaking decay
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.3, now + 0.9);
|
||
env.gain.linearRampToValueAtTime(0.22, now + 1.7);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.05);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Sporadic structural strain moans on haunted deep-space hulls
|
||
*/
|
||
startHullStrainMoans(minIntervalMs = 16000, maxIntervalMs = 40000) {
|
||
this._scheduleLoop('hullStrainMoans', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeHullStrainMoan();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Icarus I Distress Beacon (Deep Space, audio_gaps.md #31)
|
||
* Eerie hypnotic modal arpeggio echoing from the ghost ship: a 4-note
|
||
* modal sine sequence through a long feedback delay with soft lowpass
|
||
* dampening. ~7 s including delay tail.
|
||
*/
|
||
synthesizeIcarusBeacon() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const notes = [220.0, 261.63, 329.63, 440.0]; // A minor modal beacon
|
||
|
||
const delay = ctx.createDelay(1.2);
|
||
delay.delayTime.setValueAtTime(0.45, now);
|
||
const feedback = ctx.createGain();
|
||
feedback.gain.setValueAtTime(0.35, now);
|
||
const dampen = ctx.createBiquadFilter();
|
||
dampen.type = 'lowpass';
|
||
dampen.frequency.setValueAtTime(2200, now);
|
||
const wet = ctx.createGain();
|
||
wet.gain.setValueAtTime(0.5, now);
|
||
|
||
delay.connect(feedback);
|
||
feedback.connect(dampen);
|
||
dampen.connect(delay);
|
||
delay.connect(wet);
|
||
wet.connect(this.gainNode);
|
||
|
||
notes.forEach((freq, idx) => {
|
||
const t = now + idx * 0.95;
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(freq, t);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(0.17, t + 0.05);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.85);
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
env.connect(delay);
|
||
osc.start(t);
|
||
osc.stop(t + 0.9);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Periodic distress beacon arpeggio while Icarus II is engaged
|
||
*/
|
||
startIcarusBeacon(minIntervalMs = 22000, maxIntervalMs = 40000) {
|
||
this._scheduleLoop('icarusBeacon', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeIcarusBeacon();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Extradimensional Psychic Static Whisper (Deep Space, audio_gaps.md #32)
|
||
* Menacing psychoacoustic burst: multi-formant filtered white noise with
|
||
* randomized micro-envelopes evoking whispered vowels (800-2400 Hz).
|
||
*/
|
||
synthesizeVoidWhisper() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 2.2;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('white', duration + 0.1);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.12, now + 0.4);
|
||
env.gain.linearRampToValueAtTime(0.09, now + 1.3);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
env.connect(this.gainNode);
|
||
|
||
// Three parallel whispered-vowel formants with independent wobbles
|
||
const formants = [
|
||
{ center: 700, q: 7, wobbleHz: 0.8, wobbleAmt: 160 },
|
||
{ center: 1250, q: 8, wobbleHz: 1.1, wobbleAmt: 280 },
|
||
{ center: 2200, q: 10, wobbleHz: 1.6, wobbleAmt: 420 }
|
||
];
|
||
formants.forEach((formant) => {
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(formant.center, now);
|
||
bp.Q.setValueAtTime(formant.q, now);
|
||
|
||
const wobble = ctx.createOscillator();
|
||
wobble.type = 'sine';
|
||
wobble.frequency.setValueAtTime(formant.wobbleHz, now);
|
||
const wobbleDepth = ctx.createGain();
|
||
wobbleDepth.gain.setValueAtTime(formant.wobbleAmt, now);
|
||
wobble.connect(wobbleDepth);
|
||
wobbleDepth.connect(bp.frequency);
|
||
|
||
const breath = ctx.createGain();
|
||
breath.gain.setValueAtTime(0.3, now);
|
||
// Randomized micro-envelopes: syllable-like gulps of the formant
|
||
const gulpCount = 3 + Math.floor(Math.random() * 3);
|
||
for (let i = 0; i < gulpCount; i++) {
|
||
const gt = now + 0.15 + Math.random() * (duration - 0.55);
|
||
breath.gain.setValueAtTime(0.12, gt);
|
||
breath.gain.linearRampToValueAtTime(0.3 + Math.random() * 0.18, gt + 0.04);
|
||
breath.gain.linearRampToValueAtTime(0.05, gt + 0.12 + Math.random() * 0.1);
|
||
}
|
||
|
||
noise.connect(bp);
|
||
bp.connect(breath);
|
||
breath.connect(env);
|
||
|
||
wobble.start(now);
|
||
wobble.stop(now + duration + 0.05);
|
||
});
|
||
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.02);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Psychic whisper bursts on haunted deep-space hulls
|
||
*/
|
||
startVoidWhispers(minIntervalMs = 15000, maxIntervalMs = 32000) {
|
||
this._scheduleLoop('voidWhispers', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeVoidWhisper();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Beryllium Sphere Resonant Thrum (Comedy, audio_gaps.md #33)
|
||
* Deep glassy crystalline reactor hum: dual pure sines (65 + 195 Hz third
|
||
* harmonic) with slow beating detune and a subtle comb-filter ring.
|
||
* One ~10 s swell phrasing.
|
||
*/
|
||
synthesizeBerylliumThrumSwell() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 10.0;
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.16, now + 4.0);
|
||
env.gain.linearRampToValueAtTime(0.14, now + 6.5);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
env.connect(this.gainNode);
|
||
|
||
// Comb ring: short 5.1 ms reflection
|
||
const comb = ctx.createDelay(0.05);
|
||
comb.delayTime.setValueAtTime(0.0051, now);
|
||
const combFb = ctx.createGain();
|
||
combFb.gain.setValueAtTime(0.45, now);
|
||
comb.connect(combFb);
|
||
combFb.connect(comb);
|
||
const combWet = ctx.createGain();
|
||
combWet.gain.setValueAtTime(0.6, now);
|
||
comb.connect(combWet);
|
||
combWet.connect(env);
|
||
|
||
const partials = [
|
||
{ freq: 65, amp: 0.55 },
|
||
{ freq: 65.3, amp: 0.3 }, // slow 0.3 Hz beating partner
|
||
{ freq: 195, amp: 0.3 }
|
||
];
|
||
partials.forEach((partial) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(partial.freq, now);
|
||
const g = ctx.createGain();
|
||
g.gain.setValueAtTime(partial.amp, now);
|
||
osc.connect(g);
|
||
g.connect(env);
|
||
g.connect(comb);
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.05);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Beryllium sphere thrum while the NSEA Protector is engaged
|
||
*/
|
||
startBerylliumThrum(minIntervalMs = 9000, maxIntervalMs = 16000) {
|
||
this._scheduleLoop('berylliumThrum', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeBerylliumThrumSwell();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Omega-13 Temporal Capacitor Whine (Comedy, audio_gaps.md #34)
|
||
* Tremendous temporal capacitor power buildup: deep 40 Hz sub-bass swelling
|
||
* exponentially to 3200 Hz over 3 seconds, culminating in a wide
|
||
* white-noise discharge pop.
|
||
*/
|
||
synthesizeOmega13Whine() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 3.1;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(40, now);
|
||
osc.frequency.exponentialRampToValueAtTime(3200, now + 3.0);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.exponentialRampToValueAtTime(0.5, now + 2.7);
|
||
env.gain.setValueAtTime(0.5, now + 2.95);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + 3.2);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.1);
|
||
|
||
// Dimensional discharge pop (wide white-noise transient)
|
||
const popTime = now + 3.0;
|
||
const popBuf = this.am.createNoiseBuffer('white', 0.3);
|
||
const pop = ctx.createBufferSource();
|
||
pop.buffer = popBuf;
|
||
const hp = ctx.createBiquadFilter();
|
||
hp.type = 'highpass';
|
||
hp.frequency.setValueAtTime(2000, popTime);
|
||
const popEnv = ctx.createGain();
|
||
popEnv.gain.setValueAtTime(0.001, popTime);
|
||
popEnv.gain.linearRampToValueAtTime(0.42, popTime + 0.008);
|
||
popEnv.gain.exponentialRampToValueAtTime(0.001, popTime + 0.16);
|
||
pop.connect(hp);
|
||
hp.connect(popEnv);
|
||
popEnv.connect(this.gainNode);
|
||
pop.start(popTime);
|
||
pop.stop(popTime + 0.18);
|
||
}
|
||
|
||
/**
|
||
* Repulsorlift Engine Drone (Outlaw, audio_gaps.md #35)
|
||
* Quintessential anti-gravity vehicle wash: dual detuned sines (68/72 Hz)
|
||
* through an asymmetric overdrive waveshaper, lowpass-filtered at 280 Hz.
|
||
* One ~10 s wash phrasing.
|
||
*/
|
||
synthesizeRepulsorliftDrone() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 10.0;
|
||
|
||
let shaperOut = null;
|
||
if (ctx.createWaveShaper) {
|
||
const shaper = ctx.createWaveShaper();
|
||
shaper.oversample = '2x';
|
||
const curve = new Float32Array(1024);
|
||
for (let i = 0; i < 1024; i++) {
|
||
const x = (i / 512) - 1; // -1..1
|
||
curve[i] = Math.tanh(2.4 * x); // asymmetric-ish saturation
|
||
}
|
||
shaper.curve = curve;
|
||
shaperOut = shaper;
|
||
}
|
||
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(280, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.2, now + 2.6);
|
||
env.gain.linearRampToValueAtTime(0.17, now + 7.2);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
|
||
if (shaperOut) shaperOut.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
[68, 72].forEach((freq) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(freq, now);
|
||
osc.connect(shaperOut || lp);
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.05);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Repulsorlift wash while the Marauder is engaged
|
||
*/
|
||
startRepulsorliftDrone(intervalMs = 13000) {
|
||
this._scheduleLoop('repulsorliftDrone', intervalMs, intervalMs, () => {
|
||
this.synthesizeRepulsorliftDrone();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Converted Marine Trawler Engine Chug (Outlaw, audio_gaps.md #36)
|
||
* Rhythmic heavy diesel-like piston strokes: low-frequency square pulses
|
||
* (4-8 Hz cadence) through an 80 Hz resonant lowpass with mechanical
|
||
* piston wheeze. One cluster of 2-4 strokes.
|
||
*/
|
||
synthesizeBebopChugStroke() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const strokes = 2 + Math.floor(Math.random() * 3); // 2-4 strokes
|
||
|
||
const stroke = (t, peak) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'square';
|
||
osc.frequency.setValueAtTime(45 + Math.random() * 25, t);
|
||
osc.frequency.linearRampToValueAtTime(38, t + 0.3);
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(80, t);
|
||
lp.Q.setValueAtTime(3, t);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(peak, t + 0.02);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.42);
|
||
osc.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(t);
|
||
osc.stop(t + 0.45);
|
||
|
||
// Mechanical piston wheeze
|
||
const wheezeBuf = this.am.createNoiseBuffer('pink', 0.3);
|
||
const wheeze = ctx.createBufferSource();
|
||
wheeze.buffer = wheezeBuf;
|
||
const wheezeBp = ctx.createBiquadFilter();
|
||
wheezeBp.type = 'bandpass';
|
||
wheezeBp.frequency.setValueAtTime(380, t);
|
||
wheezeBp.Q.setValueAtTime(1.6, t);
|
||
const wheezeEnv = ctx.createGain();
|
||
wheezeEnv.gain.setValueAtTime(0.001, t + 0.02);
|
||
wheezeEnv.gain.linearRampToValueAtTime(0.05, t + 0.08);
|
||
wheezeEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
|
||
wheeze.connect(wheezeBp);
|
||
wheezeBp.connect(wheezeEnv);
|
||
wheezeEnv.connect(this.gainNode);
|
||
wheeze.start(t);
|
||
wheeze.stop(t + 0.32);
|
||
};
|
||
|
||
for (let i = 0; i < strokes; i++) {
|
||
stroke(now + i * (0.34 + Math.random() * 0.2), 0.3 - i * 0.04);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Rhythmic diesel chug while the Bebop is engaged
|
||
*/
|
||
startBebopChug(minIntervalMs = 2200, maxIntervalMs = 4200) {
|
||
this._scheduleLoop('bebopChug', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeBebopChugStroke();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Centrifugal Habitat Carousel Motor Groan (Military, audio_gaps.md #37)
|
||
* Deep rotational strain of a kilometer-long rotating drum: sub-audible
|
||
* 16 Hz rotational pair with bearing harmonics, modulated by a single
|
||
* ~1 RPM (0.0167 Hz) revolution LFO. One 58 s rotation pass.
|
||
*/
|
||
synthesizeCarouselGroanCycle() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 58.0;
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.55, now + 8.0);
|
||
env.gain.linearRampToValueAtTime(0.5, now + 50.0);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
env.connect(this.gainNode);
|
||
|
||
// One rotation per ~60 s: slow strain swell and release
|
||
const rotation = ctx.createOscillator();
|
||
rotation.type = 'sine';
|
||
rotation.frequency.setValueAtTime(1 / 60, now);
|
||
const rotationDepth = ctx.createGain();
|
||
rotationDepth.gain.setValueAtTime(0.38, now);
|
||
rotation.connect(rotationDepth);
|
||
rotationDepth.connect(env.gain);
|
||
|
||
const partials = [
|
||
{ freq: 16, amp: 0.55 },
|
||
{ freq: 16.04, amp: 0.32 }, // rotational beating pair
|
||
{ freq: 48, amp: 0.22 }, // structural harmonic
|
||
{ freq: 96, amp: 0.07 } // bearing hum harmonic
|
||
];
|
||
partials.forEach((partial) => {
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(partial.freq, now);
|
||
const g = ctx.createGain();
|
||
g.gain.setValueAtTime(partial.amp, now);
|
||
osc.connect(g);
|
||
g.connect(env);
|
||
osc.start(now);
|
||
osc.stop(now + duration + 0.1);
|
||
});
|
||
|
||
rotation.start(now);
|
||
rotation.stop(now + duration + 0.1);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Carousel rotation groan while the Agamemnon is engaged
|
||
* (one 58 s pass every 60 s; the 2 s gap reads as a rotation splice)
|
||
*/
|
||
startCarouselGroan(intervalMs = 60000) {
|
||
this._scheduleLoop('carouselGroan', intervalMs, intervalMs, () => {
|
||
this.synthesizeCarouselGroanCycle();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Slipstream Transition Surge (Military, audio_gaps.md #38)
|
||
* Ship tearing through an exotic slipstream: dual sweeping bandpass surges
|
||
* (200 -> 2400 Hz) with escalating resonance Q and a comb-filter shimmer
|
||
* tail. ~4.2 s.
|
||
*/
|
||
synthesizeSlipstreamSurge() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Comb shimmer tail bus
|
||
const shimmer = ctx.createDelay(0.5);
|
||
shimmer.delayTime.setValueAtTime(0.12, now);
|
||
const shimmerFb = ctx.createGain();
|
||
shimmerFb.gain.setValueAtTime(0.3, now);
|
||
const shimmerLp = ctx.createBiquadFilter();
|
||
shimmerLp.type = 'lowpass';
|
||
shimmerLp.frequency.setValueAtTime(4000, now);
|
||
shimmer.connect(shimmerFb);
|
||
shimmerFb.connect(shimmerLp);
|
||
shimmerLp.connect(shimmer);
|
||
const shimmerWet = ctx.createGain();
|
||
shimmerWet.gain.setValueAtTime(0.5, now);
|
||
shimmer.connect(shimmerWet);
|
||
shimmerWet.connect(this.gainNode);
|
||
|
||
const surge = (offset, peak) => {
|
||
const t = now + offset;
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sawtooth';
|
||
osc.frequency.setValueAtTime(200, t);
|
||
osc.frequency.exponentialRampToValueAtTime(2400, t + 2.6);
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(200, t);
|
||
bp.frequency.exponentialRampToValueAtTime(2400, t + 2.6);
|
||
bp.Q.setValueAtTime(2, t);
|
||
bp.Q.linearRampToValueAtTime(10, t + 2.6);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(peak, t + 1.4);
|
||
env.gain.setValueAtTime(peak, t + 2.7);
|
||
env.gain.exponentialRampToValueAtTime(0.001, t + 3.8);
|
||
|
||
osc.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
env.connect(shimmer);
|
||
osc.start(t);
|
||
osc.stop(t + 4.0);
|
||
};
|
||
|
||
surge(0, 0.22);
|
||
surge(0.3, 0.16);
|
||
}
|
||
|
||
/**
|
||
* Viper Pilot Oxygen Regulator Demand Valve (Military, audio_gaps.md #39)
|
||
* Pulsing rebreather of a Viper pilot mid-combat: a sharp 15 ms mechanical
|
||
* diaphragm click preceding pulsed highpass-filtered white noise breath
|
||
* (1.2-4.5 kHz, ~1.1 s). One breath cycle.
|
||
*/
|
||
synthesizeOxygenRegulatorCycle() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
|
||
// Sharp mechanical diaphragm click
|
||
const click = ctx.createOscillator();
|
||
click.type = 'triangle';
|
||
click.frequency.setValueAtTime(1800, now);
|
||
click.frequency.exponentialRampToValueAtTime(600, now + 0.015);
|
||
const clickEnv = ctx.createGain();
|
||
clickEnv.gain.setValueAtTime(0.3, now);
|
||
clickEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.02);
|
||
click.connect(clickEnv);
|
||
clickEnv.connect(this.gainNode);
|
||
click.start(now);
|
||
click.stop(now + 0.025);
|
||
|
||
// Regulated airflow breath
|
||
const noiseBuf = this.am.createNoiseBuffer('white', 1.3);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
const hp = ctx.createBiquadFilter();
|
||
hp.type = 'highpass';
|
||
hp.frequency.setValueAtTime(1200, now + 0.02);
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(4500, now + 0.02);
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now + 0.02);
|
||
env.gain.linearRampToValueAtTime(0.13, now + 0.45);
|
||
env.gain.linearRampToValueAtTime(0.09, now + 0.62);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + 1.15);
|
||
noise.connect(hp);
|
||
hp.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(now + 0.02);
|
||
noise.stop(now + 1.2);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Rhythmic oxygen demand breathing while a Viper cockpit
|
||
* is engaged
|
||
*/
|
||
startOxygenRegulator(minIntervalMs = 3400, maxIntervalMs = 5200) {
|
||
this._scheduleLoop('oxygenRegulator', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeOxygenRegulatorCycle();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Condensation Pipe Drip & Expansion Tick (Industrial, audio_gaps.md #40)
|
||
* Lonely water droplet pinging inside a kilometres-long cargo hauler:
|
||
* high-Q sine pings (1400-2600 Hz) with fast exponential decay on a
|
||
* randomized clock. One drip.
|
||
*/
|
||
synthesizePipeDrip() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const decay = 0.01 + Math.random() * 0.015;
|
||
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sine';
|
||
const freq = 1400 + Math.random() * 1200;
|
||
osc.frequency.setValueAtTime(freq, now);
|
||
osc.frequency.exponentialRampToValueAtTime(freq * 0.92, now + decay);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.1 + Math.random() * 0.2, now + 0.001);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + decay + 0.01);
|
||
|
||
osc.connect(env);
|
||
env.connect(this.gainNode);
|
||
osc.start(now);
|
||
osc.stop(now + decay + 0.03);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Sporadic condensation drips while the Nostromo is engaged
|
||
*/
|
||
startPipeDrips(minIntervalMs = 500, maxIntervalMs = 3500) {
|
||
this._scheduleLoop('pipeDrips', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizePipeDrip();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* High-Pressure Steam / Boiler Venting (Industrial, audio_gaps.md #41)
|
||
* Continuous hot-vent hiss of a steam-era mining vessel: bandpass-filtered
|
||
* white noise (800-2800 Hz) with a slow amplitude swell and random
|
||
* micro-flutter. One ~9 s vent pass.
|
||
*/
|
||
synthesizeSteamVentSwell() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 9.0;
|
||
|
||
// Looping noise bed (short buffer, sustained vent)
|
||
const noiseBuf = this.am.createNoiseBuffer('white', 4.0);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
noise.loop = true;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(1500, now);
|
||
bp.Q.setValueAtTime(0.7, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.17, now + 3.0);
|
||
env.gain.linearRampToValueAtTime(0.15, now + 5.5);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||
|
||
// Boiler pressure micro-flutter (~6-9 Hz jitter)
|
||
const flutter = ctx.createOscillator();
|
||
flutter.type = 'sine';
|
||
flutter.frequency.setValueAtTime(6 + Math.random() * 3, now);
|
||
const flutterDepth = ctx.createGain();
|
||
flutterDepth.gain.setValueAtTime(0.04, now);
|
||
flutter.connect(flutterDepth);
|
||
flutterDepth.connect(env.gain);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(now);
|
||
noise.stop(now + duration + 0.1);
|
||
flutter.start(now);
|
||
flutter.stop(now + duration + 0.1);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Boiler vent passes while Serenity-class and Starbug
|
||
* presets are engaged
|
||
*/
|
||
startSteamVent(minIntervalMs = 6000, maxIntervalMs = 10000) {
|
||
this._scheduleLoop('steamVent', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeSteamVentSwell();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Crash-Couch Hydraulic Gimbal Strain (Industrial, audio_gaps.md #42)
|
||
* Pilot's couch swinging into launch position: FM triangle carrier
|
||
* (140 Hz / 35 Hz modulator) through lowpass damping that tracks the
|
||
* acceleration strain. ~1.9 s.
|
||
*/
|
||
synthesizeCrashCouchGimbal() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const duration = 1.9;
|
||
|
||
const layer = (offset, carrierFreq, modFreq) => {
|
||
const t = now + offset;
|
||
const carrier = ctx.createOscillator();
|
||
carrier.type = 'triangle';
|
||
carrier.frequency.setValueAtTime(carrierFreq, t);
|
||
|
||
const mod = ctx.createOscillator();
|
||
mod.type = 'sine';
|
||
mod.frequency.setValueAtTime(modFreq, t);
|
||
const modDepth = ctx.createGain();
|
||
modDepth.gain.setValueAtTime(carrierFreq * 4, t);
|
||
mod.connect(modDepth);
|
||
modDepth.connect(carrier.frequency);
|
||
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(300, t);
|
||
lp.frequency.linearRampToValueAtTime(900, t + 0.5);
|
||
lp.frequency.exponentialRampToValueAtTime(350, t + duration - offset);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, t);
|
||
env.gain.linearRampToValueAtTime(0.24, t + 0.55);
|
||
env.gain.linearRampToValueAtTime(0.2, t + 1.0);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, t + duration - offset);
|
||
|
||
carrier.connect(lp);
|
||
lp.connect(env);
|
||
env.connect(this.gainNode);
|
||
carrier.start(t);
|
||
carrier.stop(t + (duration - offset) + 0.05);
|
||
mod.start(t);
|
||
mod.stop(t + (duration - offset) + 0.05);
|
||
};
|
||
|
||
layer(0, 140, 35);
|
||
layer(0.42, 152, 38);
|
||
}
|
||
|
||
/**
|
||
* HAL 9000 Breathing Loop (Retro Future, audio_gaps.md #43)
|
||
* Eerie slow respiration of a quiet, observant ship computer: rhythmic
|
||
* bandpass-filtered pink noise (450-1100 Hz) with a gentle 3.5 s
|
||
* inhalation/exhalation envelope. One breath.
|
||
*/
|
||
synthesizeHalBreathCycle() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const inhale = 1.3;
|
||
const exhale = 2.2;
|
||
|
||
const noiseBuf = this.am.createNoiseBuffer('pink', 4.0);
|
||
const noise = ctx.createBufferSource();
|
||
noise.buffer = noiseBuf;
|
||
noise.loop = true;
|
||
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(700, now);
|
||
bp.Q.setValueAtTime(1.2, now);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.085, now + inhale);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + inhale + exhale);
|
||
|
||
noise.connect(bp);
|
||
bp.connect(env);
|
||
env.connect(this.gainNode);
|
||
noise.start(now);
|
||
noise.stop(now + inhale + exhale + 0.05);
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Continuous respiration while the Discovery One is engaged
|
||
*/
|
||
startHalBreathing(intervalMs = 3500) {
|
||
this._scheduleLoop('halBreathing', intervalMs, intervalMs, () => {
|
||
this.synthesizeHalBreathCycle();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Death Blossom Energy Surge Ramp (Retro Future, audio_gaps.md #44)
|
||
* Fearsome gunstar overdrive: cascaded sawtooth exponential ramp
|
||
* (200 Hz -> 8000 Hz over 2.5 s) with rising overdrive, cutting off
|
||
* abruptly at full bloom.
|
||
*/
|
||
synthesizeDeathBlossomSurge() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const rampDur = 2.5;
|
||
|
||
const hp = ctx.createBiquadFilter();
|
||
hp.type = 'highpass';
|
||
hp.frequency.setValueAtTime(400, now);
|
||
hp.frequency.exponentialRampToValueAtTime(4000, now + rampDur);
|
||
|
||
const hp2 = ctx.createBiquadFilter();
|
||
hp2.type = 'highpass';
|
||
hp2.frequency.setValueAtTime(200, now);
|
||
hp2.frequency.exponentialRampToValueAtTime(2000, now + rampDur);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.001, now);
|
||
env.gain.linearRampToValueAtTime(0.32, now + rampDur);
|
||
env.gain.setValueAtTime(0.32, now + rampDur + 0.03);
|
||
env.gain.exponentialRampToValueAtTime(0.001, now + rampDur + 0.4);
|
||
|
||
hp.connect(hp2);
|
||
hp2.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
// Cascaded overdriven saws with staggered ignition
|
||
[0, 0.15, 0.3].forEach((offset, idx) => {
|
||
const t = now + offset;
|
||
const osc = ctx.createOscillator();
|
||
osc.type = 'sawtooth';
|
||
osc.frequency.setValueAtTime(200, t);
|
||
osc.frequency.exponentialRampToValueAtTime(8000, t + rampDur - offset);
|
||
const g = ctx.createGain();
|
||
g.gain.setValueAtTime(0.33 - idx * 0.08, t);
|
||
osc.connect(g);
|
||
g.connect(hp);
|
||
osc.start(t);
|
||
osc.stop(t + (rampDur - offset) + 0.45);
|
||
});
|
||
|
||
// Abrupt bloom transient
|
||
const burstTime = now + rampDur;
|
||
const burstBuf = this.am.createNoiseBuffer('white', 0.25);
|
||
const burst = ctx.createBufferSource();
|
||
burst.buffer = burstBuf;
|
||
const bp = ctx.createBiquadFilter();
|
||
bp.type = 'bandpass';
|
||
bp.frequency.setValueAtTime(3500, burstTime);
|
||
bp.Q.setValueAtTime(1.5, burstTime);
|
||
const burstEnv = ctx.createGain();
|
||
burstEnv.gain.setValueAtTime(0.001, burstTime);
|
||
burstEnv.gain.linearRampToValueAtTime(0.3, burstTime + 0.01);
|
||
burstEnv.gain.exponentialRampToValueAtTime(0.001, burstTime + 0.22);
|
||
burst.connect(bp);
|
||
bp.connect(burstEnv);
|
||
burstEnv.connect(this.gainNode);
|
||
burst.start(burstTime);
|
||
burst.stop(burstTime + 0.25);
|
||
}
|
||
|
||
/**
|
||
* Victorian Steam Piston Chug (Retro Future, audio_gaps.md #45)
|
||
* The steam-age skeleton of the Cygnus: lowpass-filtered noise bursts
|
||
* synchronized with heavy 40 Hz triangle thumps and a metallic slapback.
|
||
* One cluster of 2-3 stroke cycles.
|
||
*/
|
||
synthesizeCygnusPistonChug() {
|
||
this.init();
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return;
|
||
|
||
const now = ctx.currentTime;
|
||
const strokes = 2 + Math.floor(Math.random() * 2); // 2-3 strokes
|
||
|
||
const slap = ctx.createDelay(0.6);
|
||
slap.delayTime.setValueAtTime(0.35, now);
|
||
const slapFb = ctx.createGain();
|
||
slapFb.gain.setValueAtTime(0.3, now);
|
||
const slapWet = ctx.createGain();
|
||
slapWet.gain.setValueAtTime(0.4, now);
|
||
slap.connect(slapFb);
|
||
slapFb.connect(slap);
|
||
slap.connect(slapWet);
|
||
slapWet.connect(this.gainNode);
|
||
|
||
const stroke = (t, peak) => {
|
||
// Heavy piston body thump
|
||
const thump = ctx.createOscillator();
|
||
thump.type = 'triangle';
|
||
thump.frequency.setValueAtTime(40, t);
|
||
thump.frequency.linearRampToValueAtTime(32, t + 0.4);
|
||
const lp = ctx.createBiquadFilter();
|
||
lp.type = 'lowpass';
|
||
lp.frequency.setValueAtTime(120, t);
|
||
const thumpEnv = ctx.createGain();
|
||
thumpEnv.gain.setValueAtTime(0.001, t);
|
||
thumpEnv.gain.linearRampToValueAtTime(peak, t + 0.02);
|
||
thumpEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
|
||
thump.connect(lp);
|
||
lp.connect(thumpEnv);
|
||
thumpEnv.connect(this.gainNode);
|
||
thumpEnv.connect(slap);
|
||
thump.start(t);
|
||
thump.stop(t + 0.5);
|
||
|
||
// Synchronized steam exhaust puff
|
||
const puffBuf = this.am.createNoiseBuffer('white', 0.16);
|
||
const puff = ctx.createBufferSource();
|
||
puff.buffer = puffBuf;
|
||
const puffLp = ctx.createBiquadFilter();
|
||
puffLp.type = 'lowpass';
|
||
puffLp.frequency.setValueAtTime(500, t);
|
||
const puffEnv = ctx.createGain();
|
||
puffEnv.gain.setValueAtTime(0.001, t + 0.01);
|
||
puffEnv.gain.linearRampToValueAtTime(0.13, t + 0.03);
|
||
puffEnv.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
|
||
puff.connect(puffLp);
|
||
puffLp.connect(puffEnv);
|
||
puffEnv.connect(this.gainNode);
|
||
puff.start(t);
|
||
puff.stop(t + 0.17);
|
||
};
|
||
|
||
for (let i = 0; i < strokes; i++) {
|
||
stroke(now + i * (0.5 + Math.random() * 0.25), 0.26 - i * 0.03);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ambient Loop: Steam piston chug while the Cygnus is engaged
|
||
*/
|
||
startCygnusChug(minIntervalMs = 1800, maxIntervalMs = 3200) {
|
||
this._scheduleLoop('cygnusChug', minIntervalMs, maxIntervalMs, () => {
|
||
this.synthesizeCygnusPistonChug();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
});
|
||
}
|
||
}
|
||
|
||
window.ExpandedSciFiAudioSynth = ExpandedSciFiAudioSynth;
|
||
|
||
|
||
/**
|
||
* Layered & Staged Engine Startup and Shutdown Synthesizer
|
||
* Synthesizes chronological multi-phase acoustic scripts for 15 distinct starship archetypes.
|
||
*/
|
||
class EngineTransitionSynth {
|
||
constructor(audioManager) {
|
||
this.am = audioManager;
|
||
this.gainNode = null;
|
||
this.activeNodes = [];
|
||
this.activeTimeouts = [];
|
||
}
|
||
|
||
init() {
|
||
if (!this.am.ctx) return;
|
||
if (!this.gainNode) {
|
||
this.gainNode = this.am.ctx.createGain();
|
||
this.gainNode.gain.setValueAtTime(0.85, this.am.ctx.currentTime);
|
||
this.gainNode.connect(this.am.compressor);
|
||
}
|
||
}
|
||
|
||
stopTransitions() {
|
||
this.activeTimeouts.forEach(t => clearTimeout(t));
|
||
this.activeTimeouts = [];
|
||
this.activeNodes.forEach(node => {
|
||
try { if (node.stop) node.stop(); } catch (e) {}
|
||
try { node.disconnect(); } catch (e) {}
|
||
});
|
||
this.activeNodes = [];
|
||
}
|
||
|
||
_tone({ type = 'sine', startFreq, endFreq, startTime, duration, startVol = 0.001, peakVol = 0.4, endVol = 0.0001, filterType = null, filterFreq = 1000, filterQ = 1.0 }) {
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return null;
|
||
const osc = ctx.createOscillator();
|
||
osc.type = type;
|
||
osc.frequency.setValueAtTime(Math.max(10, startFreq), startTime);
|
||
if (endFreq && endFreq !== startFreq) {
|
||
osc.frequency.exponentialRampToValueAtTime(Math.max(10, endFreq), startTime + duration);
|
||
}
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(Math.max(0.0001, startVol), startTime);
|
||
const attack = Math.min(0.08, duration * 0.25);
|
||
env.gain.linearRampToValueAtTime(peakVol, startTime + attack);
|
||
env.gain.exponentialRampToValueAtTime(Math.max(0.0001, endVol), startTime + duration);
|
||
|
||
let lastNode = osc;
|
||
if (filterType) {
|
||
const filter = ctx.createBiquadFilter();
|
||
filter.type = filterType;
|
||
filter.frequency.setValueAtTime(filterFreq, startTime);
|
||
filter.Q.setValueAtTime(filterQ, startTime);
|
||
lastNode.connect(filter);
|
||
lastNode = filter;
|
||
this.activeNodes.push(filter);
|
||
}
|
||
|
||
lastNode.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
osc.start(startTime);
|
||
osc.stop(startTime + duration);
|
||
|
||
this.activeNodes.push(osc, env);
|
||
return osc;
|
||
}
|
||
|
||
_noise({ noiseType = 'pink', filterType = 'bandpass', startFreq = 800, endFreq = 800, Q = 1.5, startTime, duration, peakVol = 0.35 }) {
|
||
const ctx = this.am.ctx;
|
||
if (!ctx || !this.gainNode) return null;
|
||
const buf = this.am.createNoiseBuffer(noiseType, Math.max(3, Math.ceil(duration + 1)));
|
||
const src = ctx.createBufferSource();
|
||
src.buffer = buf;
|
||
|
||
const filter = ctx.createBiquadFilter();
|
||
filter.type = filterType;
|
||
filter.frequency.setValueAtTime(Math.max(20, startFreq), startTime);
|
||
if (endFreq && endFreq !== startFreq) {
|
||
filter.frequency.exponentialRampToValueAtTime(Math.max(20, endFreq), startTime + duration);
|
||
}
|
||
filter.Q.setValueAtTime(Q, startTime);
|
||
|
||
const env = ctx.createGain();
|
||
env.gain.setValueAtTime(0.0001, startTime);
|
||
const attack = Math.min(0.12, duration * 0.3);
|
||
env.gain.linearRampToValueAtTime(peakVol, startTime + attack);
|
||
env.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);
|
||
|
||
src.connect(filter);
|
||
filter.connect(env);
|
||
env.connect(this.gainNode);
|
||
|
||
src.start(startTime);
|
||
src.stop(startTime + duration);
|
||
|
||
this.activeNodes.push(src, filter, env);
|
||
return src;
|
||
}
|
||
|
||
_click(startTime, freq = 140, duration = 0.035, vol = 0.4, type = 'square') {
|
||
return this._tone({ type, startFreq: freq, endFreq: 25, startTime, duration, peakVol: vol, endVol: 0.0001 });
|
||
}
|
||
|
||
_sub(startTime, startFreq = 70, endFreq = 30, duration = 0.8, vol = 0.6) {
|
||
return this._tone({ type: 'sine', startFreq, endFreq, startTime, duration, peakVol: vol, endVol: 0.0001 });
|
||
}
|
||
|
||
_chime(startTime, pitches = [880, 1174, 1567], step = 0.05, duration = 0.35, vol = 0.25) {
|
||
pitches.forEach((freq, idx) => {
|
||
const t = startTime + idx * step;
|
||
this._tone({ type: 'sine', startFreq: freq, endFreq: freq * 1.01, startTime: t, duration, peakVol: vol, endVol: 0.0001 });
|
||
});
|
||
}
|
||
|
||
playStartup(profileKey = 'galaxy', duration = 2.5) {
|
||
this.init();
|
||
if (!this.am.ctx) return;
|
||
this.stopTransitions();
|
||
const now = this.am.ctx.currentTime;
|
||
const s = duration / 2.5;
|
||
|
||
switch (profileKey) {
|
||
case 'intrepid': this._startupIntrepid(now, s); break;
|
||
case 'defiant': this._startupDefiant(now, s); break;
|
||
case 'cardassian': this._startupCardassian(now, s); break;
|
||
case 'tos': this._startupTOS(now, s); break;
|
||
case 'nx': this._startupNX(now, s); break;
|
||
case 'tardis': this._startupTardis(now, s); break;
|
||
case 'industrial': this._startupIndustrial(now, s); break;
|
||
case 'bioship': this._startupBioship(now, s); break;
|
||
case 'retrofuture': this._startupRetrofuture(now, s); break;
|
||
case 'military': this._startupMilitary(now, s); break;
|
||
case 'deepspace': this._startupDeepSpace(now, s); break;
|
||
case 'outlaw': this._startupOutlaw(now, s); break;
|
||
case 'station': this._startupStation(now, s); break;
|
||
case 'comedy': this._startupComedy(now, s); break;
|
||
case 'galaxy':
|
||
default:
|
||
this._startupGalaxy(now, s);
|
||
break;
|
||
}
|
||
}
|
||
|
||
playShutdown(profileKey = 'galaxy', duration = 2.5) {
|
||
this.init();
|
||
if (!this.am.ctx) return;
|
||
this.stopTransitions();
|
||
const now = this.am.ctx.currentTime;
|
||
const s = duration / 2.5;
|
||
|
||
switch (profileKey) {
|
||
case 'intrepid': this._shutdownIntrepid(now, s); break;
|
||
case 'defiant': this._shutdownDefiant(now, s); break;
|
||
case 'cardassian': this._shutdownCardassian(now, s); break;
|
||
case 'tos': this._shutdownTOS(now, s); break;
|
||
case 'nx': this._shutdownNX(now, s); break;
|
||
case 'tardis': this._shutdownTardis(now, s); break;
|
||
case 'industrial': this._shutdownIndustrial(now, s); break;
|
||
case 'bioship': this._shutdownBioship(now, s); break;
|
||
case 'retrofuture': this._shutdownRetrofuture(now, s); break;
|
||
case 'military': this._shutdownMilitary(now, s); break;
|
||
case 'deepspace': this._shutdownDeepSpace(now, s); break;
|
||
case 'outlaw': this._shutdownOutlaw(now, s); break;
|
||
case 'station': this._shutdownStation(now, s); break;
|
||
case 'comedy': this._shutdownComedy(now, s); break;
|
||
case 'galaxy':
|
||
default:
|
||
this._shutdownGalaxy(now, s);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 1. TNG GALAXY CLASS
|
||
_startupGalaxy(now, s) {
|
||
this._click(now, 150, 0.04, 0.45);
|
||
this._click(now + 0.14 * s, 110, 0.03, 0.35);
|
||
this._tone({ type: 'sine', startFreq: 60, endFreq: 140, startTime: now + 0.05 * s, duration: 0.65 * s, peakVol: 0.35 });
|
||
this._tone({ type: 'sawtooth', startFreq: 120, endFreq: 480, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 750, filterQ: 3.5 });
|
||
this._sub(now + 0.85 * s, 65, 80, 0.95 * s, 0.55);
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 200, endFreq: 600, startTime: now + 0.9 * s, duration: 0.9 * s, peakVol: 0.25 });
|
||
this._chime(now + 1.8 * s, [784, 1046, 1318], 0.06 * s, 0.5 * s, 0.3);
|
||
this._sub(now + 1.8 * s, 80, 58, 0.65 * s, 0.45);
|
||
}
|
||
_shutdownGalaxy(now, s) {
|
||
this._click(now, 160, 0.05, 0.5);
|
||
this._sub(now, 85, 40, 0.65 * s, 0.6);
|
||
this._tone({ type: 'sawtooth', startFreq: 460, endFreq: 75, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 600, filterQ: 2.0 });
|
||
this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 650, endFreq: 180, startTime: now + 0.75 * s, duration: 1.0 * s, peakVol: 0.28 });
|
||
this._tone({ type: 'sine', startFreq: 75, endFreq: 24, startTime: now + 1.7 * s, duration: 0.75 * s, peakVol: 0.35 });
|
||
}
|
||
|
||
// 2. VOYAGER INTREPID CLASS
|
||
_startupIntrepid(now, s) {
|
||
this._click(now, 450, 0.025, 0.35);
|
||
this._click(now + 0.08 * s, 680, 0.025, 0.35);
|
||
this._tone({ type: 'triangle', startFreq: 240, endFreq: 580, startTime: now + 0.1 * s, duration: 0.6 * s, peakVol: 0.3 });
|
||
this._tone({ type: 'sawtooth', startFreq: 320, endFreq: 1650, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.35, filterType: 'bandpass', filterFreq: 1400, filterQ: 4.5 });
|
||
this._sub(now + 0.8 * s, 70, 95, 0.9 * s, 0.45);
|
||
this._noise({ noiseType: 'white', filterType: 'bandpass', startFreq: 1200, endFreq: 2400, startTime: now + 0.85 * s, duration: 0.9 * s, peakVol: 0.22 });
|
||
this._chime(now + 1.8 * s, [1174, 1567, 2093], 0.05 * s, 0.45 * s, 0.28);
|
||
this._tone({ type: 'sine', startFreq: 95, endFreq: 70, startTime: now + 1.85 * s, duration: 0.65 * s, peakVol: 0.4 });
|
||
}
|
||
_shutdownIntrepid(now, s) {
|
||
this._click(now, 520, 0.03, 0.4);
|
||
this._sub(now, 95, 50, 0.5 * s, 0.5);
|
||
this._tone({ type: 'sawtooth', startFreq: 1600, endFreq: 140, startTime: now + 0.5 * s, duration: 1.2 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 1200 });
|
||
this._noise({ noiseType: 'white', filterType: 'highpass', startFreq: 1400, endFreq: 400, startTime: now + 0.6 * s, duration: 0.8 * s, peakVol: 0.25 });
|
||
this._tone({ type: 'sine', startFreq: 140, endFreq: 30, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 });
|
||
}
|
||
|
||
// 3. DEFIANT ESCORT
|
||
_startupDefiant(now, s) {
|
||
this._click(now, 90, 0.06, 0.6, 'square');
|
||
this._tone({ type: 'sawtooth', startFreq: 50, endFreq: 120, startTime: now + 0.05 * s, duration: 0.6 * s, peakVol: 0.4, filterType: 'lowpass', filterFreq: 300 });
|
||
this._sub(now + 0.65 * s, 35, 95, 1.1 * s, 0.7);
|
||
this._tone({ type: 'sawtooth', startFreq: 180, endFreq: 740, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.32, filterType: 'bandpass', filterFreq: 550, filterQ: 3.0 });
|
||
this._chime(now + 1.8 * s, [660, 880], 0.08 * s, 0.4 * s, 0.35);
|
||
this._sub(now + 1.8 * s, 95, 68, 0.7 * s, 0.5);
|
||
}
|
||
_shutdownDefiant(now, s) {
|
||
this._click(now, 110, 0.05, 0.6);
|
||
this._sub(now, 90, 35, 0.6 * s, 0.65);
|
||
this._tone({ type: 'sawtooth', startFreq: 720, endFreq: 60, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.3, filterType: 'lowpass', filterFreq: 450 });
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 500, endFreq: 120, startTime: now + 0.7 * s, duration: 1.0 * s, peakVol: 0.3 });
|
||
this._tone({ type: 'sine', startFreq: 60, endFreq: 22, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.35 });
|
||
}
|
||
|
||
// 4. CARDASSIAN / DS9
|
||
_startupCardassian(now, s) {
|
||
this._click(now, 85, 0.08, 0.55);
|
||
this._tone({ type: 'triangle', startFreq: 165, endFreq: 110, startTime: now, duration: 0.7 * s, peakVol: 0.4 });
|
||
this._sub(now + 0.7 * s, 32, 68, 1.1 * s, 0.65);
|
||
this._tone({ type: 'sawtooth', startFreq: 80, endFreq: 260, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.3, filterType: 'lowpass', filterFreq: 320 });
|
||
this._noise({ noiseType: 'brown', filterType: 'lowpass', startFreq: 180, endFreq: 350, startTime: now + 0.8 * s, duration: 1.0 * s, peakVol: 0.3 });
|
||
this._chime(now + 1.8 * s, [330, 440], 0.1 * s, 0.5 * s, 0.35);
|
||
}
|
||
_shutdownCardassian(now, s) {
|
||
this._click(now, 95, 0.06, 0.5);
|
||
this._sub(now, 68, 30, 0.6 * s, 0.6);
|
||
this._tone({ type: 'sawtooth', startFreq: 250, endFreq: 40, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 220 });
|
||
this._noise({ noiseType: 'brown', filterType: 'bandpass', startFreq: 240, endFreq: 80, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.25 });
|
||
this._tone({ type: 'sine', startFreq: 40, endFreq: 20, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.3 });
|
||
}
|
||
|
||
// 5. TOS 1960s
|
||
_startupTOS(now, s) {
|
||
this._click(now, 220, 0.03, 0.45);
|
||
this._tone({ type: 'sine', startFreq: 80, endFreq: 180, startTime: now + 0.04 * s, duration: 0.6 * s, peakVol: 0.35 });
|
||
this._tone({ type: 'sine', startFreq: 220, endFreq: 980, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.32 });
|
||
this._tone({ type: 'triangle', startFreq: 225, endFreq: 990, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.2 });
|
||
this._chime(now + 1.8 * s, [880, 1108], 0.08 * s, 0.35 * s, 0.3);
|
||
}
|
||
_shutdownTOS(now, s) {
|
||
this._click(now, 260, 0.035, 0.45);
|
||
this._tone({ type: 'sine', startFreq: 950, endFreq: 90, startTime: now + 0.5 * s, duration: 1.2 * s, peakVol: 0.3 });
|
||
this._tone({ type: 'sine', startFreq: 2400, endFreq: 400, startTime: now + 0.6 * s, duration: 0.8 * s, peakVol: 0.15 });
|
||
this._tone({ type: 'sine', startFreq: 90, endFreq: 25, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 });
|
||
}
|
||
|
||
// 6. NX-01 PROTOTYPE
|
||
_startupNX(now, s) {
|
||
[0, 0.12, 0.24, 0.36, 0.48].forEach(dt => this._click(now + dt * s, 110, 0.04, 0.45));
|
||
this._tone({ type: 'sawtooth', startFreq: 95, endFreq: 420, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.32, filterType: 'lowpass', filterFreq: 600 });
|
||
this._sub(now + 0.8 * s, 45, 75, 1.0 * s, 0.6);
|
||
this._noise({ noiseType: 'brown', filterType: 'lowpass', startFreq: 220, endFreq: 480, startTime: now + 0.85 * s, duration: 0.95 * s, peakVol: 0.3 });
|
||
this._click(now + 1.8 * s, 180, 0.03, 0.4);
|
||
}
|
||
_shutdownNX(now, s) {
|
||
this._click(now, 95, 0.06, 0.55);
|
||
this._sub(now, 75, 35, 0.6 * s, 0.6);
|
||
[0.6, 0.85, 1.15, 1.5].forEach(dt => this._click(now + dt * s, 85, 0.04, 0.35));
|
||
this._noise({ noiseType: 'pink', filterType: 'highpass', startFreq: 900, endFreq: 300, startTime: now + 0.7 * s, duration: 1.0 * s, peakVol: 0.25 });
|
||
this._tone({ type: 'sine', startFreq: 40, endFreq: 18, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.3 });
|
||
}
|
||
|
||
// 7. TARDIS DIMENSIONAL
|
||
_startupTardis(now, s) {
|
||
this._click(now, 80, 0.06, 0.55);
|
||
this._tone({ type: 'sawtooth', startFreq: 140, endFreq: 680, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.32, filterType: 'bandpass', filterFreq: 550, filterQ: 3.0 });
|
||
this._tone({ type: 'sine', startFreq: 70, endFreq: 220, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.35 });
|
||
this._chime(now + 1.8 * s, [1046, 1318, 1567], 0.06 * s, 0.45 * s, 0.3);
|
||
}
|
||
_shutdownTardis(now, s) {
|
||
this._click(now, 90, 0.05, 0.5);
|
||
this._tone({ type: 'sawtooth', startFreq: 620, endFreq: 75, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.28, filterType: 'bandpass', filterFreq: 350, filterQ: 2.0 });
|
||
this._chime(now + 0.8 * s, [440], 0.1, 0.7 * s, 0.3);
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 350, endFreq: 80, startTime: now + 1.6 * s, duration: 0.9 * s, peakVol: 0.2 });
|
||
}
|
||
|
||
// 8. INDUSTRIAL FREIGHTER
|
||
_startupIndustrial(now, s) {
|
||
this._click(now, 75, 0.08, 0.6);
|
||
this._noise({ noiseType: 'white', filterType: 'highpass', startFreq: 1800, endFreq: 800, startTime: now + 0.05 * s, duration: 0.4 * s, peakVol: 0.35 });
|
||
this._tone({ type: 'sawtooth', startFreq: 55, endFreq: 290, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.35, filterType: 'lowpass', filterFreq: 400 });
|
||
this._sub(now + 0.7 * s, 35, 65, 1.1 * s, 0.7);
|
||
this._noise({ noiseType: 'brown', filterType: 'bandpass', startFreq: 400, endFreq: 900, startTime: now + 0.8 * s, duration: 1.0 * s, peakVol: 0.3 });
|
||
this._sub(now + 1.8 * s, 65, 40, 0.7 * s, 0.6);
|
||
}
|
||
_shutdownIndustrial(now, s) {
|
||
this._click(now, 85, 0.07, 0.65);
|
||
this._sub(now, 65, 28, 0.6 * s, 0.6);
|
||
this._noise({ noiseType: 'white', filterType: 'lowpass', startFreq: 2400, endFreq: 400, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.38 });
|
||
this._tone({ type: 'sawtooth', startFreq: 280, endFreq: 40, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 300 });
|
||
[1.7, 1.95, 2.2].forEach(dt => this._click(now + dt * s, 320, 0.02, 0.25));
|
||
}
|
||
|
||
// 9. BIOSHIP
|
||
_startupBioship(now, s) {
|
||
this._chime(now, [784, 1174, 1567], 0.04 * s, 0.3 * s, 0.25);
|
||
this._sub(now + 0.6 * s, 42, 28, 0.4 * s, 0.65);
|
||
this._sub(now + 0.95 * s, 44, 28, 0.4 * s, 0.7);
|
||
this._tone({ type: 'sine', startFreq: 260, endFreq: 540, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.32 });
|
||
this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 250, endFreq: 600, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.3 });
|
||
}
|
||
_shutdownBioship(now, s) {
|
||
this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 550, endFreq: 180, startTime: now, duration: 1.1 * s, peakVol: 0.35 });
|
||
this._sub(now + 0.5 * s, 38, 25, 0.4 * s, 0.55);
|
||
this._sub(now + 1.1 * s, 34, 22, 0.4 * s, 0.45);
|
||
this._tone({ type: 'sine', startFreq: 480, endFreq: 180, startTime: now + 0.6 * s, duration: 1.1 * s, peakVol: 0.22 });
|
||
this._tone({ type: 'sine', startFreq: 35, endFreq: 18, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 });
|
||
}
|
||
|
||
// 10. RETROFUTURE
|
||
_startupRetrofuture(now, s) {
|
||
[0, 0.07, 0.14, 0.21].forEach(dt => this._click(now + dt * s, 380, 0.02, 0.3));
|
||
this._tone({ type: 'triangle', startFreq: 95, endFreq: 580, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.35, filterType: 'lowpass', filterFreq: 650 });
|
||
this._tone({ type: 'sine', startFreq: 100, endFreq: 590, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.25 });
|
||
this._chime(now + 1.8 * s, [920], 0.1, 0.5 * s, 0.35);
|
||
}
|
||
_shutdownRetrofuture(now, s) {
|
||
this._click(now, 140, 0.05, 0.55);
|
||
this._tone({ type: 'triangle', startFreq: 550, endFreq: 65, startTime: now + 0.5 * s, duration: 1.2 * s, peakVol: 0.28 });
|
||
this._noise({ noiseType: 'pink', filterType: 'bandpass', startFreq: 450, endFreq: 150, startTime: now + 0.6 * s, duration: 1.1 * s, peakVol: 0.22 });
|
||
this._tone({ type: 'sine', startFreq: 3200, endFreq: 120, startTime: now + 1.7 * s, duration: 0.7 * s, peakVol: 0.2 });
|
||
}
|
||
|
||
// 11. MILITARY
|
||
_startupMilitary(now, s) {
|
||
this._click(now, 120, 0.06, 0.6);
|
||
this._tone({ type: 'square', startFreq: 60, endFreq: 120, startTime: now, duration: 0.6 * s, peakVol: 0.35, filterType: 'lowpass', filterFreq: 250 });
|
||
this._tone({ type: 'sawtooth', startFreq: 140, endFreq: 880, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.35, filterType: 'bandpass', filterFreq: 750, filterQ: 3.0 });
|
||
this._sub(now + 0.7 * s, 40, 85, 1.1 * s, 0.65);
|
||
this._chime(now + 1.8 * s, [750, 750], 0.08 * s, 0.35 * s, 0.35);
|
||
}
|
||
_shutdownMilitary(now, s) {
|
||
this._click(now, 150, 0.04, 0.5);
|
||
this._chime(now + 0.05 * s, [880], 0.1, 0.3 * s, 0.35);
|
||
this._tone({ type: 'sawtooth', startFreq: 850, endFreq: 70, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 500 });
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 600, endFreq: 140, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.28 });
|
||
this._sub(now + 1.7 * s, 70, 25, 0.8 * s, 0.4);
|
||
}
|
||
|
||
// 12. DEEP SPACE
|
||
_startupDeepSpace(now, s) {
|
||
this._noise({ noiseType: 'white', filterType: 'bandpass', startFreq: 2400, endFreq: 1200, startTime: now, duration: 0.65 * s, peakVol: 0.32 });
|
||
this._sub(now + 0.7 * s, 28, 68, 1.1 * s, 0.7);
|
||
this._tone({ type: 'sine', startFreq: 110, endFreq: 280, startTime: now + 0.75 * s, duration: 1.1 * s, peakVol: 0.3 });
|
||
this._chime(now + 1.8 * s, [440, 554], 0.1 * s, 0.5 * s, 0.25);
|
||
}
|
||
_shutdownDeepSpace(now, s) {
|
||
this._sub(now, 68, 25, 0.7 * s, 0.65);
|
||
this._tone({ type: 'sine', startFreq: 260, endFreq: 50, startTime: now + 0.6 * s, duration: 1.2 * s, peakVol: 0.25 });
|
||
this._noise({ noiseType: 'brown', filterType: 'lowpass', startFreq: 200, endFreq: 40, startTime: now + 0.7 * s, duration: 1.1 * s, peakVol: 0.3 });
|
||
this._sub(now + 1.7 * s, 35, 15, 0.8 * s, 0.25);
|
||
}
|
||
|
||
// 13. OUTLAW
|
||
_startupOutlaw(now, s) {
|
||
[0, 0.1, 0.22, 0.32, 0.44].forEach(dt => this._click(now + dt * s, 130 + Math.random() * 40, 0.035, 0.4));
|
||
this._tone({ type: 'sawtooth', startFreq: 70, endFreq: 130, startTime: now + 0.1 * s, duration: 0.5 * s, peakVol: 0.3, filterType: 'lowpass', filterFreq: 280 });
|
||
this._tone({ type: 'sawtooth', startFreq: 180, endFreq: 1150, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.35, filterType: 'bandpass', filterFreq: 900, filterQ: 4.0 });
|
||
this._sub(now + 0.7 * s, 45, 80, 1.1 * s, 0.65);
|
||
this._noise({ noiseType: 'white', filterType: 'highpass', startFreq: 2800, endFreq: 1500, startTime: now + 1.8 * s, duration: 0.3 * s, peakVol: 0.35 });
|
||
}
|
||
_shutdownOutlaw(now, s) {
|
||
this._click(now, 70, 0.08, 0.7, 'sawtooth');
|
||
this._sub(now, 90, 40, 0.4 * s, 0.7);
|
||
[0.5, 0.8, 1.15, 1.55].forEach(dt => this._click(now + dt * s, 90, 0.04, 0.35));
|
||
this._tone({ type: 'sawtooth', startFreq: 750, endFreq: 55, startTime: now + 0.5 * s, duration: 1.3 * s, peakVol: 0.25, filterType: 'lowpass', filterFreq: 350 });
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 350, endFreq: 80, startTime: now + 1.7 * s, duration: 0.8 * s, peakVol: 0.25 });
|
||
}
|
||
|
||
// 14. STATION
|
||
_startupStation(now, s) {
|
||
this._chime(now, [587, 880], 0.08 * s, 0.35 * s, 0.3);
|
||
this._click(now + 0.15 * s, 110, 0.05, 0.45);
|
||
this._sub(now + 0.6 * s, 30, 65, 1.2 * s, 0.7);
|
||
this._tone({ type: 'sawtooth', startFreq: 70, endFreq: 240, startTime: now + 0.65 * s, duration: 1.15 * s, peakVol: 0.28, filterType: 'lowpass', filterFreq: 280 });
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 200, endFreq: 500, startTime: now + 0.8 * s, duration: 1.0 * s, peakVol: 0.25 });
|
||
this._click(now + 1.8 * s, 85, 0.06, 0.5);
|
||
}
|
||
_shutdownStation(now, s) {
|
||
this._click(now, 105, 0.05, 0.5);
|
||
this._sub(now + 0.5 * s, 65, 25, 1.4 * s, 0.55);
|
||
this._tone({ type: 'sawtooth', startFreq: 220, endFreq: 45, startTime: now + 0.55 * s, duration: 1.3 * s, peakVol: 0.22, filterType: 'lowpass', filterFreq: 200 });
|
||
this._noise({ noiseType: 'pink', filterType: 'lowpass', startFreq: 280, endFreq: 90, startTime: now + 1.6 * s, duration: 0.9 * s, peakVol: 0.2 });
|
||
}
|
||
|
||
// 15. COMEDY
|
||
_startupComedy(now, s) {
|
||
this._chime(now, [330, 440, 554, 659], 0.07 * s, 0.35 * s, 0.32);
|
||
this._tone({ type: 'sawtooth', startFreq: 110, endFreq: 1400, startTime: now + 0.55 * s, duration: 1.25 * s, peakVol: 0.3, filterType: 'bandpass', filterFreq: 800, filterQ: 3.5 });
|
||
this._sub(now + 0.7 * s, 40, 75, 1.1 * s, 0.5);
|
||
this._chime(now + 1.8 * s, [1200], 0.1, 0.5 * s, 0.4);
|
||
}
|
||
_shutdownComedy(now, s) {
|
||
this._tone({ type: 'sine', startFreq: 920, endFreq: 140, startTime: now, duration: 0.85 * s, peakVol: 0.32 });
|
||
[0.7, 0.82, 0.96, 1.12].forEach(dt => this._click(now + dt * s, 420 + Math.random() * 200, 0.025, 0.3));
|
||
this._noise({ noiseType: 'white', filterType: 'bandpass', startFreq: 300, endFreq: 100, startTime: now + 1.6 * s, duration: 0.6 * s, peakVol: 0.25 });
|
||
}
|
||
}
|
||
|
||
window.EngineTransitionSynth = EngineTransitionSynth;
|
||
|
||
|