feat(05-02): decouple NewBank from global config and fix dynamic GainPerLayer
- NewBank now accepts (tau float64, cfgs map[classify.TrafficClass]FreqConfig) - gainPerLayer field added to OscillatorBank, computed as 1.0/float64(len(cfgs)) - RenderWindow UpdateTarget loop iterates b.layers (not classify.AllClasses()) - RenderWindow render loop uses b.gainPerLayer (not GainPerLayer constant) - encode/mp3.go updated to pass synth.ClassFreqConfigs as default config map
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render all windows to stereo frames
|
// 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
|
var allFrames [][2]float64
|
||||||
for _, snap := range snapshots {
|
for _, snap := range snapshots {
|
||||||
frames := bank.RenderWindow(snap)
|
frames := bank.RenderWindow(snap)
|
||||||
|
|||||||
+18
-16
@@ -2,22 +2,25 @@ package synth
|
|||||||
|
|
||||||
import "github.com/netsynth/netsynth/classify"
|
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.
|
// It consumes WindowSnapshot data and renders stereo PCM frames.
|
||||||
type OscillatorBank struct {
|
type OscillatorBank struct {
|
||||||
layers map[classify.TrafficClass]*Layer
|
layers map[classify.TrafficClass]*Layer
|
||||||
tau float64
|
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).
|
// 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{
|
b := &OscillatorBank{
|
||||||
layers: make(map[classify.TrafficClass]*Layer, NumLayers),
|
layers: make(map[classify.TrafficClass]*Layer, len(cfgs)),
|
||||||
tau: tau,
|
tau: tau,
|
||||||
|
gainPerLayer: 1.0 / float64(len(cfgs)),
|
||||||
}
|
}
|
||||||
for _, class := range classify.AllClasses() {
|
for class, cfg := range cfgs {
|
||||||
cfg := ClassFreqConfigs[class]
|
|
||||||
b.layers[class] = NewLayer(cfg, SampleRate, tau)
|
b.layers[class] = NewLayer(cfg, SampleRate, tau)
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
@@ -25,7 +28,7 @@ func NewBank(tau float64) *OscillatorBank {
|
|||||||
|
|
||||||
// RenderWindow updates amplitude targets from snap, then renders SamplesPerWindow
|
// RenderWindow updates amplitude targets from snap, then renders SamplesPerWindow
|
||||||
// stereo frames. Each frame is [2]float64{left, right} with values in [-1, 1].
|
// 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 {
|
func (b *OscillatorBank) RenderWindow(snap classify.WindowSnapshot) [][2]float64 {
|
||||||
// Find max count for normalization
|
// Find max count for normalization
|
||||||
var maxCount int64
|
var maxCount int64
|
||||||
@@ -36,21 +39,20 @@ func (b *OscillatorBank) RenderWindow(snap classify.WindowSnapshot) [][2]float64
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update target amplitudes for all layers
|
// Update target amplitudes for all layers
|
||||||
for _, class := range classify.AllClasses() {
|
for class, layer := range b.layers {
|
||||||
count := snap.Counts[class]
|
count := snap.Counts[class]
|
||||||
b.layers[class].UpdateTarget(count, maxCount)
|
layer.UpdateTarget(count, maxCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render frames
|
// Render frames
|
||||||
frames := make([][2]float64, SamplesPerWindow)
|
frames := make([][2]float64, SamplesPerWindow)
|
||||||
for i := range frames {
|
for i := range frames {
|
||||||
var sumL, sumR float64
|
var sumL, sumR float64
|
||||||
for _, class := range classify.AllClasses() {
|
for _, layer := range b.layers {
|
||||||
layer := b.layers[class]
|
|
||||||
sample := layer.AdvanceSample()
|
sample := layer.AdvanceSample()
|
||||||
gainL, gainR := PanGains(layer.Config.Pan)
|
gainL, gainR := PanGains(layer.Config.Pan)
|
||||||
sumL += sample * GainPerLayer * gainL
|
sumL += sample * b.gainPerLayer * gainL
|
||||||
sumR += sample * GainPerLayer * gainR
|
sumR += sample * b.gainPerLayer * gainR
|
||||||
}
|
}
|
||||||
frames[i] = [2]float64{sumL, sumR}
|
frames[i] = [2]float64{sumL, sumR}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user