- waves: square (duty), saw, sine, triangle, pitched noise (seeded, deterministic) - envelope attack/sustain/decay, freq slide, vibrato, arpeggio jump, one-pole low/high-pass filters, clamped output - presets blip/coin/explosion/hurt/jump/laser/powerup with seeded variants; preset writes editable .sfx or renders .wav directly - go tests: parse/validate, determinism, per-preset RMS, WAV header roundtrip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmdG9GqfSWCzts7AkDwRDh
115 lines
2.4 KiB
Go
115 lines
2.4 KiB
Go
package sfx
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
)
|
|
|
|
// Preset returns ready-made parameters for classic game sounds.
|
|
// seed 0 gives the canonical version; other seeds vary it slightly so
|
|
// agents can generate alternatives ("give me three coin variants").
|
|
func Preset(name string, seed int64) (*Params, error) {
|
|
p := Defaults()
|
|
p.Name = name
|
|
switch name {
|
|
case "jump":
|
|
p.Wave = "square"
|
|
p.Duty = 0.5
|
|
p.Freq = 330
|
|
p.FreqSlide = 900
|
|
p.Attack = 0.01
|
|
p.Sustain = 0.08
|
|
p.Decay = 0.18
|
|
case "coin":
|
|
p.Wave = "square"
|
|
p.Duty = 0.5
|
|
p.Freq = 988
|
|
p.ArpFactor = 1.335 // up a fourth: B5 -> E6
|
|
p.ArpTime = 0.06
|
|
p.Attack = 0.005
|
|
p.Sustain = 0.08
|
|
p.Decay = 0.25
|
|
case "laser":
|
|
p.Wave = "saw"
|
|
p.Freq = 1400
|
|
p.FreqSlide = -6000
|
|
p.Attack = 0.005
|
|
p.Sustain = 0.05
|
|
p.Decay = 0.12
|
|
p.HighPass = 300
|
|
case "explosion":
|
|
p.Wave = "noise"
|
|
p.Freq = 900
|
|
p.FreqSlide = -600
|
|
p.Attack = 0.01
|
|
p.Sustain = 0.15
|
|
p.Decay = 0.55
|
|
p.LowPass = 2200
|
|
case "hurt":
|
|
p.Wave = "saw"
|
|
p.Freq = 300
|
|
p.FreqSlide = -700
|
|
p.Attack = 0.005
|
|
p.Sustain = 0.04
|
|
p.Decay = 0.14
|
|
case "powerup":
|
|
p.Wave = "square"
|
|
p.Duty = 0.4
|
|
p.Freq = 220
|
|
p.FreqSlide = 700
|
|
p.VibratoDepth = 25
|
|
p.VibratoRate = 9
|
|
p.Attack = 0.01
|
|
p.Sustain = 0.25
|
|
p.Decay = 0.25
|
|
case "blip":
|
|
p.Wave = "square"
|
|
p.Duty = 0.4
|
|
p.Freq = 660
|
|
p.Attack = 0.002
|
|
p.Sustain = 0.03
|
|
p.Decay = 0.05
|
|
default:
|
|
return nil, fmt.Errorf("unknown preset %q (available: %s)", name, PresetNames())
|
|
}
|
|
if seed != 0 {
|
|
vary(&p, seed)
|
|
}
|
|
if err := p.Validate(); err != nil {
|
|
return nil, fmt.Errorf("preset %s (seed %d): %w", name, seed, err)
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// vary nudges the tonal parameters deterministically from the seed.
|
|
func vary(p *Params, seed int64) {
|
|
rng := rand.New(rand.NewSource(seed))
|
|
jitter := func(v, amount float64) float64 {
|
|
return v * (1 + amount*(rng.Float64()*2-1))
|
|
}
|
|
p.Freq = jitter(p.Freq, 0.15)
|
|
p.FreqSlide = jitter(p.FreqSlide, 0.25)
|
|
p.Sustain = jitter(p.Sustain, 0.2)
|
|
p.Decay = jitter(p.Decay, 0.2)
|
|
if p.ArpFactor != 0 {
|
|
p.ArpFactor = jitter(p.ArpFactor, 0.05)
|
|
}
|
|
p.Seed = seed // noise variation too
|
|
}
|
|
|
|
var presetNames = []string{"blip", "coin", "explosion", "hurt", "jump", "laser", "powerup"}
|
|
|
|
// PresetNames lists the available presets, sorted.
|
|
func PresetNames() string {
|
|
sort.Strings(presetNames)
|
|
out := ""
|
|
for i, n := range presetNames {
|
|
if i > 0 {
|
|
out += ", "
|
|
}
|
|
out += n
|
|
}
|
|
return out
|
|
}
|