summaryrefslogtreecommitdiff
path: root/internal/generator/templates/shared/shared.js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-26 12:25:14 +0300
committerPaul Buetow <paul@buetow.org>2026-04-26 12:25:14 +0300
commitb99f2c6f8f426e97dea97ef2938fc3312690a0fe (patch)
tree656c673715bb0c4bebcc3ceea3501dec8f566b21 /internal/generator/templates/shared/shared.js
parente9bddae89614f12ce7d2d26e70c2c2de3b90590e (diff)
audio: make ambient music polyphonic with real songs, fix engine silence
Rewrite all 19 theme ambient presets with chord progressions, bass drones, overlapping chord pads (dur > step on melodyNote), and swing melodies. Add per-theme 16-step drum patterns (kick, snare, hat, clap) synthesized in the Web Audio engine. Go (theme_sounds.go): - Add Step field to melodyNote for sequencer timing independent of duration - Add Drums []string field to ambientPreset for 16-step patterns - Add buildMeasure, buildSong, buildSwingMelody, and pat helpers - Rewrite every sounds*() func with I–IV–V–I progressions, bass, and melody JS (shared.js): - Fix startEngine() to await ctx.resume() before scheduling - Make masterGain a binary gate (fade to 1.0 in ~50 ms), not preset.gain - Fix startDrones() to scale by preset.gain instead of 1.0/numDrones - Add twin-oscillator detuned timbre in playMelodyNote() - Add synthesized drum engine: playDrum(), scheduleDrums(), stopDrums() - Keep splash sounds bypassing masterGain so they never get gated out
Diffstat (limited to 'internal/generator/templates/shared/shared.js')
-rw-r--r--internal/generator/templates/shared/shared.js115
1 files changed, 115 insertions, 0 deletions
diff --git a/internal/generator/templates/shared/shared.js b/internal/generator/templates/shared/shared.js
index 7f6192a..70b97e0 100644
--- a/internal/generator/templates/shared/shared.js
+++ b/internal/generator/templates/shared/shared.js
@@ -387,6 +387,8 @@
var currentPreset = null;
var melodyTimer = null;
var melodyIndex = 0;
+ var drumTimer = null;
+ var drumIndex = 0;
function wildifyPreset(base) {
if (!base) return base;
@@ -645,10 +647,122 @@
clearTimeout(pulseTimer);
pulseTimer = null;
stopMelody();
+ stopDrums();
stopDrones();
stopNoise();
}
+ // ── drum synthesizer ──────────────────────────────────────────────
+
+ function stopDrums() {
+ if (drumTimer) { clearTimeout(drumTimer); drumTimer = null; }
+ drumIndex = 0;
+ }
+
+ function playDrum(type, preset) {
+ try {
+ var c = ctx;
+ if (!c) return;
+ var now = c.currentTime;
+ if (type === 'kick') {
+ var gBase = preset.gain != null ? preset.gain : 0.08;
+ var osc = c.createOscillator();
+ var g = c.createGain();
+ osc.connect(g);
+ g.connect(masterGain);
+ osc.frequency.setValueAtTime(150, now);
+ osc.frequency.exponentialRampToValueAtTime(40, now + 0.12);
+ g.gain.setValueAtTime(Math.min(gBase * 5, 0.6), now);
+ g.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
+ osc.start(now);
+ osc.stop(now + 0.13);
+ } else if (type === 'snare') {
+ var gBase = preset.gain != null ? preset.gain : 0.08;
+ var osc = c.createOscillator();
+ var g1 = c.createGain();
+ osc.type = 'triangle';
+ osc.frequency.setValueAtTime(250, now);
+ osc.connect(g1);
+ g1.connect(masterGain);
+ g1.gain.setValueAtTime(Math.min(gBase * 3, 0.4), now);
+ g1.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
+ osc.start(now);
+ osc.stop(now + 0.11);
+ // snap noise burst
+ var ns = c.createBufferSource();
+ var bs = 2 * c.sampleRate;
+ var buf = c.createBuffer(1, bs, c.sampleRate);
+ var d = buf.getChannelData(0);
+ for (var i = 0; i < bs; i++) { d[i] = Math.random() * 2 - 1; }
+ ns.buffer = buf;
+ var g2 = c.createGain();
+ var bp = c.createBiquadFilter();
+ bp.type = 'bandpass';
+ bp.frequency.value = 2000;
+ bp.Q.value = 1;
+ ns.connect(bp);
+ bp.connect(g2);
+ g2.connect(masterGain);
+ g2.gain.setValueAtTime(Math.min(gBase * 5, 0.6), now);
+ g2.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
+ ns.start(now);
+ ns.stop(now + 0.09);
+ } else if (type === 'hat') {
+ var gBase = preset.gain != null ? preset.gain : 0.08;
+ var ns = c.createBufferSource();
+ var bs = 2 * c.sampleRate;
+ var buf = c.createBuffer(1, bs, c.sampleRate);
+ var d = buf.getChannelData(0);
+ for (var i = 0; i < bs; i++) { d[i] = Math.random() * 2 - 1; }
+ ns.buffer = buf;
+ var g2 = c.createGain();
+ var hp = c.createBiquadFilter();
+ hp.type = 'highpass';
+ hp.frequency.value = 7000;
+ ns.connect(hp);
+ hp.connect(g2);
+ g2.connect(masterGain);
+ g2.gain.setValueAtTime(Math.min(gBase * 2, 0.25), now);
+ g2.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
+ ns.start(now);
+ ns.stop(now + 0.05);
+ } else if (type === 'clap') {
+ var gBase = preset.gain != null ? preset.gain : 0.08;
+ // Reverb-y broadband snap
+ var ns = c.createBufferSource();
+ var bs = c.sampleRate * 0.15;
+ var buf = c.createBuffer(1, bs, c.sampleRate);
+ var d = buf.getChannelData(0);
+ for (var i = 0; i < bs; i++) { d[i] = Math.random() * 2 - 1; }
+ ns.buffer = buf;
+ var g2 = c.createGain();
+ g2.gain.setValueAtTime(Math.min(gBase * 3, 0.35), now);
+ g2.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
+ ns.connect(g2);
+ g2.connect(masterGain);
+ ns.start(now);
+ ns.stop(now + 0.16);
+ }
+ } catch (_) {}
+ }
+
+ function scheduleDrums(preset, firstDelayMs) {
+ if (!isPlaying || !preset || !preset.drums || preset.drums.length === 0) return;
+ var delay = firstDelayMs != null ? firstDelayMs : 0;
+ drumTimer = setTimeout(function() {
+ if (!isPlaying) return;
+ var beat = preset.drums[drumIndex];
+ if (beat !== '_') {
+ playDrum(beat, preset);
+ }
+ drumIndex = (drumIndex + 1) % preset.drums.length;
+ var beatDur = preset.bpm ? (60000.0 / preset.bpm / 4.0) : 250.0; // 1 beat = 1/16th note at BPM
+ scheduleDrums(preset, beatDur);
+ }, delay);
+ }
+
+ // ── drum synthesizer end ────────────────────────────────────────
+
function startEngine() {
var preset = getPreset();
if (!preset) return;
@@ -669,6 +783,7 @@
startDrones(preset);
startNoise(preset);
schedulePulse();
+ scheduleDrums(preset, 0);
// masterGain is a binary gate: open it quickly (50 ms) so the
// first scheduled notes are not swallowed by a long ramp.
// preset.attack controls per-note envelopes, not the gate.