- 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
55 lines
1.8 KiB
Go
55 lines
1.8 KiB
Go
package sfx
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
)
|
|
|
|
// WriteWAV encodes samples ([-1,1] floats) as a 16-bit mono PCM WAV.
|
|
func WriteWAV(w io.Writer, samples []float64, sampleRate int) error {
|
|
dataLen := len(samples) * 2
|
|
var hdr [44]byte
|
|
copy(hdr[0:4], "RIFF")
|
|
binary.LittleEndian.PutUint32(hdr[4:8], uint32(36+dataLen))
|
|
copy(hdr[8:12], "WAVE")
|
|
copy(hdr[12:16], "fmt ")
|
|
binary.LittleEndian.PutUint32(hdr[16:20], 16) // fmt chunk size
|
|
binary.LittleEndian.PutUint16(hdr[20:22], 1) // PCM
|
|
binary.LittleEndian.PutUint16(hdr[22:24], 1) // mono
|
|
binary.LittleEndian.PutUint32(hdr[24:28], uint32(sampleRate)) // sample rate
|
|
binary.LittleEndian.PutUint32(hdr[28:32], uint32(sampleRate*2)) // byte rate
|
|
binary.LittleEndian.PutUint16(hdr[32:34], 2) // block align
|
|
binary.LittleEndian.PutUint16(hdr[34:36], 16) // bits per sample
|
|
copy(hdr[36:40], "data")
|
|
binary.LittleEndian.PutUint32(hdr[40:44], uint32(dataLen))
|
|
if _, err := w.Write(hdr[:]); err != nil {
|
|
return err
|
|
}
|
|
buf := make([]byte, 2*len(samples))
|
|
for i, s := range samples {
|
|
v := int16(math.Round(s * 32767))
|
|
binary.LittleEndian.PutUint16(buf[i*2:], uint16(v))
|
|
}
|
|
_, err := w.Write(buf)
|
|
return err
|
|
}
|
|
|
|
// ReadWAVHeader sanity-parses a WAV header (used in tests and info).
|
|
func ReadWAVHeader(r io.Reader) (sampleRate, bits, channels, dataBytes int, err error) {
|
|
var hdr [44]byte
|
|
if _, err = io.ReadFull(r, hdr[:]); err != nil {
|
|
return
|
|
}
|
|
if string(hdr[0:4]) != "RIFF" || string(hdr[8:12]) != "WAVE" {
|
|
err = fmt.Errorf("not a WAV file")
|
|
return
|
|
}
|
|
channels = int(binary.LittleEndian.Uint16(hdr[22:24]))
|
|
sampleRate = int(binary.LittleEndian.Uint32(hdr[24:28]))
|
|
bits = int(binary.LittleEndian.Uint16(hdr[34:36]))
|
|
dataBytes = int(binary.LittleEndian.Uint32(hdr[40:44]))
|
|
return
|
|
}
|