summaryrefslogtreecommitdiff
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
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
-rw-r--r--internal/generator/generator_test.go16
-rw-r--r--internal/generator/templates/shared/shared.js65
-rw-r--r--internal/generator/theme_sounds.go340
3 files changed, 287 insertions, 134 deletions
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 3ceb72c..49d1a2f 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -158,11 +158,11 @@ func TestThemeSoundPresetsAmbientPopulated(t *testing.T) {
normal := preset.Ambient.Normal
wild := preset.Ambient.Wild
- if len(normal.DroneFreqs) == 0 && len(normal.PulseFreqs) == 0 {
- t.Errorf("theme %q ambient.Normal has no drone or pulse frequencies", name)
+ if len(normal.DroneFreqs) == 0 && len(normal.PulseFreqs) == 0 && len(normal.Melody) == 0 {
+ t.Errorf("theme %q ambient.Normal has no drone, pulse, or melody frequencies", name)
}
- if len(wild.DroneFreqs) == 0 && len(wild.PulseFreqs) == 0 {
- t.Errorf("theme %q ambient.Wild has no drone or pulse frequencies", name)
+ if len(wild.DroneFreqs) == 0 && len(wild.PulseFreqs) == 0 && len(wild.Melody) == 0 {
+ t.Errorf("theme %q ambient.Wild has no drone, pulse, or melody frequencies", name)
}
}
}
@@ -215,6 +215,14 @@ func TestThemeSoundPresetsAmbientValuesBounded(t *testing.T) {
t.Errorf("theme %q ambient.%s pulseFreqs[%d]=%f; want positive", name, mode, i, f)
}
}
+ for i, m := range a.Melody {
+ if m.Freq <= 0 {
+ t.Errorf("theme %q ambient.%s melody[%d].freq=%f; want positive", name, mode, i, m.Freq)
+ }
+ if m.Dur <= 0 {
+ t.Errorf("theme %q ambient.%s melody[%d].dur=%f; want positive", name, mode, i, m.Dur)
+ }
+ }
if a.CutoffMin < 0 || a.CutoffMin > 10000 {
t.Errorf("theme %q ambient.%s cutoffMin=%f; want [0, 10000]", name, mode, a.CutoffMin)
}
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);
diff --git a/internal/generator/theme_sounds.go b/internal/generator/theme_sounds.go
index 44cf85a..c3235dd 100644
--- a/internal/generator/theme_sounds.go
+++ b/internal/generator/theme_sounds.go
@@ -5,23 +5,31 @@ import (
"html/template"
)
+// melodyNote is a single note in a looping ambient melody.
+type melodyNote struct {
+ Freq float64 `json:"freq"`
+ Dur float64 `json:"dur"`
+ 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"`
- Gain float64 `json:"gain,omitempty"`
- Wave string `json:"wave,omitempty"`
- DroneFreqs []float64 `json:"droneFreqs,omitempty"`
- PulseFreqs []float64 `json:"pulseFreqs,omitempty"`
- CutoffMin float64 `json:"cutoffMin,omitempty"`
- CutoffMax float64 `json:"cutoffMax,omitempty"`
- NoiseGain float64 `json:"noiseGain,omitempty"`
- Attack float64 `json:"attack,omitempty"`
- Release float64 `json:"release,omitempty"`
- DetuneCents float64 `json:"detuneCents,omitempty"`
- Rhythm []float64 `json:"rhythm,omitempty"`
+ BPM float64 `json:"bpm,omitempty"`
+ PulseInterval float64 `json:"pulseInterval,omitempty"`
+ Gain float64 `json:"gain,omitempty"`
+ Wave string `json:"wave,omitempty"`
+ DroneFreqs []float64 `json:"droneFreqs,omitempty"`
+ PulseFreqs []float64 `json:"pulseFreqs,omitempty"`
+ CutoffMin float64 `json:"cutoffMin,omitempty"`
+ CutoffMax float64 `json:"cutoffMax,omitempty"`
+ NoiseGain float64 `json:"noiseGain,omitempty"`
+ Attack float64 `json:"attack,omitempty"`
+ Release float64 `json:"release,omitempty"`
+ DetuneCents float64 `json:"detuneCents,omitempty"`
+ Rhythm []float64 `json:"rhythm,omitempty"`
+ Melody []melodyNote `json:"melody,omitempty"`
}
// ambientSounds holds normal and wild-mode ambient presets for a theme.
@@ -69,17 +77,9 @@ type themeSounds struct {
Ambient ambientSounds `json:"ambient,omitempty"`
}
-// ambient is a concise helper for building an ambientPreset with sensible defaults.
-func ambient(gain, bpm float64, wave string, drone, pulse []float64) ambientPreset {
- return ambientPreset{
- Gain: gain,
- BPM: bpm,
- Wave: wave,
- DroneFreqs: drone,
- PulseFreqs: pulse,
- Attack: 0.4,
- Release: 0.8,
- }
+// n is a concise helper for building a melodyNote.
+func n(freq, dur float64) melodyNote {
+ return melodyNote{Freq: freq, Dur: dur}
}
// themeSoundPresets maps CLI theme names to synth parameters (see themes.go registry).
@@ -116,16 +116,20 @@ func soundsNeon() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 50, Wave: "sine",
DroneFreqs: []float64{523.25, 659.25, 783.99, 1046.5},
- PulseFreqs: []float64{1046.5},
- Attack: 0.6, Release: 1.5,
- CutoffMin: 800, CutoffMax: 3000,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 120, Wave: "triangle",
DroneFreqs: []float64{261.63, 523.25, 659.25},
- PulseFreqs: []float64{1046.5, 2093},
- Attack: 0.2, Release: 0.6,
- CutoffMin: 1500, CutoffMax: 6000, DetuneCents: 8,
+ 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),
+ },
}
return s
}
@@ -141,16 +145,18 @@ func soundsTerminal() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 40, Wave: "square",
DroneFreqs: []float64{60, 120},
- PulseFreqs: []float64{800},
- Attack: 0.1, Release: 0.3,
- PulseInterval: 2.0,
+ Attack: 0.1, Release: 0.3, PulseInterval: 2.0,
+ Melody: []melodyNote{
+ n(196.00, 0.6), n(246.94, 0.3),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.05, BPM: 160, Wave: "square",
DroneFreqs: []float64{50, 100},
- PulseFreqs: []float64{400, 800, 1600},
- Attack: 0.05, Release: 0.15,
- PulseInterval: 0.25,
+ 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),
+ },
}
return s
}
@@ -166,15 +172,21 @@ func soundsSynthwave() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 55, Wave: "sine",
DroneFreqs: []float64{98, 196, 293.66},
- PulseFreqs: []float64{587.33},
- Attack: 0.8, Release: 1.5,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.06, BPM: 110, Wave: "triangle",
DroneFreqs: []float64{65.41, 130.81, 196},
- PulseFreqs: []float64{440, 880},
- Attack: 0.3, Release: 0.7,
- DetuneCents: 12,
+ 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),
+ },
}
return s
}
@@ -190,18 +202,20 @@ func soundsPlasma() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.03, BPM: 70, Wave: "triangle",
DroneFreqs: []float64{329.63, 466.16},
- PulseFreqs: []float64{622.25},
- Attack: 0.4, Release: 0.9,
- DetuneCents: 15,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.065, BPM: 140, Wave: "square",
DroneFreqs: []float64{164.81, 329.63},
- PulseFreqs: []float64{622.25, 1244.5},
- Attack: 0.1, Release: 0.4,
- DetuneCents: 25,
- CutoffMin: 400, CutoffMax: 3000,
- NoiseGain: 0.02,
+ 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),
+ },
}
return s
}
@@ -217,15 +231,18 @@ func soundsBrutalist() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 45, Wave: "triangle",
DroneFreqs: []float64{80, 120},
- PulseFreqs: []float64{160},
- Attack: 0.5, Release: 1.0,
+ Attack: 0.5, Release: 1.0,
+ Melody: []melodyNote{
+ n(65.41, 1.2), n(98.00, 0.4), n(65.41, 1.2),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.06, BPM: 100, Wave: "square",
DroneFreqs: []float64{40, 80},
- PulseFreqs: []float64{120, 240},
- Attack: 0.05, Release: 0.3,
- Rhythm: []float64{1, 0.5, 1.5, 0.5},
+ 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),
+ },
}
return s
}
@@ -241,15 +258,19 @@ func soundsVolcano() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 35, Wave: "sine",
DroneFreqs: []float64{82.41, 123.47},
- PulseFreqs: []float64{164.81},
- Attack: 1.0, Release: 2.0,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 85, Wave: "triangle",
DroneFreqs: []float64{65.41, 98},
- PulseFreqs: []float64{164.81, 329.63},
- Attack: 0.2, Release: 0.6,
- NoiseGain: 0.015,
+ 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),
+ },
}
return s
}
@@ -265,15 +286,20 @@ func soundsAurora() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 40, Wave: "sine",
DroneFreqs: []float64{440, 880, 1320},
- PulseFreqs: []float64{1760},
- Attack: 1.2, Release: 2.5,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.05, BPM: 95, Wave: "sine",
DroneFreqs: []float64{220, 440, 880},
- PulseFreqs: []float64{1320, 1760},
- Attack: 0.3, Release: 0.8,
- DetuneCents: 20,
+ 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),
+ },
}
return s
}
@@ -289,14 +315,20 @@ func soundsMatrix() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 75, Wave: "square",
DroneFreqs: []float64{523.25, 659.25},
- PulseFreqs: []float64{880, 1046.5},
- Attack: 0.1, Release: 0.3,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.065, BPM: 160, Wave: "square",
DroneFreqs: []float64{261.63, 523.25},
- PulseFreqs: []float64{880, 1318.5, 1760},
- Attack: 0.05, Release: 0.15,
+ 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),
+ },
}
return s
}
@@ -312,16 +344,20 @@ func soundsOcean() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 30, Wave: "sine",
DroneFreqs: []float64{130.81, 174.61},
- PulseFreqs: []float64{349.23},
- Attack: 1.5, Release: 3.0,
- NoiseGain: 0.02,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 80, Wave: "triangle",
DroneFreqs: []float64{98, 130.81},
- PulseFreqs: []float64{349.23, 698.46},
- Attack: 0.3, Release: 0.7,
- NoiseGain: 0.04,
+ 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),
+ },
}
return s
}
@@ -337,14 +373,20 @@ func soundsDos() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 60, Wave: "square",
DroneFreqs: []float64{200, 400},
- PulseFreqs: []float64{800, 1200},
- Attack: 0.05, Release: 0.15,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.065, BPM: 200, Wave: "square",
DroneFreqs: []float64{100, 200},
- PulseFreqs: []float64{400, 800, 1600},
- Attack: 0.02, Release: 0.08,
+ 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),
+ },
}
return s
}
@@ -360,16 +402,21 @@ func soundsRetro() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 80, Wave: "square",
DroneFreqs: []float64{523.25, 659.25},
- PulseFreqs: []float64{1046.5},
- Attack: 0.1, Release: 0.3,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 155, Wave: "square",
DroneFreqs: []float64{261.63, 523.25},
- PulseFreqs: []float64{1046.5, 2093},
- Attack: 0.05, Release: 0.15,
- DetuneCents: 30,
- NoiseGain: 0.02,
+ 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),
+ },
}
return s
}
@@ -385,14 +432,20 @@ func soundsCosmos() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 30, Wave: "sine",
DroneFreqs: []float64{220, 440, 660},
- PulseFreqs: []float64{880},
- Attack: 1.5, Release: 3.0,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.05, BPM: 75, Wave: "triangle",
DroneFreqs: []float64{110, 220, 440},
- PulseFreqs: []float64{660, 880},
- Attack: 0.3, Release: 0.8,
+ 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),
+ },
}
return s
}
@@ -408,21 +461,25 @@ func soundsRetrofuture() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 55, Wave: "triangle",
DroneFreqs: []float64{220, 330, 440},
- PulseFreqs: []float64{523.25},
- Attack: 0.8, Release: 1.8,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 120, Wave: "square",
DroneFreqs: []float64{110, 220},
- PulseFreqs: []float64{440, 880},
- Attack: 0.05, Release: 0.15,
- Rhythm: []float64{1, 2, 0.5, 1.5},
+ 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),
+ },
}
return s
}
// soundsSpaceage returns synth parameters for the Space Age theme.
-// Clean teal-toned tones evoke retro-futuristic space-age electronics.
func soundsSpaceage() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{440, 554.37, 659.25, 880}
@@ -434,22 +491,25 @@ func soundsSpaceage() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 50, Wave: "sine",
DroneFreqs: []float64{440, 880},
- PulseFreqs: []float64{1320},
- Attack: 0.8, Release: 1.5,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.05, BPM: 110, Wave: "triangle",
DroneFreqs: []float64{220, 440},
- PulseFreqs: []float64{880, 1320},
- Attack: 0.1, Release: 0.4,
- PulseInterval: 0.5,
+ 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),
+ },
}
return s
}
// soundsTropical returns synth parameters for the Tropical Beach theme.
-// Warm sine arpeggio across a C-major pentatonic — steel drum impression on splash.
-// Nav uses a breathy mid-freq sine "bird tone"; open/close sweep like breaking surf.
func soundsTropical() themeSounds {
var s themeSounds
s.Splash.Freqs = []float64{523.25, 659.25, 783.99, 1046.5}
@@ -461,17 +521,20 @@ func soundsTropical() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 65, Wave: "sine",
DroneFreqs: []float64{261.63, 329.63, 392, 523.25},
- PulseFreqs: []float64{659.25},
- Attack: 0.6, Release: 1.5,
- NoiseGain: 0.015,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 140, Wave: "triangle",
DroneFreqs: []float64{130.81, 261.63, 329.63, 392},
- PulseFreqs: []float64{523.25, 659.25},
- Attack: 0.1, Release: 0.3,
- NoiseGain: 0.03,
- Rhythm: []float64{0.5, 1, 0.5, 2},
+ 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),
+ },
}
return s
}
@@ -487,16 +550,20 @@ func soundsNoir() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.02, BPM: 40, Wave: "sine",
DroneFreqs: []float64{174.61, 220, 261.63},
- PulseFreqs: []float64{330},
- Attack: 1.0, Release: 2.0,
- NoiseGain: 0.015,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.05, BPM: 90, Wave: "triangle",
DroneFreqs: []float64{130.81, 174.61, 220},
- PulseFreqs: []float64{330, 440},
- Attack: 0.3, Release: 0.7,
- PulseInterval: 1.2,
+ 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),
+ },
}
return s
}
@@ -512,14 +579,20 @@ func soundsCathedral() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 35, Wave: "sine",
DroneFreqs: []float64{293.66, 440, 587.33},
- PulseFreqs: []float64{880},
- Attack: 1.5, Release: 3.0,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 70, Wave: "triangle",
DroneFreqs: []float64{146.83, 293.66, 440},
- PulseFreqs: []float64{587.33, 880, 1174.66},
- Attack: 0.3, Release: 0.8,
+ 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),
+ },
}
return s
}
@@ -535,15 +608,19 @@ func soundsSurveillance() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 55, Wave: "square",
DroneFreqs: []float64{440, 660},
- PulseFreqs: []float64{880},
- Attack: 0.2, Release: 0.5,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.06, BPM: 140, Wave: "square",
DroneFreqs: []float64{220, 440},
- PulseFreqs: []float64{880, 1760},
- Attack: 0.05, Release: 0.15,
- PulseInterval: 0.3,
+ 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),
+ },
}
return s
}
@@ -559,17 +636,20 @@ func soundsBiomech() themeSounds {
s.Ambient.Normal = ambientPreset{
Gain: 0.025, BPM: 50, Wave: "triangle",
DroneFreqs: []float64{164.81, 246.94},
- PulseFreqs: []float64{440},
- Attack: 0.7, Release: 1.5,
- DetuneCents: 8,
+ 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),
+ },
}
s.Ambient.Wild = ambientPreset{
Gain: 0.055, BPM: 115, Wave: "square",
DroneFreqs: []float64{82.41, 164.81},
- PulseFreqs: []float64{440, 880},
- Attack: 0.1, Release: 0.3,
- DetuneCents: 18,
- Rhythm: []float64{1, 0.5, 1.5, 0.75},
+ 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),
+ },
}
return s
}