gofxr

package module
v0.0.0-...-21e76d2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 8 Imported by: 0

README

gofxr

Quick and easy game sound effects generator for Go. A bit-exact port of jsfxr (which powers sfxr.me), itself a port of DrPetter's classic sfxr.

  • Zero dependencies, pure Go.
  • Eleven preset algorithms: pickupCoin, laserShoot, explosion, powerUp, hitHurt, jump, blipSelect, synth, tone, click, random.
  • Reads and writes the base58 sound strings used in sfxr.me share URLs, and jsfxr JSON synthdefs — design sounds in the browser, render them in Go.
  • Renders to normalized float64 samples, raw 8/16-bit PCM, or WAV.
  • Deterministic: pass a seeded RNG and get the same sound every time, on every platform.
  • Byte-for-byte identical output to jsfxr (see Fidelity).

Install

go get github.com/domano/gofxr

Or install the CLI:

go install github.com/domano/gofxr/cmd/gofxr@latest

Usage

Generate a random sound from a preset and save it as WAV:

params, err := gofxr.Generate(gofxr.PresetExplosion, nil)
if err != nil { ... }
params.SampleSize = 16 // default is 8-bit, like jsfxr

sound := params.Render(nil)

f, _ := os.Create("boom.wav")
defer f.Close()
sound.WriteWAV(f)

Design a sound on sfxr.me, copy the share URL, and render it in your game:

params, err := gofxr.FromB58("https://sfxr.me/#34T6Pknn6QP7YJBMdYAFzZ...")
sound := params.Render(nil)

gofxr.FromJSON loads the JSON synthdefs that sfxr.me exports, and params.ToB58() / params.JSON() go the other way — a sound you generated in Go can be opened on sfxr.me for tweaking.

Reproducible sounds

Anything random accepts a gofxr.RNG (satisfied by *rand.Rand from math/rand/v2). Pass nil for the shared global generator, or seed one for sounds that are identical on every run and every machine:

rng := rand.New(rand.NewPCG(42, 0))
params, _ := gofxr.Generate(gofxr.PresetJump, rng)
sound := params.Render(rng) // rng also drives the noise wave type

params.Mutate(rng) nudges an existing sound slightly — handy for variation on repeated effects (footsteps, shots) without storing multiple sounds.

Hand-crafted sounds

All synthesis parameters are exported fields on gofxr.Params (envelope, frequency slides, vibrato, arpeggiation, duty cycle, retrigger, flanger, and low/high-pass filters), matching the sliders on sfxr.me. Start from gofxr.NewParams() and set fields directly; each field's comment names its jsfxr equivalent.

Playing in a game engine

Sound.Samples holds normalized float64 samples, and Sound.PCM holds the quantized bytes (unsigned for 8-bit, little-endian signed for 16-bit), mono at Sound.SampleRate. For Ebitengine:

ctx := audio.NewContext(44100)

params, _ := gofxr.Generate(gofxr.PresetPickupCoin, nil)
params.SampleSize = 16
pcm := params.Render(nil).PCM

// duplicate mono samples into the stereo interleaved format Ebitengine expects
stereo := make([]byte, 0, len(pcm)*2)
for i := 0; i < len(pcm); i += 2 {
    stereo = append(stereo, pcm[i], pcm[i+1], pcm[i], pcm[i+1])
}
audio.NewPlayerFromBytes(ctx, stereo).Play()

CLI

Usage: gofxr [flags] [preset | b58-string | sfxr.me URL]

gofxr laserShoot                             # random laser -> laserShoot.wav
gofxr -preset explosion -seed 42 -bits 16    # reproducible 16-bit explosion
gofxr 'https://sfxr.me/#34T6Pknn6QP7...'     # render an sfxr.me sound
gofxr -json sound.json -o sound.wav          # render an exported synthdef
gofxr -preset powerUp -print-b58             # print the sfxr.me string too

Flags: -o output path, -seed reproducible generation, -rate/-bits/-vol overrides, -print-b58/-print-json to export the sound definition.

Fidelity

