82 lines
3.0 KiB
Go
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()
|
|
}
|