Files
agent-tools/sfx-maker/sfx/presets.go
claude e7ac31b5c8 sfx-maker: sfxc - sfxr-style .sfx text presets -> 16-bit WAV synthesis
- 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
2026-07-14 07:08:04 +02:00

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
}