This is not a "sounds the same" port; it is a "same bytes" port:

  • Given the same parameters, Render produces PCM that is byte-for-byte identical to jsfxr running under Node — verified by golden fixtures (testdata/jsfxr_fixtures.json) generated by running the actual jsfxr code with a seeded PRNG shared by the Go tests, across all presets, bit depths and sample rates, including the noise wave type.
  • ToB58 output matches jsfxr's encoder exactly (including its float truncation quirks), so strings round-trip between gofxr and sfxr.me.
  • JavaScript's Math.pow/sin/exp/log differ from Go's math package by one ulp on some inputs, and tiny differences get amplified by the synth's recursive filters. gofxr therefore ships ports of V8's fdlibm implementations (jspow.go, jsmath.go), validated bit-for-bit against V8 on hundreds of thousands of inputs (testdata/jsmath_fixtures.json).
  • Go may fuse floating-point multiply-adds on some architectures (e.g. arm64), which rounds differently from JavaScript. The synthesis code uses explicit float64() conversions as fusion barriers, so output is identical across architectures. Do not remove these conversions — they look redundant but are load-bearing.

Regenerating fixtures requires Node (see the gen_*.js scripts in testdata/); running the tests does not.

Known deviations from jsfxr, all intentional:

  • Sample rates above 44100 Hz render unresampled instead of dividing by zero.
  • FromB58 validates input and returns errors instead of producing NaNs.
  • Mutate skips jsfxr's mutation of p_vib_delay, an unused field that jsfxr corrupts to NaN (the RNG stream stays aligned).

Credits and license

MIT (see LICENSE). Derived from jsfxr by Chris McCormick (Unlicense), based on sfxr by Tomas Pettersson. jspow.go and jsmath.go are ported from V8's fdlibm-derived src/base/ieee754.cc, © Sun Microsystems, used under its permissive notice (preserved in the source files).

Documentation

Overview

