diff --git a/encode/mp3.go b/encode/mp3.go index 534c45e..3279dca 100644 --- a/encode/mp3.go +++ b/encode/mp3.go @@ -54,7 +54,7 @@ func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string) error } // Render all windows to stereo frames - bank := synth.NewBank(1.0) // tau=1.0s per D-07 + bank := synth.NewBank(1.0, synth.ClassFreqConfigs) // tau=1.0s per D-07 var allFrames [][2]float64 for _, snap := range snapshots { frames := bank.RenderWindow(snap) diff --git a/synth/bank.go b/synth/bank.go index 4bf80e0..592f628 100644 --- a/synth/bank.go +++ b/synth/bank.go @@ -2,22 +2,25 @@ package synth import "github.com/netsynth/netsynth/classify" -// OscillatorBank holds 11 synthesis layers, one per TrafficClass. +// OscillatorBank holds synthesis layers, one per TrafficClass in the injected config map. // It consumes WindowSnapshot data and renders stereo PCM frames. type OscillatorBank struct { - layers map[classify.TrafficClass]*Layer - tau float64 + layers map[classify.TrafficClass]*Layer + tau float64 + gainPerLayer float64 } -// NewBank creates an OscillatorBank with one Layer per TrafficClass. +// NewBank creates an OscillatorBank with one Layer per entry in cfgs. // tau is the EMA time constant in seconds (use 1.0 for D-07's "1-2 second" feel). -func NewBank(tau float64) *OscillatorBank { +// gainPerLayer is computed dynamically as 1/len(cfgs) so that all layers at max +// amplitude sum to exactly 1.0 (no clipping), regardless of how many classes are active. +func NewBank(tau float64, cfgs map[classify.TrafficClass]FreqConfig) *OscillatorBank { b := &OscillatorBank{ - layers: make(map[classify.TrafficClass]*Layer, NumLayers), - tau: tau, + layers: make(map[classify.TrafficClass]*Layer, len(cfgs)), + tau: tau, + gainPerLayer: 1.0 / float64(len(cfgs)), } - for _, class := range classify.AllClasses() { - cfg := ClassFreqConfigs[class] + for class, cfg := range cfgs { b.layers[class] = NewLayer(cfg, SampleRate, tau) } return b @@ -25,7 +28,7 @@ func NewBank(tau float64) *OscillatorBank { // 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). +// Each layer gets 1/N of the total gain where N is the number of layers. func (b *OscillatorBank) RenderWindow(snap classify.WindowSnapshot) [][2]float64 { // Find max count for normalization var maxCount int64 @@ -36,21 +39,20 @@ func (b *OscillatorBank) RenderWindow(snap classify.WindowSnapshot) [][2]float64 } // Update target amplitudes for all layers - for _, class := range classify.AllClasses() { + for class, layer := range b.layers { count := snap.Counts[class] - b.layers[class].UpdateTarget(count, maxCount) + layer.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] + for _, layer := range b.layers { sample := layer.AdvanceSample() gainL, gainR := PanGains(layer.Config.Pan) - sumL += sample * GainPerLayer * gainL - sumR += sample * GainPerLayer * gainR + sumL += sample * b.gainPerLayer * gainL + sumR += sample * b.gainPerLayer * gainR } frames[i] = [2]float64{sumL, sumR} }