summaryrefslogtreecommitdiff
path: root/internal/generator/templates/shared/shared.js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-26 09:33:29 +0300
committerPaul Buetow <paul@buetow.org>2026-04-26 09:33:29 +0300
commit44f82e79f0b1a65d9df62c3fed7271affcb0a673 (patch)
tree4a41b3065e0d3f39cc8135ed925c216107312de4 /internal/generator/templates/shared/shared.js
parenteebb1df14b26b65493702c090d13e1a9c7563db6 (diff)
Add unique ambient melodies for all 19 themes
Each theme now has a distinctive looping melodic phrase (normal + wild variants) played by the Web Audio ambient engine. This replaces the generic random-pulse behavior that made all themes sound the same. Go changes: - Add melodyNote struct with freq/dur/gain fields to ambientPreset - Add n() helper for concise melody construction - Rewrite all 19 theme presets with unique melodic phrases: neon=glassy arpeggio, terminal=sparse CRT beeps, synthwave=warm minor, plasma=tritone shimmer, brutalist=concrete rumble, volcano=phrygian, aurora=high shimmer, matrix=digital ostinato, ocean=wave flow, dos=8-bit arpeggio, retro=VHS warmth, cosmos=vast fifths, retrofuture=lounge minor-7, spaceage=pure ascending, tropicale=pentatonic, noir=smoky minor-7, cathedral=organ thirds, surveillance=fourth alert, biomech=diminished unease JS changes: - Add melodyTimer/melodyIndex state to ambient engine - Add playMelodyNote() with per-note gain and filter support - Add scheduleMelodyNote() with phrase-rest gaps - schedulePulse() now routes to melody player when melody is present - Add sawtooth support to snonuxWaveType() - Reset melody index on startEngine() and stopAll() Test changes: - TestThemeSoundPresetsAmbientPopulated now accepts melody as valid content - TestThemeSoundPresetsAmbientValuesBounded validates melody note freq/dur
Diffstat (limited to 'internal/generator/templates/shared/shared.js')
-rw-r--r--internal/generator/templates/shared/shared.js65
1 files changed, 65 insertions, 0 deletions
diff --git a/internal/generator/templates/shared/shared.js b/internal/generator/templates/shared/shared.js
index 6391d6c..2f4c260 100644
--- a/internal/generator/templates/shared/shared.js
+++ b/internal/generator/templates/shared/shared.js
@@ -366,6 +366,7 @@
function snonuxWaveType(w) {
if (w === 'square') return 'square';
if (w === 'triangle') return 'triangle';
+ if (w === 'sawtooth') return 'sawtooth';
return 'sine';
}
@@ -384,6 +385,8 @@
var isPlaying = false;
var isWild = false;
var currentPreset = null;
+ var melodyTimer = null;
+ var melodyIndex = 0;
function wildifyPreset(base) {
if (!base) return base;
@@ -475,9 +478,69 @@
noiseSrc = src;
}
+ function stopMelody() {
+ if (melodyTimer) { clearTimeout(melodyTimer); melodyTimer = null; }
+ melodyIndex = 0;
+ }
+
+ function playMelodyNote(note, preset) {
+ var c = ensureCtx();
+ var freq = note.freq || 440;
+ var dur = note.dur || 0.3;
+ if (freq <= 0 || dur <= 0) return;
+ var wt = snonuxWaveType(preset.wave);
+ var g = note.gain != null ? note.gain : (preset.gain != null ? preset.gain : 0.08);
+ var pulseGain = Math.min(g * 0.6, 0.12);
+
+ var osc = c.createOscillator();
+ var gain = c.createGain();
+ osc.type = wt;
+ osc.frequency.value = freq;
+
+ var lastNode = osc;
+ if (preset.filterFreq) {
+ var filter = c.createBiquadFilter();
+ filter.type = 'lowpass';
+ filter.frequency.value = preset.filterFreq;
+ filter.Q.value = preset.filterQ || 1;
+ var now = c.currentTime;
+ filter.frequency.setValueAtTime(preset.filterFreq, now);
+ filter.frequency.exponentialRampToValueAtTime(Math.max(preset.filterFreq * 0.2, 100), now + dur);
+ lastNode.connect(filter);
+ lastNode = filter;
+ }
+
+ lastNode.connect(gain);
+ gain.connect(masterGain);
+ var now = c.currentTime;
+ gain.gain.setValueAtTime(0, now);
+ gain.gain.linearRampToValueAtTime(pulseGain, now + Math.min(0.05, dur * 0.2));
+ gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
+ osc.start(now);
+ osc.stop(now + dur + 0.02);
+ }
+
+ function scheduleMelodyNote(preset) {
+ if (!isPlaying || !preset || !preset.melody || preset.melody.length === 0) return;
+ var note = preset.melody[melodyIndex];
+ playMelodyNote(note, preset);
+ melodyIndex = (melodyIndex + 1) % preset.melody.length;
+ var gap = preset.pulseInterval || (preset.bpm ? 60.0 / preset.bpm : 0.5);
+ var isEndOfPhrase = melodyIndex === 0;
+ var delay = note.dur * 1000 * 0.7 + (isEndOfPhrase ? gap * 1000 * 1.5 : gap * 1000 * 0.2);
+ melodyTimer = setTimeout(function() {
+ if (!isPlaying) return;
+ scheduleMelodyNote(currentPreset);
+ }, delay);
+ }
+
function schedulePulse() {
if (!isPlaying || !currentPreset) return;
var preset = currentPreset;
+ if (preset.melody && preset.melody.length > 0) {
+ scheduleMelodyNote(preset);
+ return;
+ }
var interval = preset.pulseInterval;
if (!interval && preset.bpm) {
interval = 60.0 / preset.bpm;
@@ -542,6 +605,7 @@
function stopAll() {
clearTimeout(pulseTimer);
pulseTimer = null;
+ stopMelody();
stopDrones();
stopNoise();
}
@@ -552,6 +616,7 @@
currentPreset = preset;
ensureCtx();
isPlaying = true;
+ melodyIndex = 0;
startDrones(preset);
startNoise(preset);