package sfx import ( "math" "math/rand" ) // Render synthesizes the effect into float64 samples in [-1, 1]. func Render(p *Params) []float64 { sr := float64(p.SampleRate) n := int(p.Duration() * sr) out := make([]float64, n) rng := rand.New(rand.NewSource(p.Seed)) phase := 0.0 noiseVal := 0.0 noiseCounter := 0.0 // one-pole filter states lpState := 0.0 hpState := 0.0 hpPrevIn := 0.0 dt := 1 / sr lpAlpha := 0.0 if p.LowPass > 0 { rc := 1 / (2 * math.Pi * p.LowPass) lpAlpha = dt / (rc + dt) } hpAlpha := 0.0 if p.HighPass > 0 { rc := 1 / (2 * math.Pi * p.HighPass) hpAlpha = rc / (rc + dt) } for i := 0; i < n; i++ { t := float64(i) / sr f := p.Freq + p.FreqSlide*t if p.ArpFactor != 0 && p.ArpTime > 0 && t >= p.ArpTime { f *= p.ArpFactor } if p.VibratoDepth > 0 && p.VibratoRate > 0 { f += p.VibratoDepth * math.Sin(2*math.Pi*p.VibratoRate*t) } if f < p.FreqMin { f = p.FreqMin } var s float64 if p.Wave == "noise" { // pitched noise: new random value f*4 times per second noiseCounter += f * 4 * dt if noiseCounter >= 1 || i == 0 { noiseCounter = math.Mod(noiseCounter, 1) noiseVal = rng.Float64()*2 - 1 } s = noiseVal } else { phase += f * dt ph := math.Mod(phase, 1) switch p.Wave { case "square": if ph < p.Duty { s = 1 } else { s = -1 } case "saw": s = 2*ph - 1 case "triangle": if ph < 0.5 { s = 4*ph - 1 } else { s = 3 - 4*ph } case "sine": s = math.Sin(2 * math.Pi * ph) } } s *= envelope(p, t) if lpAlpha > 0 { lpState += lpAlpha * (s - lpState) s = lpState } if hpAlpha > 0 { hpState = hpAlpha * (hpState + s - hpPrevIn) hpPrevIn = s s = hpState } s *= p.Volume if s > 1 { s = 1 } else if s < -1 { s = -1 } out[i] = s } return out } // envelope is a linear attack / sustain / decay gain in 0..1. func envelope(p *Params, t float64) float64 { switch { case t < p.Attack: return t / p.Attack case t < p.Attack+p.Sustain: return 1 default: d := t - p.Attack - p.Sustain if p.Decay <= 0 { return 0 } g := 1 - d/p.Decay if g < 0 { g = 0 } return g } }