Package gofxr generates retro game sound effects. It is a Go port of jsfxr (https://github.com/chr15m/jsfxr), which is itself a port of DrPetter's sfxr. Sounds are described by a Params struct, which can be randomized with preset algorithms (PickupCoin, LaserShoot, Explosion, ...), serialized to/from the base58 format used by https://sfxr.me share URLs, and rendered to raw PCM or WAV.

The port is faithful to jsfxr: identical parameters produce identical audio, and base58 strings are interchangeable between the two libraries.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Params

type Params struct {
	WaveType WaveType `json:"wave_type"`

	// Envelope
	EnvAttack  float64 `json:"p_env_attack"`  // Attack time
	EnvSustain float64 `json:"p_env_sustain"` // Sustain time
	EnvPunch   float64 `json:"p_env_punch"`   // Sustain punch
	EnvDecay   float64 `json:"p_env_decay"`   // Decay time

	// Tone
	BaseFreq  float64 `json:"p_base_freq"`  // Start frequency
	FreqLimit float64 `json:"p_freq_limit"` // Min frequency cutoff
	FreqRamp  float64 `json:"p_freq_ramp"`  // Slide (SIGNED)
	FreqDramp float64 `json:"p_freq_dramp"` // Delta slide (SIGNED)

	// Vibrato
	VibStrength float64 `json:"p_vib_strength"` // Vibrato depth
	VibSpeed    float64 `json:"p_vib_speed"`    // Vibrato speed

	// Tonal change
	ArpMod   float64 `json:"p_arp_mod"`   // Change amount (SIGNED)
	ArpSpeed float64 `json:"p_arp_speed"` // Change speed

	// Square wave duty (proportion of time signal is high vs. low)
	Duty     float64 `json:"p_duty"`      // Square duty
	DutyRamp float64 `json:"p_duty_ramp"` // Duty sweep (SIGNED)

	// Repeat
	RepeatSpeed float64 `json:"p_repeat_speed"` // Repeat speed

	// Flanger
	PhaOffset float64 `json:"p_pha_offset"` // Flanger offset (SIGNED)
	PhaRamp   float64 `json:"p_pha_ramp"`   // Flanger sweep (SIGNED)

	// Low-pass filter
	LpfFreq      float64 `json:"p_lpf_freq"`      // Low-pass filter cutoff
	LpfRamp      float64 `json:"p_lpf_ramp"`      // Low-pass filter cutoff sweep (SIGNED)
	LpfResonance float64 `json:"p_lpf_resonance"` // Low-pass filter resonance

	// High-pass filter
	HpfFreq float64 `json:"p_hpf_freq"` // High-pass filter cutoff
	HpfRamp float64 `json:"p_hpf_ramp"` // High-pass filter cutoff sweep (SIGNED)

	// Sample parameters
	SoundVol   float64 `json:"sound_vol"`
	SampleRate int     `json:"sample_rate"` // 44100, 22050, 11025 or 5512
	SampleSize int     `json:"sample_size"` // bits per sample: 8 or 16
}

Params holds the sound generation parameters. All float parameters are on [0,1] unless marked SIGNED, which are on [-1,1]. The JSON encoding matches jsfxr's synthdef format, so files exported from https://sfxr.me load directly with FromJSON.

func FromB58

func FromB58(s string) (*Params, error)

FromB58 decodes a jsfxr/sfxr.me base58 sound string. A leading "#" (or a full sfxr.me URL) is accepted. Parameters not covered by the encoding (volume, sample rate, sample size) get the NewParams defaults.

Example

Render a sound designed on https://sfxr.me by pasting its share URL.

package main

import (
	"fmt"

	"github.com/domano/gofxr"
)

func main() {
	params, err := gofxr.FromB58("https://sfxr.me/#34T6Pknn6QP7YJBMdYAFzZHibZojmNg4iwGuSayTBZuHpEKgakmZq8mD9LSKHrE8GN9988sAmVMxvFZgkeFaUCr5XV7bv3YTnZyWHMAbZeArr84YxA7Vhf5xB")
	if err != nil {
		panic(err)
	}
	sound := params.Render(nil)
	fmt.Println(sound.SampleRate)
}
Output:
44100

func FromJSON

func FromJSON(data []byte) (*Params, error)

FromJSON parses a jsfxr synthdef JSON object (as exported by sfxr.me). Fields absent from the JSON keep the NewParams defaults.

func Generate

func Generate(preset Preset, rng RNG) (*Params, error)

Generate creates parameters using a named preset, mirroring jsfxr's sfxr.generate: volume 0.25, 44100 Hz, 8-bit. A nil rng uses the shared math/rand generator.

Example

Generate a random coin sound and write it to a WAV file. Seeding the RNG makes the sound reproducible across runs.

package main

import (
	"math/rand/v2"
	"os"

	"github.com/domano/gofxr"
)

func main() {
	rng := rand.New(rand.NewPCG(42, 0))
	params, err := gofxr.Generate(gofxr.PresetPickupCoin, rng)
	if err != nil {
		panic(err)
	}
	params.SampleSize = 16

	f, err := os.CreateTemp("", "coin-*.wav")
	if err != nil {
		panic(err)
	}
	defer os.Remove(f.Name())
	defer f.Close()

	if err := params.Render(rng).WriteWAV(f); err != nil {
		panic(err)
	}
}

func NewParams

func NewParams() *Params

NewParams returns parameters with the same defaults as jsfxr's Params constructor: a short square-wave blip at 44100 Hz, 8-bit.

func (*Params) BlipSelect

func (p *Params) BlipSelect(rng RNG) *Params

BlipSelect randomizes the parameters into a blip/menu selection sound.

func (*Params) Click

func (p *Params) Click(rng RNG) *Params

Click randomizes the parameters into a short click, built on top of the Explosion or HitHurt presets.

func (*Params) Explosion

func (p *Params) Explosion(rng RNG) *Params

Explosion randomizes the parameters into an explosion sound.

func (*Params) HitHurt

func (p *Params) HitHurt(rng RNG) *Params

HitHurt randomizes the parameters into a hit/hurt sound.

func (*Params) JSON

func (p *Params) JSON() ([]byte, error)

JSON encodes the parameters as a jsfxr-compatible synthdef object.

func (*Params) Jump

func (p *Params) Jump(rng RNG) *Params

Jump randomizes the parameters into a jump sound.

func (*Params) LaserShoot

func (p *Params) LaserShoot(rng RNG) *Params

LaserShoot randomizes the parameters into a laser/shoot sound.

func (*Params) Mutate

func (p *Params) Mutate(rng RNG) *Params

Mutate randomly nudges each parameter by up to ±0.05 with 50% probability, like jsfxr's mutate. (jsfxr also touches an unused, never-serialized p_vib_delay field; that has no audible effect and is omitted here.)

func (*Params) PickupCoin

func (p *Params) PickupCoin(rng RNG) *Params

PickupCoin randomizes the parameters into a coin/point pickup sound.

func (*Params) PowerUp

func (p *Params) PowerUp(rng RNG) *Params

PowerUp randomizes the parameters into a power-up sound.

func (*Params) Random

func (p *Params) Random(rng RNG) *Params

Random fully randomizes the parameters.

func (*Params) Render

func (p *Params) Render(rng RNG) *Sound

Render synthesizes the sound described by the parameters. The rng is only consumed by the Noise wave type (and a fixed 32 draws of priming that jsfxr performs for every wave type); a nil rng uses the shared math/rand generator. Render does not modify p and may be called repeatedly.

It panics if p.WaveType is not one of the four defined shapes, matching jsfxr's throw.

Example

The tone preset is deterministic: a one-second 440 Hz sine.

package main

import (
	"fmt"

	"github.com/domano/gofxr"
)

func main() {
	sound := gofxr.NewParams().Tone().Render(nil)
	fmt.Println(sound.SampleRate, sound.BitDepth, len(sound.Samples))
}
Output:
44100 8 44104

func (*Params) Synth

func (p *Params) Synth(rng RNG) *Params

Synth randomizes the parameters into a synth note.

func (*Params) ToB58

func (p *Params) ToB58() string

ToB58 encodes the parameters as a base58 string compatible with jsfxr and https://sfxr.me share URLs (the part after the #).

func (*Params) Tone

func (p *Params) Tone() *Params

Tone sets the parameters to a one-second 440 Hz sine tone. It is the only preset that is deterministic.

type Preset

type Preset string

Preset names a randomized sound design algorithm, matching the preset names used by jsfxr and sfxr.me.

const (
	PresetPickupCoin Preset = "pickupCoin"
	PresetLaserShoot Preset = "laserShoot"
	PresetExplosion  Preset = "explosion"
	PresetPowerUp    Preset = "powerUp"
	PresetHitHurt    Preset = "hitHurt"
	PresetJump       Preset = "jump"
	PresetBlipSelect Preset = "blipSelect"
	PresetSynth      Preset = "synth"
	PresetTone       Preset = "tone"
	PresetClick      Preset = "click"
	PresetRandom     Preset = "random"
)

func Presets

func Presets() []Preset

Presets lists all preset algorithms.

type RNG

type RNG interface {
	Float64() float64
}

RNG is the source of randomness used by the preset generators and by noise-wave synthesis. *rand.Rand from math/rand/v2 satisfies it. Passing a nil RNG to any function in this package uses the shared math/rand/v2 generator; pass a seeded source for reproducible sounds.

type Sound

type Sound struct {
	SampleRate int
	BitDepth   int // bits per sample in PCM: 8 or 16

	// Samples holds the normalized floating point samples. Values are
	// nominally in [-1, 1] but can exceed it; clipping is only applied
	// during PCM quantization.
	Samples []float64

	// PCM holds the quantized samples: unsigned bytes for BitDepth 8,
	// signed little-endian for BitDepth 16.
	PCM []byte

	// Clipped counts the samples that were clamped during quantization.
	Clipped int
}

Sound is rendered audio: a single mono channel both as normalized floats and as quantized PCM.

func (*Sound) Duration

func (s *Sound) Duration() time.Duration

Duration returns the length of the sound.

func (*Sound) WAV

func (s *Sound) WAV() []byte

WAV encodes the sound as a mono RIFF WAVE file (PCM, 8 or 16-bit).

func (*Sound) WriteWAV

func (s *Sound) WriteWAV(w io.Writer) error

WriteWAV writes the sound to w as a mono RIFF WAVE file.

type WaveType

type WaveType int

WaveType selects the oscillator shape.

const (
	Square   WaveType = 0
	Sawtooth WaveType = 1
	Sine     WaveType = 2
	Noise    WaveType = 3
)

Directories

Path Synopsis
cmd
gofxr command
Command gofxr generates retro game sound effects as WAV files, using the same algorithms and formats as jsfxr / https://sfxr.me.
Command gofxr generates retro game sound effects as WAV files, using the same algorithms and formats as jsfxr / https://sfxr.me.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL