feat(02-02): OscillatorBank multi-layer rendering from WindowSnapshot
- NewBank creates 11 layers from ClassFreqConfigs, one per TrafficClass - RenderWindow transforms WindowSnapshot into SamplesPerWindow stereo frames - GainPerLayer (1/11) applied per layer guarantees no clipping with all layers at max - PanGains applied per layer for constant-power stereo positioning - EMA amplitude smoothing provides temporal convergence across windows - 7 bank tests: layer count, output length, silence, non-zero, no-clip, stereo pan, EMA convergence
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package synth
|
||||
|
||||
import "github.com/netsynth/netsynth/classify"
|
||||
|
||||
// OscillatorBank holds 11 synthesis layers, one per TrafficClass.
|
||||
// It consumes WindowSnapshot data and renders stereo PCM frames.
|
||||
type OscillatorBank struct {
|
||||
layers map[classify.TrafficClass]*Layer
|
||||
tau float64
|
||||
}
|
||||
|
||||
// NewBank creates an OscillatorBank with one Layer per TrafficClass.
|
||||
// tau is the EMA time constant in seconds (use 1.0 for D-07's "1-2 second" feel).
|
||||
func NewBank(tau float64) *OscillatorBank {
|
||||
b := &OscillatorBank{
|
||||
layers: make(map[classify.TrafficClass]*Layer, NumLayers),
|
||||
tau: tau,
|
||||
}
|
||||
for _, class := range classify.AllClasses() {
|
||||
cfg := ClassFreqConfigs[class]
|
||||
b.layers[class] = NewLayer(cfg, SampleRate, tau)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// RenderWindow updates amplitude targets from snap, then renders SamplesPerWindow
|
||||
// stereo frames. Each frame is [2]float64{left, right} with values in [-1, 1].
|
||||
// Per D-10: each layer gets GainPerLayer (1/11) so 11 max-amplitude layers sum to 1.0 (no clipping).
|
||||
func (b *OscillatorBank) RenderWindow(snap classify.WindowSnapshot) [][2]float64 {
|
||||
// Find max count for normalization
|
||||
var maxCount int64
|
||||
for _, count := range snap.Counts {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
}
|
||||
}
|
||||
|
||||
// Update target amplitudes for all layers
|
||||
for _, class := range classify.AllClasses() {
|
||||
count := snap.Counts[class]
|
||||
b.layers[class].UpdateTarget(count, maxCount)
|
||||
}
|
||||
|
||||
// Render frames
|
||||
frames := make([][2]float64, SamplesPerWindow)
|
||||
for i := range frames {
|
||||
var sumL, sumR float64
|
||||
for _, class := range classify.AllClasses() {
|
||||
layer := b.layers[class]
|
||||
sample := layer.AdvanceSample()
|
||||
gainL, gainR := PanGains(layer.Config.Pan)
|
||||
sumL += sample * GainPerLayer * gainL
|
||||
sumR += sample * GainPerLayer * gainR
|
||||
}
|
||||
frames[i] = [2]float64{sumL, sumR}
|
||||
}
|
||||
return frames
|
||||
}
|
||||
Reference in New Issue
Block a user