v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user