synth

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package synth renders the family's procedural sound effects and loops as interleaved 16-bit stereo PCM at SampleRate, ready for an Ebiten/oto audio player.

Render is the core loop: it samples a generator function over a duration, clips, and interleaves. Sine, Noise, Env, and Attack are the building blocks; Pan and Panned place a finished sound in the stereo field. On top of those sit the parameterised cue shapes the games share — Blip, TwoTone, Arpeggio, Slide, Rumble, Thud, and Drone — while each game's cue enums and sound design stay its own.

Everything is deterministic: noise takes explicit seeds, so a cue renders sample-identical everywhere.

Index

Examples

Constants

View Source
const (
	// SampleRate is the render and playback rate in Hz.
	SampleRate = 44100
	// ChannelCount is the number of interleaved channels (stereo).
	ChannelCount = 2
	// BitDepthInBytes is the sample width (16-bit).
	BitDepthInBytes = 2
	// BytesPerFrame is the size of one interleaved sample frame.
	BytesPerFrame = ChannelCount * BitDepthInBytes
)

Audio format constants shared by every player in the family.

Variables

This section is empty.

Functions

func Arpeggio

func Arpeggio(notes []float64, step, dur, decay float64) []byte

Arpeggio steps through notes at the given interval, restarting the decay envelope on each note — the conventional secret/fanfare shape.

Example

ExampleArpeggio renders the family's conventional secret-found fanfare: a C5-E5-G5 arpeggio.

package main

import (
	"fmt"

	"github.com/danielriddell21/crucible/synth"
)

func main() {
	pcm := synth.Arpeggio([]float64{523, 659, 784}, 0.15, 0.45, 10)
	fmt.Printf("%.2fs\n", float64(len(pcm)/synth.BytesPerFrame)/synth.SampleRate)
}
Output:
0.45s

func Attack

func Attack(t, rate float64) float64

Attack is a linear rise envelope: 0 at t=0 reaching 1 at t=1/rate.

func Blip

func Blip(freq, dur, decay float64) []byte

Blip is a short pure tone with a fast decay — the family's menu tick.

func Drone

func Drone(voices []Voice, lfoRate, lfoDepth, dur float64) []byte

Drone layers the given voices (frequency, amplitude pairs) under a slow sine LFO — the conventional ambient bed. The LFO rate is in Hz.

func Env

func Env(t, decay float64) float64

Env is an exponential decay envelope: 1 at t=0 falling at the given rate.

func Noise

func Noise(seed1, seed2 uint64) func() float64

Noise returns a deterministic white-noise sampler seeded with the given values. Each call to the sampler yields a value in [-1, 1].

func Pan

func Pan(bearing float64) (left, right float64)

Pan converts a bearing relative to the listener's facing (0 = dead ahead, positive to the right) into constant-power left/right channel gains in [0, 1]. Sounds ahead or behind sit centred; sounds to a side swing toward that ear.

Example

ExamplePan places a sound by its bearing from the listener's facing: ahead is centred, hard right favours the right ear.

package main

import (
	"fmt"
	"math"

	"github.com/danielriddell21/crucible/synth"
)

func main() {
	l, r := synth.Pan(0)
	fmt.Printf("ahead  %.2f %.2f\n", l, r)
	l, r = synth.Pan(math.Pi / 2)
	fmt.Printf("right  %.2f %.2f\n", l, r)
}
Output:
ahead  0.71 0.71
right  0.00 1.00

func Panned

func Panned(pcm []byte, left, right float64) []byte

Panned returns a copy of interleaved 16-bit stereo PCM with the left and right channels scaled by the given gains.

func Render

func Render(dur float64, gen func(t float64) float64) []byte

Render synthesises dur seconds of audio by sampling gen at each frame time. gen returns an amplitude in [-1, 1]; values outside are clipped. Both stereo channels carry the same signal — use Panned to place a sound.

Example

ExampleRender synthesises half a second of a decaying 440 Hz tone — the bones of every cue in the family.

package main

import (
	"fmt"

	"github.com/danielriddell21/crucible/synth"
)

func main() {
	pcm := synth.Render(0.5, func(t float64) float64 {
		return 0.5 * synth.Sine(440, t) * synth.Env(t, 6)
	})
	fmt.Println(len(pcm) / synth.BytesPerFrame)
}
Output:
22050

func Rumble

func Rumble(base, rise, dur float64, seed1, seed2 uint64) []byte

Rumble is a rising low tone with a noise grit layer and a brief attack — the conventional door sound.

func Sine

func Sine(freq, t float64) float64

Sine is a sine oscillator at freq Hz evaluated at time t.

func Slide

func Slide(f0, rate, floor, dur, decay float64) []byte

Slide sweeps a tone from f0 by rate Hz per second, clamped at floor — the conventional downward death groan when rate is negative.

func Thud

func Thud(freq, dur, decay float64, seed1, seed2 uint64) []byte

Thud is a low tone mixed with noise under a hard decay — the conventional impact sound.

func TwoTone

func TwoTone(f1, f2, split, dur, decay float64) []byte

TwoTone plays f1 then jumps to f2 at the split point — the conventional pickup chirp.

func WAV added in v0.15.2

func WAV(pcm []byte) []byte

WAV wraps rendered PCM in a RIFF/WAVE header, describing it with the package's own format constants.

Render and the cue shapes above it produce raw interleaved samples, which suits an Ebiten or oto player because those take a stream of them directly. Most other front-ends do not: raylib, SDL and the browser all load an encoded file, and will not accept bare samples however well described. Forty-four bytes of header is the whole difference, so this saves every non-Ebiten caller from carrying its own copy of a fiddly little spec.

sound := rl.LoadSoundFromWave(rl.LoadWaveFromMemory(".wav", w, int32(len(w))))

The returned slice is a fresh buffer; pcm is not retained or modified.

Types

type Voice

type Voice struct {
	Freq, Amp float64
}

Voice is one layer of a Drone: a frequency and its amplitude.

Jump to

Keyboard shortcuts

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