// sfxc synthesizes retro game sound effects from .sfx text files // (sfxr-style parameters) and writes 16-bit mono WAV. package main import ( "flag" "fmt" "os" "path/filepath" "strings" "gitea.brasse-pc.eu/brasse/agent-tools/sfx-maker/sfx" ) var version = "dev" const usage = `sfxc - sound effect maker for agents Usage: sfxc build [-o out.wav] synthesize a .sfx file to WAV sfxc preset [-o out.sfx|out.wav] [--seed n] write a preset (editable .sfx text, or straight to .wav) sfxc info validate + print parameters sfxc version Presets: blip, coin, explosion, hurt, jump, laser, powerup --seed 0 (default) is the canonical sound; other seeds give variants. The .sfx format (all keys optional, '#' comments): sfx: jump name wave: square square | saw | sine | triangle | noise volume: 0.7 0..1 attack: 0.01 seconds: fade in sustain: 0.08 hold decay: 0.18 fade out freq: 330 start pitch, Hz freq-slide: 900 Hz per second (negative = falling) duty: 0.5 square pulse width 0.05..0.95 vibrato-depth: 25 Hz vibrato-rate: 9 Hz arpeggio: 1.335 pitch multiplier that kicks in at... arpeggio-time: 0.06 ...this many seconds lowpass: 2200 filter cutoff Hz highpass: 300 filter cutoff Hz sample-rate: 44100 seed: 1 noise randomness (deterministic) Play the result with e.g.: mpv out.wav or aplay out.wav ` func main() { if len(os.Args) < 2 { fmt.Print(usage) os.Exit(2) } switch os.Args[1] { case "build": cmdBuild(os.Args[2:]) case "preset": cmdPreset(os.Args[2:]) case "info": cmdInfo(os.Args[2:]) case "version", "--version", "-v": fmt.Println("sfxc", version) case "help", "--help", "-h": fmt.Print(usage) default: die("unknown command %q — run 'sfxc help'", os.Args[1]) } } // parseInterspersed lets flags appear before or after positional args. func parseInterspersed(fs *flag.FlagSet, args []string) { var flags, pos []string for i := 0; i < len(args); i++ { a := args[i] if len(a) > 1 && a[0] == '-' { flags = append(flags, a) name := strings.TrimLeft(a, "-") if !strings.Contains(name, "=") { f := fs.Lookup(name) isBool := false if f != nil { if bv, ok := f.Value.(interface{ IsBoolFlag() bool }); ok && bv.IsBoolFlag() { isBool = true } } if !isBool && i+1 < len(args) { i++ flags = append(flags, args[i]) } } } else { pos = append(pos, a) } } fs.Parse(append(flags, pos...)) } func writeWAVFile(path string, p *sfx.Params) error { samples := sfx.Render(p) f, err := os.Create(path) if err != nil { return err } defer f.Close() if err := sfx.WriteWAV(f, samples, p.SampleRate); err != nil { return err } return f.Close() } func cmdBuild(args []string) { fs := flag.NewFlagSet("build", flag.ExitOnError) out := fs.String("o", "", "output .wav (default: input name with .wav)") parseInterspersed(fs, args) if fs.NArg() != 1 { die("build takes exactly one .sfx file") } p, err := sfx.ParseFile(fs.Arg(0)) if err != nil { die("%v", err) } path := *out if path == "" { path = strings.TrimSuffix(fs.Arg(0), filepath.Ext(fs.Arg(0))) + ".wav" } if err := writeWAVFile(path, p); err != nil { die("%v", err) } fmt.Printf("%s (%s, %.2fs, %d Hz)\n", path, p.Wave, p.Duration(), p.SampleRate) } func cmdPreset(args []string) { fs := flag.NewFlagSet("preset", flag.ExitOnError) out := fs.String("o", "", "output file: .sfx (editable text) or .wav (rendered)") seed := fs.Int64("seed", 0, "0 = canonical, other values = variants") parseInterspersed(fs, args) if fs.NArg() != 1 { die("preset takes exactly one preset name (%s)", sfx.PresetNames()) } name := fs.Arg(0) p, err := sfx.Preset(name, *seed) if err != nil { die("%v", err) } path := *out if path == "" { path = name + ".sfx" } switch strings.ToLower(filepath.Ext(path)) { case ".wav": if err := writeWAVFile(path, p); err != nil { die("%v", err) } fmt.Printf("%s (%s preset, seed %d, %.2fs)\n", path, name, *seed, p.Duration()) case ".sfx": f, err := os.Create(path) if err != nil { die("%v", err) } defer f.Close() comment := fmt.Sprintf("%s preset (seed %d) — edit freely, then: sfxc build %s", name, *seed, path) if err := p.Write(f, comment); err != nil { die("%v", err) } fmt.Printf("%s (%s preset, seed %d — edit then 'sfxc build')\n", path, name, *seed) default: die("-o must end in .sfx or .wav") } } func cmdInfo(args []string) { if len(args) != 1 { die("info takes exactly one .sfx file") } p, err := sfx.ParseFile(args[0]) if err != nil { die("%v", err) } fmt.Printf("sfx: %s\n", p.Name) fmt.Printf("wave: %s\n", p.Wave) fmt.Printf("duration: %.3fs (attack %.3g + sustain %.3g + decay %.3g)\n", p.Duration(), p.Attack, p.Sustain, p.Decay) if p.Wave != "noise" { fmt.Printf("freq: %g Hz", p.Freq) if p.FreqSlide != 0 { fmt.Printf(" (slide %+g Hz/s)", p.FreqSlide) } fmt.Println() } if p.ArpFactor != 0 { fmt.Printf("arpeggio: x%g at %gs\n", p.ArpFactor, p.ArpTime) } if p.VibratoDepth > 0 { fmt.Printf("vibrato: ±%g Hz at %g Hz\n", p.VibratoDepth, p.VibratoRate) } if p.LowPass > 0 { fmt.Printf("lowpass: %g Hz\n", p.LowPass) } if p.HighPass > 0 { fmt.Printf("highpass: %g Hz\n", p.HighPass) } fmt.Printf("volume: %g\nsamplerate:%d\n", p.Volume, p.SampleRate) fmt.Println("valid: yes") } func die(format string, a ...any) { fmt.Fprintf(os.Stderr, "sfxc: "+format+"\n", a...) os.Exit(1) }