Implement Phase 3 audio gaps: continuous beds (zocalo chatter, teletype chatter, heterodyne drone)
This commit is contained in:
@@ -301,6 +301,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (presetId === 'bio-vorlon') expandedAudio.startVorlonSong(); // #26
|
||||
if (presetId === 'bio-wraith') expandedAudio.startHullRegen(); // #28
|
||||
if (presetId === 'deep-event-horizon' || presetId === 'deep-icarus-ii') expandedAudio.startHullStrainMoans(); // #30
|
||||
// Phase 3 (audio_gaps.md #46-#54)
|
||||
if (presetId === 'sta-babylon-5') expandedAudio.startZocaloChatter(); // #46
|
||||
if (presetId === 'ind-nostromo') expandedAudio.startTeletypeChatter(); // #51
|
||||
if (presetId === 'ret-jupiter-2') expandedAudio.startHeterodyneDrone(); // #54
|
||||
if (presetId === 'deep-icarus-ii') expandedAudio.startIcarusBeacon(); // #31
|
||||
if (presetId === 'deep-event-horizon' || presetId === 'deep-nightflyer') expandedAudio.startVoidWhispers(); // #32
|
||||
if (presetId === 'com-protector') expandedAudio.startBerylliumThrum(); // #33
|
||||
@@ -935,6 +939,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'btn-teletype':
|
||||
expandedAudio.synthesizeTeletypeChatterPhrase();
|
||||
break;
|
||||
case 'btn-zocalo-chatter':
|
||||
if (expandedAudio.zocaloChatterActive) expandedAudio.stopZocaloChatter();
|
||||
else expandedAudio.startZocaloChatter();
|
||||
break;
|
||||
case 'btn-hetero-drone':
|
||||
if (expandedAudio.heterodyneDroneActive) expandedAudio.stopHeterodyneDrone();
|
||||
else expandedAudio.startHeterodyneDrone();
|
||||
break;
|
||||
|
||||
default:
|
||||
telemetry.synthesizeLCARSSingleChirp();
|
||||
|
||||
+202
@@ -5719,9 +5719,211 @@ class ExpandedSciFiAudioSynth {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
|
||||
+3
-1
@@ -510,7 +510,8 @@ const UniverseRegistry = {
|
||||
{ id: 'btn-chirp-single', label: 'ASTROGATOR BEAT' },
|
||||
{ id: 'btn-chirp-sweep', label: 'ANALOG GLISSANDO' },
|
||||
{ id: 'btn-relay-click', label: 'TAPE REEL SPOOL' },
|
||||
{ id: 'btn-tape-spooler', label: 'DUAL-REEL TAPE SPOOLER' }
|
||||
{ id: 'btn-tape-spooler', label: 'DUAL-REEL TAPE SPOOLER' },
|
||||
{ id: 'btn-hetero-drone', label: 'HETERO-DRONE TONE' }
|
||||
],
|
||||
presets: {
|
||||
'ret-jupiter-2': {
|
||||
@@ -903,6 +904,7 @@ const UniverseRegistry = {
|
||||
{ id: 'btn-commlock', label: 'COMMLOCK CALLING TONE' },
|
||||
{ id: 'btn-sevas-spark', label: 'CONDUIT SPARK ARC' },
|
||||
{ id: 'btn-station-ecg', label: 'CLINICAL VITAL (ECG)' },
|
||||
{ id: 'btn-zocalo-chatter', label: 'ZOCALO PLAZA CHATTER' },
|
||||
{ id: 'btn-belter-telemetry', label: 'BELTER JURY-RIG TELEMETRY', span: 2 }
|
||||
],
|
||||
presets: {
|
||||
|
||||
Reference in New Issue
Block a user