diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-26 09:33:29 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-26 09:33:29 +0300 |
| commit | 44f82e79f0b1a65d9df62c3fed7271affcb0a673 (patch) | |
| tree | 4a41b3065e0d3f39cc8135ed925c216107312de4 | |
| parent | eebb1df14b26b65493702c090d13e1a9c7563db6 (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.go | 16 | ||||
| -rw-r--r-- | internal/generator/templates/shared/shared.js | 65 | ||||
| -rw-r--r-- | internal/generator/theme_sounds.go | 340 |
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 } |
