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
This commit is contained in:
54
sfx-maker/sfx/wav.go
Normal file
54
sfx-maker/sfx/wav.go
Normal file
@@ -0,0 +1,54 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user