summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-26 11:47:55 +0300
committerPaul Buetow <paul@buetow.org>2026-04-26 11:47:55 +0300
commite9bddae89614f12ce7d2d26e70c2c2de3b90590e (patch)
treef74bccce3e0c7a4240a680deee82312acd286820
parent44f82e79f0b1a65d9df62c3fed7271affcb0a673 (diff)
ambient: polyphonic songs, fix cascaded gain and resume bugs
Rewrite all 19 theme melodies into proper ~10-16 second looping songs with bass drones, chord pads, and scalar melody lines using the new 'step' field for polyphonic overlap on a monophonic step sequencer. Fix four bugs that caused total silence: 1. masterGain was faded to preset.gain while each note was ALSO scaled by preset.gain*0.6, squaring volume to ~-68 dB. 2. fadeMasterTo ramped the gate for 0.5-1.5s, swallowing first notes. 3. startDrones set per-osc gain to 1.0/numDrones, clipping and burying the melody. 4. AudioContext resume was not awaited; notes fired into a suspended context and were lost. Shared.js: - startEngine() waits for ctx.resume() before scheduling anything. - masterGain acts as a binary gate (fades to 1.0 over 50ms). - playMelodyNote uses twin detuned oscillators for richer timbre. - startDrones scales by preset.gain so drones match melody level. Theme_sounds.go: - Add 'step' field to melodyNote struct. - Add chord-building helpers (major/minor triads). - Add buildSong() / buildMeasure() for structured 4-bar loops. - All 19 themes: audible bass (130-220Hz), chord pads (260-520Hz), melodies (330-1046Hz), real chord progressions, proper BPM.
-rw-r--r--internal/generator/templates/shared/shared.js105
-rw-r--r--internal/generator/theme_sounds.go900
2 files changed, 753 insertions, 252 deletions
diff --git a/internal/generator/templates/shared/shared.js b/internal/generator/templates/shared/shared.js
index 2f4c260..7f6192a 100644
--- a/internal/generator/templates/shared/shared.js
+++ b/internal/generator/templates/shared/shared.js
@@ -438,7 +438,11 @@
if (freqs.length === 0) return;
var wt = snonuxWaveType(preset.wave);
var detune = preset.detuneCents || 0;
- var perOscGain = 1.0 / Math.max(1, freqs.length);
+ // Drones must respect the theme's ambient gain so they don't
+ // drown the melody. Four drones at 0.25 each = 1.0 total,
+ // which clips and buries everything else.
+ var gBase = preset.gain != null ? preset.gain : 0.08;
+ var perOscGain = gBase / Math.max(1, freqs.length);
freqs.forEach(function(freq) {
var osc = c.createOscillator();
var g = c.createGain();
@@ -471,6 +475,7 @@
src.buffer = buffer;
src.loop = true;
noiseGainNode = c.createGain();
+ // Keep noise subtle so it doesn't bury the melody.
noiseGainNode.gain.value = preset.noiseGain;
src.connect(noiseGainNode);
noiseGainNode.connect(masterGain);
@@ -489,15 +494,30 @@
var dur = note.dur || 0.3;
if (freq <= 0 || dur <= 0) return;
var wt = snonuxWaveType(preset.wave);
+ // masterGain is now a binary "gate" (fades to 1 when on), so per-note
+ // volume carries the real preset.gain. Without the old masterGain
+ // squaring, this makes music audible at normal levels.
var g = note.gain != null ? note.gain : (preset.gain != null ? preset.gain : 0.08);
- var pulseGain = Math.min(g * 0.6, 0.12);
+ var pulseGain = Math.min(g, 0.08);
- var osc = c.createOscillator();
+ // Build a richer timbre with two slightly detuned oscillators so
+ // melody lines sound like actual notes instead of sterile blips.
+ var osc1 = c.createOscillator();
+ var osc2 = c.createOscillator();
+ var mix = c.createGain();
var gain = c.createGain();
- osc.type = wt;
- osc.frequency.value = freq;
- var lastNode = osc;
+ osc1.type = wt;
+ osc1.frequency.value = freq;
+ osc2.type = wt;
+ osc2.frequency.value = freq;
+ osc2.detune.value = 5 + Math.random() * 3; // subtle chorus
+
+ mix.gain.value = 0.5;
+ osc1.connect(mix);
+ osc2.connect(mix);
+
+ var lastNode = mix;
if (preset.filterFreq) {
var filter = c.createBiquadFilter();
filter.type = 'lowpass';
@@ -513,11 +533,22 @@
lastNode.connect(gain);
gain.connect(masterGain);
var now = c.currentTime;
+
+ // Proper ADSR-ish envelope: attack up, sustain, then release.
+ var attack = Math.min(0.04, dur * 0.15);
+ var release = Math.min(0.12, dur * 0.35);
+ var sustainTime = Math.max(0, dur - attack - release);
+
gain.gain.setValueAtTime(0, now);
- gain.gain.linearRampToValueAtTime(pulseGain, now + Math.min(0.05, dur * 0.2));
+ gain.gain.linearRampToValueAtTime(pulseGain, now + attack);
+ if (sustainTime > 0) {
+ gain.gain.setValueAtTime(pulseGain, now + attack + sustainTime);
+ }
gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
- osc.start(now);
- osc.stop(now + dur + 0.02);
+ osc1.start(now);
+ osc1.stop(now + dur + 0.02);
+ osc2.start(now);
+ osc2.stop(now + dur + 0.02);
}
function scheduleMelodyNote(preset) {
@@ -525,13 +556,21 @@
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);
+ // Timing to the next note: explicit 'step' on the current note
+ // controls exactly when the sequencer advances. If step is
+ // omitted the scheduler falls back to dur + a tiny BPM-scaled
+ // gap so the line stays intelligible.
+ var step;
+ if (note.step != null && note.step > 0) {
+ step = note.step;
+ } else {
+ var beat = preset.bpm ? 60.0 / preset.bpm : 0.5;
+ step = note.dur + beat * 0.15;
+ }
melodyTimer = setTimeout(function() {
if (!isPlaying) return;
scheduleMelodyNote(currentPreset);
- }, delay);
+ }, step * 1000);
}
function schedulePulse() {
@@ -615,16 +654,38 @@
if (!preset) return;
currentPreset = preset;
ensureCtx();
- isPlaying = true;
- melodyIndex = 0;
- startDrones(preset);
- startNoise(preset);
- schedulePulse();
+ // Begin scheduling only once the AudioContext is running.
+ // Modern browsers start AudioContext in 'suspended' state; firing
+ // note events before it is running schedules them into the void.
+ function begin() {
+ if (!isPlaying) return; // user toggled off while awaiting resume
+ // Audio must actually be running or all scheduled notes are lost
+ if (ctx.state !== 'running') {
+ isPlaying = false;
+ return;
+ }
+ melodyIndex = 0;
+ startDrones(preset);
+ startNoise(preset);
+ schedulePulse();
+ // 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.
+ fadeMasterTo(1.0, 0.05);
+ }
- var targetGain = preset.gain != null ? preset.gain : 0.08;
- var fadeIn = preset.attack != null ? preset.attack : 0.5;
- fadeMasterTo(targetGain, fadeIn);
+ if (ctx.state === 'suspended') {
+ isPlaying = true; // flag the intent so begin() can run later
+ ctx.resume().then(begin).catch(function() {
+ // Resume failed (no autoplay permission). Reset state so
+ // the next keypress triggers a fresh start attempt.
+ isPlaying = false;
+ });
+ } else {
+ isPlaying = true;
+ begin();
+ }
}
function pauseEngine() {
@@ -668,7 +729,7 @@
startDrones(newPreset);
startNoise(newPreset);
schedulePulse();
- var targetGain = newPreset.gain != null ? newPreset.gain : 0.08;
+ var targetGain = 1.0;
fadeMasterTo(targetGain, 0.5);
}, 350);
}
diff --git a/internal/generator/theme_sounds.go b/internal/generator/theme_sounds.go
index c3235dd..045c06a 100644
--- a/internal/generator/theme_sounds.go
+++ b/internal/generator/theme_sounds.go
@@ -3,18 +3,18 @@ package generator
import (
"encoding/json"
"html/template"
+ "math"
)
// melodyNote is a single note in a looping ambient melody.
type melodyNote struct {
Freq float64 `json:"freq"`
Dur float64 `json:"dur"`
+ Step float64 `json:"step,omitempty"`
Gain float64 `json:"gain,omitempty"`
}
// ambientPreset describes a generative ambient background layer for a theme.
-// All fields are optional at runtime; missing values should be treated as
-// silence or safe defaults by the consumer.
type ambientPreset struct {
BPM float64 `json:"bpm,omitempty"`
PulseInterval float64 `json:"pulseInterval,omitempty"`
@@ -77,12 +77,111 @@ type themeSounds struct {
Ambient ambientSounds `json:"ambient,omitempty"`
}
-// n is a concise helper for building a melodyNote.
+// ── helpers ────────────────────────────────────────────────────────
+
func n(freq, dur float64) melodyNote {
return melodyNote{Freq: freq, Dur: dur}
}
-// themeSoundPresets maps CLI theme names to synth parameters (see themes.go registry).
+// ns builds a note that rings for 'dur' but advances the sequencer by 'step'.
+// When dur > step, the note overlaps the next one — creating chords on a
+// monophonic step sequencer.
+func ns(freq, dur, step float64) melodyNote {
+ return melodyNote{Freq: freq, Dur: dur, Step: step}
+}
+
+// equal-temperament intervals
+var (
+ intMajor3rd = math.Pow(2, 4.0/12)
+ intMinor3rd = math.Pow(2, 3.0/12)
+ intPerf5th = math.Pow(2, 7.0/12)
+)
+
+func major(freq float64) [3]float64 {
+ return [3]float64{freq, freq * intMajor3rd, freq * intPerf5th}
+}
+func minor(freq float64) [3]float64 {
+ return [3]float64{freq, freq * intMinor3rd, freq * intPerf5th}
+}
+
+// mNote returns one note in a measure.
+func mNote(freq, dur, step float64) melodyNote {
+ return ns(freq, dur, step)
+}
+
+// chordArp returns an up/down arpeggio of a triad; each note sustains for 'dur'
+// while the sequencer only advances by 'step', so multiple voices overlap.
+func chordArp(chord [3]float64, dur, step float64) []melodyNote {
+ return []melodyNote{
+ ns(chord[0], dur, step),
+ ns(chord[1], dur, step),
+ ns(chord[2], dur, step),
+ ns(chord[1], dur, step),
+ }
+}
+
+// concatNotes flattens many melody slices into one.
+func concatNotes(groups ...[]melodyNote) []melodyNote {
+ var out []melodyNote
+ for _, g := range groups {
+ out = append(out, g...)
+ }
+ return out
+}
+
+// ── song builder ───────────────────────────────────────────────────
+
+// buildMeasure creates one measure: bass + chord pad + melody top-line.
+// bassFreq: audible bass root (130–260 Hz).
+// chord: triad in middle register (260–520 Hz).
+// melody: 2–3 scalar notes in upper register (400–1000 Hz).
+// Returns notes that sum to ~4.0 s total when stepNorm=0.5s.
+func buildMeasure(bassFreq float64, chord [3]float64, melody []float64, step float64) []melodyNote {
+ // Bass sustains for 7.5×step, giving long drone during the measure.
+ bassDur := step * 7.5
+ chordDur := step * 3.0
+ melDur := step * 1.6
+
+ // If bass is too low, shift it up one octave.
+ if bassFreq < 120 {
+ bassFreq *= 2
+ }
+
+ // Ensure chord is in audible middle register (260–520 Hz).
+ if chord[0] < 200 {
+ chord = [3]float64{chord[0] * 4, chord[1] * 4, chord[2] * 4}
+ } else if chord[0] < 260 {
+ chord = [3]float64{chord[0] * 2, chord[1] * 2, chord[2] * 2}
+ }
+
+ var notes []melodyNote
+ notes = append(notes, ns(bassFreq, bassDur, step))
+ notes = append(notes, chordArp(chord, chordDur, step)...)
+
+ for _, f := range melody {
+ ff := f
+ if ff < 300 {
+ ff *= 4
+ } else if ff < 400 {
+ ff *= 2
+ }
+ notes = append(notes, ns(ff, melDur, step))
+ }
+ return notes
+}
+
+// buildSong creates a 4-measure loop (~16 s normal, ~8 s wild) from a chord
+// progression and matching melody lines.
+func buildSong(bassFreqs [4]float64, chords [4][3]float64, melodies [4][]float64, step float64) []melodyNote {
+ var out []melodyNote
+ for i := 0; i < 4; i++ {
+ out = append(out, buildMeasure(bassFreqs[i], chords[i], melodies[i], step)...)
+ }
+ return out
+}
+
+// ── theme registry ─────────────────────────────────────────────────
+
var themeSoundPresets = map[string]themeSounds{
"neon": soundsNeon(),
"terminal": soundsTerminal(),
@@ -105,6 +204,9 @@ var themeSoundPresets = map[string]themeSounds{
"biomech": soundsBiomech(),
}
+// ════════════════════════════════════════════════════════════════════
+// NEON – bright synth-pop in C major (C → F → G → C)
+// ════════════════════════════════════════════════════════════════════
func soundsNeon() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{523.25, 659.25, 783.99, 1046.5}
@@ -113,27 +215,44 @@ func soundsNeon() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 523.25, 1046.5, 0.13, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 880, 261.63, 0.16, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 180, 90, 0.12, 0.1
+
+ cm := major(65.41)
+ fm := major(87.31)
+ gm := major(98.00)
+
+ bass := [4]float64{130.81, 174.61, 196.00, 130.81} // C3 F3 G3 C3
+ chords := [4][3]float64{cm, fm, gm, cm}
+ normalMel := [4][]float64{
+ {523.25, 587.33, 659.25, 523.25}, // C D E C
+ {698.46, 783.99, 880.00, 698.46}, // F G A F
+ {783.99, 880.00, 1046.5, 783.99}, // G A C G
+ {523.25, 659.25, 783.99, 1046.5}, // C E G C
+ }
+ wildMel := [4][]float64{
+ {1046.5, 1174.66, 1318.5, 1046.5},
+ {1396.92, 1567.98, 1760.00, 1396.92},
+ {1567.98, 1760.00, 2093.0, 1567.98},
+ {1046.5, 1318.5, 1567.98, 2093.0},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 50, Wave: "sine",
- DroneFreqs: []float64{523.25, 659.25, 783.99, 1046.5},
+ Gain: 0.03, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{130.81, 174.61, 196.00, 261.63},
Attack: 0.6, Release: 1.5, CutoffMin: 800, CutoffMax: 3000,
- Melody: []melodyNote{
- n(523.25, 0.15), n(659.25, 0.15), n(783.99, 0.15), n(1046.5, 0.15),
- n(783.99, 0.15), n(659.25, 0.15), n(523.25, 0.15), n(659.25, 0.15),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 120, Wave: "triangle",
- DroneFreqs: []float64{261.63, 523.25, 659.25},
+ Gain: 0.06, BPM: 120, Wave: "triangle",
+ DroneFreqs: []float64{261.63, 349.23, 392.00, 523.25},
Attack: 0.2, Release: 0.6, CutoffMin: 1500, CutoffMax: 6000, DetuneCents: 8,
- Melody: []melodyNote{
- n(523.25, 0.08), n(659.25, 0.08), n(783.99, 0.08), n(1046.5, 0.08),
- n(1318.5, 0.08), n(1046.5, 0.08), n(783.99, 0.08), n(659.25, 0.08),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// TERMINAL – dark industrial, E minor (Em → Bm → Am → Em)
+// ════════════════════════════════════════════════════════════════════
func soundsTerminal() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{523.25, 659.25, 783.99}
@@ -142,25 +261,44 @@ func soundsTerminal() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 600, 1200, 0.12, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 900, 400, 0.14, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 200, 100, 0.1, 0.1
+
+ em := minor(82.41)
+ bm := minor(61.74)
+ am := minor(110.00)
+
+ bass := [4]float64{164.81, 123.47, 220.00, 164.81} // E3 B2 A3 E3
+ chords := [4][3]float64{em, bm, am, em}
+ normalMel := [4][]float64{
+ {329.63, 392.00, 440.00, 329.63}, // E3=164, so E4=329
+ {246.94, 293.66, 329.63, 246.94}, // B2=123, B3=246
+ {440.00, 523.25, 587.33, 440.00}, // A3=220
+ {329.63, 392.00, 493.88, 329.63}, // E4=E4
+ }
+ wildMel := [4][]float64{
+ {659.25, 783.99, 880.00, 659.25},
+ {493.88, 587.33, 659.25, 493.88},
+ {880.00, 1046.5, 1174.66, 880.00},
+ {659.25, 783.99, 987.77, 659.25},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 40, Wave: "square",
- DroneFreqs: []float64{60, 120},
- Attack: 0.1, Release: 0.3, PulseInterval: 2.0,
- Melody: []melodyNote{
- n(196.00, 0.6), n(246.94, 0.3),
- },
+ Gain: 0.025, BPM: 60, Wave: "square",
+ DroneFreqs: []float64{82.41, 123.47, 164.81},
+ Attack: 0.1, Release: 0.3,
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.05, BPM: 160, Wave: "square",
- DroneFreqs: []float64{50, 100},
- Attack: 0.05, Release: 0.15, PulseInterval: 0.25,
- Melody: []melodyNote{
- n(196.00, 0.15), n(246.94, 0.15), n(293.66, 0.15), n(392.00, 0.15),
- },
+ Gain: 0.055, BPM: 140, Wave: "square",
+ DroneFreqs: []float64{164.81, 246.94, 329.63},
+ Attack: 0.05, Release: 0.15,
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// SYNTHWAVE – retro-future, A minor (Am → F → C → G)
+// ════════════════════════════════════════════════════════════════════
func soundsSynthwave() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{196, 246.94, 293.66, 349.23}
@@ -169,28 +307,45 @@ func soundsSynthwave() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 220, 440, 0.18, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 440, 110, 0.17, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 150, 75, 0.14, 0.09
+
+ am := minor(55.00)
+ fm := major(87.31)
+ cm := major(65.41)
+ gm := major(98.00)
+
+ bass := [4]float64{110.00, 174.61, 130.81, 196.00} // A2 F3 C3 G3
+ chords := [4][3]float64{am, fm, cm, gm}
+ normalMel := [4][]float64{
+ {220.00, 261.63, 293.66, 220.00},
+ {349.23, 392.00, 440.00, 349.23},
+ {261.63, 329.63, 392.00, 261.63},
+ {392.00, 440.00, 493.88, 392.00},
+ }
+ wildMel := [4][]float64{
+ {440.00, 523.25, 587.33, 440.00},
+ {698.46, 783.99, 880.00, 698.46},
+ {523.25, 659.25, 783.99, 523.25},
+ {783.99, 880.00, 987.77, 783.99},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 55, Wave: "sine",
- DroneFreqs: []float64{98, 196, 293.66},
+ Gain: 0.03, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{110.00, 130.81, 174.61, 196.00},
Attack: 0.8, Release: 1.5,
- Melody: []melodyNote{
- n(220.00, 0.3), n(261.63, 0.3), n(329.63, 0.3),
- n(293.66, 0.3), n(261.63, 0.3), n(246.94, 0.3), n(220.00, 0.3),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.06, BPM: 110, Wave: "triangle",
- DroneFreqs: []float64{65.41, 130.81, 196},
+ Gain: 0.065, BPM: 130, Wave: "triangle",
+ DroneFreqs: []float64{220.00, 261.63, 349.23, 392.00},
Attack: 0.3, Release: 0.7, DetuneCents: 12,
- Melody: []melodyNote{
- n(220.00, 0.15), n(261.63, 0.15), n(329.63, 0.15), n(392.00, 0.15),
- n(329.63, 0.15), n(261.63, 0.15), n(220.00, 0.15), n(261.63, 0.15),
- n(329.63, 0.15), n(440.00, 0.15),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// PLASMA – energetic, D minor (Dm → Bb → F → C)
+// ════════════════════════════════════════════════════════════════════
func soundsPlasma() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{311.13, 415.3, 466.16, 622.25}
@@ -199,27 +354,46 @@ func soundsPlasma() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 349.23, 698.46, 0.15, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 523.25, 174.61, 0.17, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "triangle", 200, 100, 0.13, 0.09
+
+ dm := minor(73.42)
+ bb := major(58.27)
+ fm := major(87.31)
+ cm := major(65.41)
+
+ bass := [4]float64{146.83, 116.54, 174.61, 130.81}
+ chords := [4][3]float64{dm, bb, fm, cm}
+ normalMel := [4][]float64{
+ {293.66, 349.23, 392.00, 293.66},
+ {233.08, 293.66, 349.23, 233.08},
+ {349.23, 440.00, 523.25, 349.23},
+ {261.63, 329.63, 392.00, 261.63},
+ }
+ wildMel := [4][]float64{
+ {587.33, 698.46, 783.99, 587.33},
+ {466.16, 587.33, 698.46, 466.16},
+ {698.46, 880.00, 1046.5, 698.46},
+ {523.25, 659.25, 783.99, 523.25},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.03, BPM: 70, Wave: "triangle",
- DroneFreqs: []float64{329.63, 466.16},
+ Gain: 0.035, BPM: 60, Wave: "triangle",
+ DroneFreqs: []float64{146.83, 233.08, 349.23},
Attack: 0.4, Release: 0.9, DetuneCents: 15,
- Melody: []melodyNote{
- n(261.63, 0.25), n(369.99, 0.25), n(261.63, 0.25), n(369.99, 0.25),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.065, BPM: 140, Wave: "square",
- DroneFreqs: []float64{164.81, 329.63},
+ Gain: 0.07, BPM: 150, Wave: "square",
+ DroneFreqs: []float64{293.66, 466.16, 698.46},
Attack: 0.1, Release: 0.4, DetuneCents: 25,
CutoffMin: 400, CutoffMax: 3000, NoiseGain: 0.02,
- Melody: []melodyNote{
- n(261.63, 0.12), n(369.99, 0.12), n(261.63, 0.12), n(369.99, 0.12),
- n(523.25, 0.12), n(369.99, 0.12), n(261.63, 0.12), n(369.99, 0.12),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// BRUTALIST – concrete minimalism, D major (D → A → D → A)
+// ════════════════════════════════════════════════════════════════════
func soundsBrutalist() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{100, 150, 200, 120}
@@ -228,25 +402,43 @@ func soundsBrutalist() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 200, 400, 0.12, 0.11
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 400, 100, 0.14, 0.1
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 120, 60, 0.1, 0.12
+
+ dm := major(73.42)
+ am := major(55.00)
+
+ bass := [4]float64{146.83, 110.00, 146.83, 110.00}
+ chords := [4][3]float64{dm, am, dm, am}
+ normalMel := [4][]float64{
+ {293.66, 349.23, 392.00, 293.66},
+ {220.00, 261.63, 293.66, 220.00},
+ {293.66, 392.00, 440.00, 293.66},
+ {220.00, 293.66, 349.23, 220.00},
+ }
+ wildMel := [4][]float64{
+ {587.33, 698.46, 783.99, 587.33},
+ {440.00, 523.25, 587.33, 440.00},
+ {587.33, 783.99, 880.00, 587.33},
+ {440.00, 587.33, 698.46, 440.00},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 45, Wave: "triangle",
- DroneFreqs: []float64{80, 120},
+ Gain: 0.03, BPM: 60, Wave: "triangle",
+ DroneFreqs: []float64{73.42, 110.00, 146.83},
Attack: 0.5, Release: 1.0,
- Melody: []melodyNote{
- n(65.41, 1.2), n(98.00, 0.4), n(65.41, 1.2),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.06, BPM: 100, Wave: "square",
- DroneFreqs: []float64{40, 80},
+ Gain: 0.065, BPM: 120, Wave: "square",
+ DroneFreqs: []float64{146.83, 220.00, 293.66},
Attack: 0.05, Release: 0.3,
- Melody: []melodyNote{
- n(65.41, 0.4), n(98.00, 0.2), n(65.41, 0.4), n(77.78, 0.2),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// VOLCANO – molten, C major (C → D → F → G)
+// ════════════════════════════════════════════════════════════════════
func soundsVolcano() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{196, 246.94, 293.66, 349.23}
@@ -255,26 +447,45 @@ func soundsVolcano() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 261.63, 523.25, 0.16, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 392, 98, 0.17, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 160, 80, 0.13, 0.1
+
+ cm := major(65.41)
+ dm := major(73.42)
+ fm := major(87.31)
+ gm := major(98.00)
+
+ bass := [4]float64{130.81, 146.83, 174.61, 196.00}
+ chords := [4][3]float64{cm, dm, fm, gm}
+ normalMel := [4][]float64{
+ {261.63, 293.66, 329.63, 261.63},
+ {293.66, 349.23, 392.00, 293.66},
+ {349.23, 440.00, 523.25, 349.23},
+ {392.00, 440.00, 493.88, 392.00},
+ }
+ wildMel := [4][]float64{
+ {523.25, 587.33, 659.25, 523.25},
+ {587.33, 698.46, 783.99, 587.33},
+ {698.46, 880.00, 1046.5, 698.46},
+ {783.99, 880.00, 987.77, 783.99},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 35, Wave: "sine",
- DroneFreqs: []float64{82.41, 123.47},
+ Gain: 0.03, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{65.41, 130.81, 174.61},
Attack: 1.0, Release: 2.0,
- Melody: []melodyNote{
- n(65.41, 0.5), n(77.78, 0.5), n(98.00, 0.5), n(130.81, 0.5),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 85, Wave: "triangle",
- DroneFreqs: []float64{65.41, 98},
+ Gain: 0.06, BPM: 110, Wave: "triangle",
+ DroneFreqs: []float64{130.81, 261.63, 349.23},
Attack: 0.2, Release: 0.6, NoiseGain: 0.015,
- Melody: []melodyNote{
- n(65.41, 0.25), n(77.78, 0.25), n(98.00, 0.25), n(130.81, 0.25),
- n(155.56, 0.25), n(196.00, 0.25),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// AURORA – ethereal, E major (E → C#m → A → E)
+// ════════════════════════════════════════════════════════════════════
func soundsAurora() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{659.25, 880, 987.77, 1046.5}
@@ -283,27 +494,44 @@ func soundsAurora() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 523.25, 880, 0.2, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 704, 352, 0.18, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 220, 110, 0.15, 0.08
+
+ em := major(82.41)
+ csm := minor(69.30)
+ am := major(55.00)
+
+ bass := [4]float64{164.81, 138.59, 110.00, 164.81}
+ chords := [4][3]float64{em, csm, am, em}
+ normalMel := [4][]float64{
+ {329.63, 369.99, 440.00, 329.63},
+ {277.18, 329.63, 369.99, 277.18},
+ {220.00, 261.63, 293.66, 220.00},
+ {329.63, 369.99, 440.00, 329.63},
+ }
+ wildMel := [4][]float64{
+ {659.25, 739.99, 880.00, 659.25},
+ {554.37, 659.25, 739.99, 554.37},
+ {440.00, 523.25, 587.33, 440.00},
+ {659.25, 739.99, 880.00, 659.25},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 40, Wave: "sine",
- DroneFreqs: []float64{440, 880, 1320},
+ Gain: 0.025, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{82.41, 110.00, 138.59, 164.81},
Attack: 1.2, Release: 2.5,
- Melody: []melodyNote{
- n(659.25, 0.3), n(783.99, 0.3), n(987.77, 0.3), n(1318.5, 0.3),
- n(987.77, 0.3), n(783.99, 0.3), n(659.25, 0.3), n(783.99, 0.3),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.05, BPM: 95, Wave: "sine",
- DroneFreqs: []float64{220, 440, 880},
+ Gain: 0.055, BPM: 120, Wave: "sine",
+ DroneFreqs: []float64{164.81, 220.00, 277.18, 329.63},
Attack: 0.3, Release: 0.8, DetuneCents: 20,
- Melody: []melodyNote{
- n(659.25, 0.15), n(783.99, 0.15), n(987.77, 0.15), n(1318.5, 0.15),
- n(1567.98, 0.15), n(1318.5, 0.15), n(987.77, 0.15), n(783.99, 0.15),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// MATRIX – cyberpunk, E minor (Em → D → C → B)
+// ════════════════════════════════════════════════════════════════════
func soundsMatrix() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{523.25, 587.33, 659.25, 698.46}
@@ -312,27 +540,45 @@ func soundsMatrix() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 880, 1318.5, 0.11, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 880, 330, 0.13, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 260, 130, 0.1, 0.09
+
+ em := minor(82.41)
+ dm := major(73.42)
+ cm := major(65.41)
+ bm := major(61.74)
+
+ bass := [4]float64{164.81, 146.83, 130.81, 123.47}
+ chords := [4][3]float64{em, dm, cm, bm}
+ normalMel := [4][]float64{
+ {329.63, 392.00, 440.00, 329.63},
+ {293.66, 349.23, 392.00, 293.66},
+ {261.63, 329.63, 392.00, 261.63},
+ {246.94, 293.66, 349.23, 246.94},
+ }
+ wildMel := [4][]float64{
+ {659.25, 783.99, 880.00, 659.25},
+ {587.33, 698.46, 783.99, 587.33},
+ {523.25, 659.25, 783.99, 523.25},
+ {493.88, 587.33, 698.46, 493.88},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 75, Wave: "square",
- DroneFreqs: []float64{523.25, 659.25},
+ Gain: 0.03, BPM: 60, Wave: "square",
+ DroneFreqs: []float64{82.41, 130.81, 164.81},
Attack: 0.1, Release: 0.3,
- Melody: []melodyNote{
- n(293.66, 0.2), n(440.00, 0.2), n(587.33, 0.2), n(739.99, 0.2),
- n(587.33, 0.2), n(440.00, 0.2), n(293.66, 0.2),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.065, BPM: 160, Wave: "square",
- DroneFreqs: []float64{261.63, 523.25},
+ Gain: 0.07, BPM: 150, Wave: "square",
+ DroneFreqs: []float64{164.81, 261.63, 329.63},
Attack: 0.05, Release: 0.15,
- Melody: []melodyNote{
- n(293.66, 0.1), n(440.00, 0.1), n(587.33, 0.1), n(739.99, 0.1),
- n(880.00, 0.1), n(739.99, 0.1), n(587.33, 0.1), n(440.00, 0.1),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// OCEAN – flowing, G major (G → D → Em → C)
+// ════════════════════════════════════════════════════════════════════
func soundsOcean() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{174.61, 196, 220, 246.94}
@@ -341,27 +587,45 @@ func soundsOcean() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 349.23, 523.25, 0.2, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 415.3, 246.94, 0.18, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 140, 70, 0.15, 0.08
+
+ gm := major(98.00)
+ dm := major(73.42)
+ em := minor(82.41)
+ cm := major(65.41)
+
+ bass := [4]float64{196.00, 146.83, 164.81, 130.81}
+ chords := [4][3]float64{gm, dm, em, cm}
+ normalMel := [4][]float64{
+ {392.00, 440.00, 493.88, 392.00},
+ {293.66, 349.23, 392.00, 293.66},
+ {329.63, 392.00, 440.00, 329.63},
+ {261.63, 329.63, 392.00, 261.63},
+ }
+ wildMel := [4][]float64{
+ {783.99, 880.00, 987.77, 783.99},
+ {587.33, 698.46, 783.99, 587.33},
+ {659.25, 783.99, 880.00, 659.25},
+ {523.25, 659.25, 783.99, 523.25},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 30, Wave: "sine",
- DroneFreqs: []float64{130.81, 174.61},
+ Gain: 0.025, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{98.00, 130.81, 164.81, 196.00},
Attack: 1.5, Release: 3.0, NoiseGain: 0.02,
- Melody: []melodyNote{
- n(130.81, 0.35), n(164.81, 0.35), n(196.00, 0.35), n(164.81, 0.35),
- n(130.81, 0.35), n(196.00, 0.35), n(164.81, 0.35), n(130.81, 0.35),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 80, Wave: "triangle",
- DroneFreqs: []float64{98, 130.81},
+ Gain: 0.06, BPM: 120, Wave: "triangle",
+ DroneFreqs: []float64{196.00, 261.63, 329.63, 392.00},
Attack: 0.3, Release: 0.7, NoiseGain: 0.04,
- Melody: []melodyNote{
- n(130.81, 0.18), n(164.81, 0.18), n(196.00, 0.18), n(261.63, 0.18),
- n(196.00, 0.18), n(164.81, 0.18), n(130.81, 0.18), n(98.00, 0.18),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// DOS – 8-bit chip-tune, C major (C → G → Am → F)
+// ════════════════════════════════════════════════════════════════════
func soundsDos() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{800, 1000}
@@ -370,27 +634,45 @@ func soundsDos() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 400, 800, 0.1, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 800, 200, 0.1, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 300, 150, 0.08, 0.1
+
+ cm := major(65.41)
+ gm := major(98.00)
+ am := minor(110.00)
+ fm := major(87.31)
+
+ bass := [4]float64{130.81, 196.00, 220.00, 174.61}
+ chords := [4][3]float64{cm, gm, am, fm}
+ normalMel := [4][]float64{
+ {523.25, 587.33, 659.25, 523.25},
+ {783.99, 880.00, 987.77, 783.99},
+ {880.00, 1046.5, 1174.66, 880.00},
+ {698.46, 783.99, 880.00, 698.46},
+ }
+ wildMel := [4][]float64{
+ {1046.5, 1174.66, 1318.5, 1046.5},
+ {1567.98, 1760.00, 1975.53, 1567.98},
+ {1760.00, 2093.0, 2349.32, 1760.00},
+ {1396.92, 1567.98, 1760.00, 1396.92},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 60, Wave: "square",
- DroneFreqs: []float64{200, 400},
+ Gain: 0.03, BPM: 70, Wave: "square",
+ DroneFreqs: []float64{65.41, 130.81, 196.00, 220.00},
Attack: 0.05, Release: 0.15,
- Melody: []melodyNote{
- n(261.63, 0.18), n(329.63, 0.18), n(392.00, 0.18), n(523.25, 0.18),
- n(392.00, 0.18), n(329.63, 0.18), n(261.63, 0.18),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.065, BPM: 200, Wave: "square",
- DroneFreqs: []float64{100, 200},
+ Gain: 0.07, BPM: 180, Wave: "square",
+ DroneFreqs: []float64{130.81, 261.63, 392.00, 440.00},
Attack: 0.02, Release: 0.08,
- Melody: []melodyNote{
- n(261.63, 0.08), n(329.63, 0.08), n(392.00, 0.08), n(523.25, 0.08),
- n(659.25, 0.08), n(523.25, 0.08), n(392.00, 0.08), n(329.63, 0.08),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// RETRO – warm nostalgia, C major (C → Am → F → G)
+// ════════════════════════════════════════════════════════════════════
func soundsRetro() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{1046.5, 1318.5}
@@ -399,28 +681,46 @@ func soundsRetro() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 800, 1600, 0.14, 0.1
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 1600, 400, 0.15, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 400, 200, 0.1, 0.1
+
+ cm := major(65.41)
+ am := minor(55.00)
+ fm := major(87.31)
+ gm := major(98.00)
+
+ bass := [4]float64{130.81, 110.00, 174.61, 196.00}
+ chords := [4][3]float64{cm, am, fm, gm}
+ normalMel := [4][]float64{
+ {261.63, 329.63, 392.00, 261.63},
+ {220.00, 261.63, 293.66, 220.00},
+ {349.23, 392.00, 440.00, 349.23},
+ {392.00, 440.00, 493.88, 392.00},
+ }
+ wildMel := [4][]float64{
+ {523.25, 659.25, 783.99, 523.25},
+ {440.00, 523.25, 587.33, 440.00},
+ {698.46, 783.99, 880.00, 698.46},
+ {783.99, 880.00, 987.77, 783.99},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 80, Wave: "square",
- DroneFreqs: []float64{523.25, 659.25},
+ Gain: 0.03, BPM: 60, Wave: "square",
+ DroneFreqs: []float64{65.41, 110.00, 130.81, 174.61},
Attack: 0.1, Release: 0.3,
- Melody: []melodyNote{
- n(261.63, 0.25), n(349.23, 0.25), n(440.00, 0.25), n(523.25, 0.25),
- n(440.00, 0.25), n(349.23, 0.25), n(261.63, 0.25),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 155, Wave: "square",
- DroneFreqs: []float64{261.63, 523.25},
+ Gain: 0.06, BPM: 140, Wave: "square",
+ DroneFreqs: []float64{130.81, 220.00, 261.63, 349.23},
Attack: 0.05, Release: 0.15,
DetuneCents: 30, NoiseGain: 0.02,
- Melody: []melodyNote{
- n(261.63, 0.12), n(349.23, 0.12), n(440.00, 0.12), n(523.25, 0.12),
- n(698.46, 0.12), n(523.25, 0.12), n(440.00, 0.12), n(349.23, 0.12),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// COSMOS – vast space, A major (A → D → C → G)
+// ════════════════════════════════════════════════════════════════════
func soundsCosmos() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{220, 277.18, 329.63, 392}
@@ -429,27 +729,45 @@ func soundsCosmos() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 392, 587.33, 0.22, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 587.33, 196, 0.2, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 170, 85, 0.16, 0.08
+
+ am := major(55.00)
+ dm := major(73.42)
+ cm := major(65.41)
+ gm := major(98.00)
+
+ bass := [4]float64{110.00, 146.83, 130.81, 196.00}
+ chords := [4][3]float64{am, dm, cm, gm}
+ normalMel := [4][]float64{
+ {220.00, 261.63, 293.66, 220.00},
+ {293.66, 349.23, 392.00, 293.66},
+ {261.63, 329.63, 392.00, 261.63},
+ {392.00, 440.00, 493.88, 392.00},
+ }
+ wildMel := [4][]float64{
+ {440.00, 523.25, 587.33, 440.00},
+ {587.33, 698.46, 783.99, 587.33},
+ {523.25, 659.25, 783.99, 523.25},
+ {783.99, 880.00, 987.77, 783.99},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 30, Wave: "sine",
- DroneFreqs: []float64{220, 440, 660},
+ Gain: 0.025, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{55.00, 110.00, 146.83, 196.00},
Attack: 1.5, Release: 3.0,
- Melody: []melodyNote{
- n(130.81, 0.5), n(196.00, 0.5), n(261.63, 0.5), n(329.63, 0.5),
- n(261.63, 0.5), n(196.00, 0.5), n(130.81, 0.5),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.05, BPM: 75, Wave: "triangle",
- DroneFreqs: []float64{110, 220, 440},
+ Gain: 0.06, BPM: 120, Wave: "triangle",
+ DroneFreqs: []float64{110.00, 220.00, 293.66, 392.00},
Attack: 0.3, Release: 0.8,
- Melody: []melodyNote{
- n(130.81, 0.2), n(196.00, 0.2), n(261.63, 0.2), n(329.63, 0.2),
- n(392.00, 0.2), n(329.63, 0.2), n(261.63, 0.2), n(196.00, 0.2),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// RETROFUTURE – retro sci-fi, E minor (Em → C → G → D)
+// ════════════════════════════════════════════════════════════════════
func soundsRetrofuture() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{196, 246.94, 329.63, 440}
@@ -458,28 +776,45 @@ func soundsRetrofuture() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 330, 523.25, 0.18, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 415.3, 165, 0.17, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "triangle", 190, 95, 0.14, 0.09
+
+ em := minor(82.41)
+ cm := major(65.41)
+ gm := major(98.00)
+ dm := major(73.42)
+
+ bass := [4]float64{164.81, 130.81, 196.00, 146.83}
+ chords := [4][3]float64{em, cm, gm, dm}
+ normalMel := [4][]float64{
+ {329.63, 392.00, 440.00, 329.63},
+ {261.63, 329.63, 392.00, 261.63},
+ {392.00, 440.00, 493.88, 392.00},
+ {293.66, 349.23, 392.00, 293.66},
+ }
+ wildMel := [4][]float64{
+ {659.25, 783.99, 880.00, 659.25},
+ {523.25, 659.25, 783.99, 523.25},
+ {783.99, 880.00, 987.77, 783.99},
+ {587.33, 698.46, 783.99, 587.33},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 55, Wave: "triangle",
- DroneFreqs: []float64{220, 330, 440},
+ Gain: 0.03, BPM: 60, Wave: "triangle",
+ DroneFreqs: []float64{82.41, 130.81, 164.81, 196.00},
Attack: 0.8, Release: 1.8,
- Melody: []melodyNote{
- n(261.63, 0.3), n(311.13, 0.3), n(392.00, 0.3), n(466.16, 0.3),
- n(392.00, 0.3), n(311.13, 0.3), n(261.63, 0.3),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 120, Wave: "square",
- DroneFreqs: []float64{110, 220},
+ Gain: 0.06, BPM: 130, Wave: "square",
+ DroneFreqs: []float64{164.81, 261.63, 329.63, 392.00},
Attack: 0.05, Release: 0.15,
- Melody: []melodyNote{
- n(261.63, 0.12), n(311.13, 0.12), n(392.00, 0.12), n(466.16, 0.12),
- n(523.25, 0.12), n(466.16, 0.12), n(392.00, 0.12), n(311.13, 0.12),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
-// soundsSpaceage returns synth parameters for the Space Age theme.
+// ════════════════════════════════════════════════════════════════════
+// SPACEAGE – mid-century optimism, C major (C → Am → F → G)
+// ════════════════════════════════════════════════════════════════════
func soundsSpaceage() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{440, 554.37, 659.25, 880}
@@ -488,28 +823,45 @@ func soundsSpaceage() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 440, 880, 0.18, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 659.25, 330, 0.17, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 240, 120, 0.13, 0.08
+
+ cm := major(65.41)
+ am := minor(55.00)
+ fm := major(87.31)
+ gm := major(98.00)
+
+ bass := [4]float64{130.81, 110.00, 174.61, 196.00}
+ chords := [4][3]float64{cm, am, fm, gm}
+ normalMel := [4][]float64{
+ {261.63, 329.63, 392.00, 261.63},
+ {220.00, 261.63, 293.66, 220.00},
+ {349.23, 392.00, 440.00, 349.23},
+ {392.00, 440.00, 493.88, 392.00},
+ }
+ wildMel := [4][]float64{
+ {523.25, 659.25, 783.99, 523.25},
+ {440.00, 523.25, 587.33, 440.00},
+ {698.46, 783.99, 880.00, 698.46},
+ {783.99, 880.00, 987.77, 783.99},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 50, Wave: "sine",
- DroneFreqs: []float64{440, 880},
+ Gain: 0.025, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{65.41, 110.00, 130.81, 174.61},
Attack: 0.8, Release: 1.5,
- Melody: []melodyNote{
- n(261.63, 0.35), n(329.63, 0.35), n(392.00, 0.35), n(523.25, 0.35),
- n(392.00, 0.35), n(329.63, 0.35), n(261.63, 0.35),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.05, BPM: 110, Wave: "triangle",
- DroneFreqs: []float64{220, 440},
+ Gain: 0.06, BPM: 130, Wave: "triangle",
+ DroneFreqs: []float64{130.81, 220.00, 261.63, 349.23},
Attack: 0.1, Release: 0.4, PulseInterval: 0.5,
- Melody: []melodyNote{
- n(261.63, 0.15), n(329.63, 0.15), n(392.00, 0.15), n(523.25, 0.15),
- n(659.25, 0.15), n(523.25, 0.15), n(392.00, 0.15), n(329.63, 0.15),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
-// soundsTropical returns synth parameters for the Tropical Beach theme.
+// ════════════════════════════════════════════════════════════════════
+// TROPICALE – island vacation, C major (C → G → Am → F)
+// ════════════════════════════════════════════════════════════════════
func soundsTropical() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{523.25, 659.25, 783.99, 1046.5}
@@ -518,27 +870,45 @@ func soundsTropical() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 440, 880, 0.18, 0.08
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 660, 330, 0.17, 0.075
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 200, 100, 0.12, 0.07
+
+ cm := major(65.41)
+ gm := major(98.00)
+ am := minor(110.00)
+ fm := major(87.31)
+
+ bass := [4]float64{130.81, 196.00, 220.00, 174.61}
+ chords := [4][3]float64{cm, gm, am, fm}
+ normalMel := [4][]float64{
+ {261.63, 293.66, 329.63, 261.63},
+ {392.00, 440.00, 493.88, 392.00},
+ {440.00, 523.25, 587.33, 440.00},
+ {349.23, 392.00, 440.00, 349.23},
+ }
+ wildMel := [4][]float64{
+ {523.25, 587.33, 659.25, 523.25},
+ {783.99, 880.00, 987.77, 783.99},
+ {880.00, 1046.5, 1174.66, 880.00},
+ {698.46, 783.99, 880.00, 698.46},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 65, Wave: "sine",
- DroneFreqs: []float64{261.63, 329.63, 392, 523.25},
+ Gain: 0.025, BPM: 65, Wave: "sine",
+ DroneFreqs: []float64{65.41, 130.81, 196.00, 220.00},
Attack: 0.6, Release: 1.5, NoiseGain: 0.015,
- Melody: []melodyNote{
- n(261.63, 0.28), n(293.66, 0.28), n(329.63, 0.28), n(392.00, 0.28), n(440.00, 0.28),
- n(392.00, 0.28), n(329.63, 0.28), n(293.66, 0.28), n(261.63, 0.28),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 140, Wave: "triangle",
- DroneFreqs: []float64{130.81, 261.63, 329.63, 392},
+ Gain: 0.06, BPM: 140, Wave: "triangle",
+ DroneFreqs: []float64{130.81, 261.63, 392.00, 440.00},
Attack: 0.1, Release: 0.3, NoiseGain: 0.03,
- Melody: []melodyNote{
- n(261.63, 0.12), n(293.66, 0.12), n(329.63, 0.12), n(392.00, 0.12), n(440.00, 0.12),
- n(523.25, 0.12), n(440.00, 0.12), n(392.00, 0.12), n(329.63, 0.12), n(293.66, 0.12),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// NOIR – smoky film noir, D minor (Dm → C → Bb → A)
+// ════════════════════════════════════════════════════════════════════
func soundsNoir() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{174.61, 220, 261.63}
@@ -547,27 +917,45 @@ func soundsNoir() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 220, 392, 0.18, 0.085
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "triangle", 330, 165, 0.2, 0.08
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "triangle", 130, 65, 0.14, 0.08
+
+ dm := minor(73.42)
+ cm := major(65.41)
+ bb := major(58.27)
+ am := major(55.00)
+
+ bass := [4]float64{146.83, 130.81, 116.54, 110.00}
+ chords := [4][3]float64{dm, cm, bb, am}
+ normalMel := [4][]float64{
+ {293.66, 349.23, 392.00, 293.66},
+ {261.63, 293.66, 329.63, 261.63},
+ {233.08, 261.63, 293.66, 233.08},
+ {220.00, 246.94, 277.18, 220.00},
+ }
+ wildMel := [4][]float64{
+ {587.33, 698.46, 783.99, 587.33},
+ {523.25, 587.33, 659.25, 523.25},
+ {466.16, 523.25, 587.33, 466.16},
+ {440.00, 493.88, 554.37, 440.00},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.02, BPM: 40, Wave: "sine",
- DroneFreqs: []float64{174.61, 220, 261.63},
+ Gain: 0.025, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{73.42, 110.00, 130.81, 146.83},
Attack: 1.0, Release: 2.0, NoiseGain: 0.015,
- Melody: []melodyNote{
- n(130.81, 0.45), n(155.56, 0.45), n(196.00, 0.45), n(233.08, 0.45),
- n(196.00, 0.45), n(155.56, 0.45), n(130.81, 0.45),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.05, BPM: 90, Wave: "triangle",
- DroneFreqs: []float64{130.81, 174.61, 220},
+ Gain: 0.055, BPM: 120, Wave: "triangle",
+ DroneFreqs: []float64{146.83, 220.00, 261.63, 293.66},
Attack: 0.3, Release: 0.7, PulseInterval: 1.2,
- Melody: []melodyNote{
- n(130.81, 0.2), n(155.56, 0.2), n(196.00, 0.2), n(233.08, 0.2),
- n(261.63, 0.2), n(233.08, 0.2), n(196.00, 0.2), n(155.56, 0.2),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// CATHEDRAL – sacred organ, A minor (Am → C → G → D)
+// ════════════════════════════════════════════════════════════════════
func soundsCathedral() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{293.66, 392, 587.33}
@@ -576,27 +964,45 @@ func soundsCathedral() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 392, 783.99, 0.22, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 523.25, 196, 0.22, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "sine", 180, 90, 0.18, 0.08
+
+ am := minor(55.00)
+ cm := major(65.41)
+ gm := major(98.00)
+ dm := major(73.42)
+
+ bass := [4]float64{110.00, 130.81, 196.00, 146.83}
+ chords := [4][3]float64{am, cm, gm, dm}
+ normalMel := [4][]float64{
+ {220.00, 261.63, 293.66, 220.00},
+ {261.63, 329.63, 392.00, 261.63},
+ {392.00, 440.00, 493.88, 392.00},
+ {293.66, 349.23, 392.00, 293.66},
+ }
+ wildMel := [4][]float64{
+ {440.00, 523.25, 587.33, 440.00},
+ {523.25, 659.25, 783.99, 523.25},
+ {783.99, 880.00, 987.77, 783.99},
+ {587.33, 698.46, 783.99, 587.33},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 35, Wave: "sine",
- DroneFreqs: []float64{293.66, 440, 587.33},
+ Gain: 0.03, BPM: 60, Wave: "sine",
+ DroneFreqs: []float64{55.00, 110.00, 130.81, 196.00},
Attack: 1.5, Release: 3.0,
- Melody: []melodyNote{
- n(130.81, 0.5), n(164.81, 0.5), n(196.00, 0.5), n(261.63, 0.5), n(329.63, 0.5),
- n(261.63, 0.5), n(196.00, 0.5), n(164.81, 0.5), n(130.81, 0.5),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 70, Wave: "triangle",
- DroneFreqs: []float64{146.83, 293.66, 440},
+ Gain: 0.06, BPM: 120, Wave: "triangle",
+ DroneFreqs: []float64{110.00, 220.00, 261.63, 392.00},
Attack: 0.3, Release: 0.8,
- Melody: []melodyNote{
- n(130.81, 0.22), n(164.81, 0.22), n(196.00, 0.22), n(261.63, 0.22), n(329.63, 0.22),
- n(392.00, 0.22), n(329.63, 0.22), n(261.63, 0.22), n(196.00, 0.22), n(164.81, 0.22),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// SURVEILLANCE – spy tension, E major (E → C → D → B)
+// ════════════════════════════════════════════════════════════════════
func soundsSurveillance() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{440, 554.37, 659.25}
@@ -605,26 +1011,45 @@ func soundsSurveillance() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 660, 1320, 0.12, 0.09
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 990, 330, 0.14, 0.085
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "square", 280, 140, 0.09, 0.09
+
+ em := major(82.41)
+ cm := major(65.41)
+ dm := major(73.42)
+ bm := major(61.74)
+
+ bass := [4]float64{164.81, 130.81, 146.83, 123.47}
+ chords := [4][3]float64{em, cm, dm, bm}
+ normalMel := [4][]float64{
+ {329.63, 369.99, 440.00, 329.63},
+ {261.63, 293.66, 329.63, 261.63},
+ {293.66, 349.23, 392.00, 293.66},
+ {246.94, 277.18, 311.13, 246.94},
+ }
+ wildMel := [4][]float64{
+ {659.25, 739.99, 880.00, 659.25},
+ {523.25, 587.33, 659.25, 523.25},
+ {587.33, 698.46, 783.99, 587.33},
+ {493.88, 554.37, 622.25, 493.88},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 55, Wave: "square",
- DroneFreqs: []float64{440, 660},
+ Gain: 0.03, BPM: 60, Wave: "square",
+ DroneFreqs: []float64{82.41, 130.81, 146.83, 164.81},
Attack: 0.2, Release: 0.5,
- Melody: []melodyNote{
- n(261.63, 0.35), n(349.23, 0.35), n(261.63, 0.35), n(349.23, 0.35),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.06, BPM: 140, Wave: "square",
- DroneFreqs: []float64{220, 440},
+ Gain: 0.065, BPM: 140, Wave: "square",
+ DroneFreqs: []float64{164.81, 261.63, 293.66, 329.63},
Attack: 0.05, Release: 0.15, PulseInterval: 0.3,
- Melody: []melodyNote{
- n(261.63, 0.12), n(349.23, 0.12), n(261.63, 0.12), n(349.23, 0.12),
- n(523.25, 0.12), n(349.23, 0.12), n(261.63, 0.12), n(349.23, 0.12),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}
+// ════════════════════════════════════════════════════════════════════
+// BIOMECH – organic meets machine, C# major (C# → A → E → B)
+// ════════════════════════════════════════════════════════════════════
func soundsBiomech() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{164.81, 246.94, 311.13, 466.16}
@@ -633,23 +1058,38 @@ func soundsBiomech() themeSounds {
s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 220, 523.25, 0.18, 0.095
s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 392, 130.81, 0.2, 0.09
s.Bounce.Wave, s.Bounce.Start, s.Bounce.End, s.Bounce.Dur, s.Bounce.Gain = "triangle", 160, 80, 0.14, 0.09
+
+ cs := major(69.30)
+ am := major(55.00)
+ emin := major(82.41)
+ bm := major(61.74)
+
+ bass := [4]float64{138.59, 110.00, 164.81, 123.47}
+ chords := [4][3]float64{cs, am, emin, bm}
+ normalMel := [4][]float64{
+ {277.18, 329.63, 369.99, 277.18},
+ {220.00, 261.63, 293.66, 220.00},
+ {329.63, 392.00, 440.00, 329.63},
+ {246.94, 293.66, 329.63, 246.94},
+ }
+ wildMel := [4][]float64{
+ {554.37, 659.25, 739.99, 554.37},
+ {440.00, 523.25, 587.33, 440.00},
+ {659.25, 783.99, 880.00, 659.25},
+ {493.88, 587.33, 659.25, 493.88},
+ }
+
s.Ambient.Normal = ambientPreset{
- Gain: 0.025, BPM: 50, Wave: "triangle",
- DroneFreqs: []float64{164.81, 246.94},
+ Gain: 0.03, BPM: 60, Wave: "triangle",
+ DroneFreqs: []float64{69.30, 110.00, 138.59, 164.81},
Attack: 0.7, Release: 1.5, DetuneCents: 8,
- Melody: []melodyNote{
- n(130.81, 0.3), n(155.56, 0.3), n(185.00, 0.3), n(220.00, 0.3),
- n(185.00, 0.3), n(155.56, 0.3), n(130.81, 0.3),
- },
+ Melody: buildSong(bass, chords, normalMel, 0.5),
}
s.Ambient.Wild = ambientPreset{
- Gain: 0.055, BPM: 115, Wave: "square",
- DroneFreqs: []float64{82.41, 164.81},
+ Gain: 0.06, BPM: 140, Wave: "square",
+ DroneFreqs: []float64{138.59, 220.00, 277.18, 329.63},
Attack: 0.1, Release: 0.3, DetuneCents: 18,
- Melody: []melodyNote{
- n(130.81, 0.13), n(155.56, 0.13), n(185.00, 0.13), n(220.00, 0.13),
- n(261.63, 0.13), n(220.00, 0.13), n(185.00, 0.13), n(155.56, 0.13),
- },
+ Melody: buildSong(bass, chords, wildMel, 0.25),
}
return s
}