Files
2026-07-15 01:28:56 -04:00

82 lines
3.0 KiB
Go

package audio
import (
"bytes"
"encoding/binary"
"math"
)
const sampleRate = 48000 // PipeWire native rate
// seg is one tone segment: linear frequency sweep with pluck envelope
type seg struct {
f0, f1 float64 // Hz
dur float64 // seconds
amp float64 // 0..1
square bool
}
// effectSegs defines each effect's tone sequence — the tuning surface
var effectSegs = [EffectCount][]seg{
EffectPickup: {{988, 988, 0.055, 0.5, false}, {1319, 1319, 0.11, 0.5, false}}, // B5→E6 coin
EffectMagnet: {{523, 1319, 0.10, 0.45, false}, {784, 1976, 0.16, 0.4, false}}, // rising double sweep — reads as a suction, distinct from the coin
EffectShield: {{659, 988, 0.07, 0.45, false}, {1319, 1319, 0.16, 0.40, false}}, // chime up into a held tone — armor
EffectShieldBreak: {{1200, 300, 0.06, 0.50, true}, {700, 180, 0.14, 0.45, true}}, // square shatter; brighter and shorter than the death crunch
EffectBoost: {{392, 1568, 0.16, 0.50, false}, {1568, 1568, 0.10, 0.35, false}}, // fast sweep up — tempo kick
EffectDeflect: {{1200, 1600, 0.025, 0.20, true}, {1600, 900, 0.035, 0.15, true}}, // fires once per walled chord traversed under a Boost — short, dry, low
EffectMoveRow: {{300, 620, 0.07, 0.35, false}}, // whoosh up
EffectMoveLane: {{440, 470, 0.04, 0.28, false}}, // tick
EffectHitWall: {{140, 50, 0.30, 0.6, true}}, // crunch drop
EffectLevelClear: { // C-major arpeggio
{523, 523, 0.09, 0.45, false},
{659, 659, 0.09, 0.45, false},
{784, 784, 0.09, 0.45, false},
{1047, 1047, 0.18, 0.45, false},
},
}
// synth renders segments to 16-bit mono PCM
func synth(segs []seg) []int16 {
var out []int16
for _, s := range segs {
n := int(s.dur * sampleRate)
phase := 0.0
for i := range n {
t := float64(i) / float64(n)
f := s.f0 + (s.f1-s.f0)*t
phase += 2 * math.Pi * f / sampleRate
v := math.Sin(phase)
if s.square {
if v >= 0 {
v = 1
} else {
v = -1
}
}
env := math.Min(t*24, 1) * math.Exp(-4.2*t) // fast attack, pluck decay
out = append(out, int16(v*env*s.amp*32767))
}
}
return out
}
// wavBytes wraps PCM in a minimal RIFF/WAVE container (PCM16 mono)
func wavBytes(samples []int16) []byte {
var b bytes.Buffer
dataLen := uint32(len(samples) * 2)
b.WriteString("RIFF")
binary.Write(&b, binary.LittleEndian, 36+dataLen)
b.WriteString("WAVEfmt ")
binary.Write(&b, binary.LittleEndian, uint32(16))
binary.Write(&b, binary.LittleEndian, uint16(1)) // PCM
binary.Write(&b, binary.LittleEndian, uint16(1)) // mono
binary.Write(&b, binary.LittleEndian, uint32(sampleRate))
binary.Write(&b, binary.LittleEndian, uint32(sampleRate*2)) // byte rate
binary.Write(&b, binary.LittleEndian, uint16(2)) // block align
binary.Write(&b, binary.LittleEndian, uint16(16)) // bits/sample
b.WriteString("data")
binary.Write(&b, binary.LittleEndian, dataLen)
binary.Write(&b, binary.LittleEndian, samples)
return b.Bytes()
}