github.com/noriah/catnip@v1.8.5/dsp/smoother.go (about) 1 package dsp 2 3 import ( 4 "math" 5 6 "github.com/noriah/catnip/util" 7 ) 8 9 type SmoothingMethod int 10 11 const ( 12 SmoothMin SmoothingMethod = iota // 0 13 SmoothSimple // 1 14 SmoothAverage // 2 15 SmoothSimpleAverage // 3 16 SmoothNew // 4 17 SmoothNewAverage // 5 18 SmoothNone // 6 19 SmoothMax // 7 20 21 SmoothDefault = SmoothSimpleAverage 22 ) 23 24 type SmootherConfig struct { 25 AverageSize int // size of window for average methods 26 ChannelCount int // number of channels 27 SampleSize int // number of samples per slice 28 SampleRate float64 // sample rate 29 SmoothingFactor float64 // smoothing factor 30 SmoothingMethod SmoothingMethod // smoothing method 31 } 32 33 type Smoother interface { 34 SmoothBuffers([][]float64) 35 SmoothBin(int, int, float64) float64 36 GetMethod() SmoothingMethod 37 SetMethod(SmoothingMethod) 38 } 39 40 type smoother struct { 41 values [][]float64 // old values used for smoothing 42 averages [][]*util.MovingWindow 43 smoothFactor float64 // smothing factor 44 smoothMethod SmoothingMethod 45 } 46 47 func NewSmoother(cfg SmootherConfig) Smoother { 48 if cfg.SmoothingFactor <= 0.0 { 49 cfg.SmoothingFactor = math.SmallestNonzeroFloat64 50 } 51 52 sm := &smoother{ 53 values: make([][]float64, cfg.ChannelCount), 54 averages: make([][]*util.MovingWindow, cfg.ChannelCount), 55 smoothFactor: cfg.SmoothingFactor, 56 smoothMethod: cfg.SmoothingMethod, 57 } 58 59 // calculate the window size if its not set 60 size := cfg.AverageSize 61 if size < 1 { 62 rate := cfg.SampleRate / float64(cfg.SampleSize) 63 // this is based on my experimentation. no evidence that it is best. 64 // the goal of the window average is to smooth out the very minor amplitude 65 // changes. the visual representation of them is a function of the frame 66 // rate. assuming a base of 60FPS, this should handle it pretty well for 67 // most rate/size combinations. 68 size = int(math.Ceil(5.0 * (rate / 60.0))) 69 } 70 71 for idx := range sm.values { 72 sm.values[idx] = make([]float64, cfg.SampleSize) 73 sm.averages[idx] = make([]*util.MovingWindow, cfg.SampleSize) 74 for i := range sm.averages[idx] { 75 sm.averages[idx][i] = util.NewMovingWindow(size) 76 } 77 } 78 79 return sm 80 } 81 82 func (sm *smoother) SmoothBuffers(bufs [][]float64) { 83 peak := 0.0 84 for _, buf := range bufs { 85 for _, v := range buf { 86 if v > peak { 87 peak = v 88 } 89 } 90 } 91 92 for ch, buf := range bufs { 93 for idx, v := range buf { 94 buf[idx] = sm.switchSmoothing(ch, idx, v, peak) 95 } 96 } 97 } 98 99 func (sm *smoother) SmoothBin(ch, idx int, value float64) float64 { 100 return sm.switchSmoothing(ch, idx, value, 0.0) 101 } 102 103 func (sm *smoother) GetMethod() SmoothingMethod { 104 return sm.smoothMethod 105 } 106 107 func (sm *smoother) SetMethod(method SmoothingMethod) { 108 switch { 109 case method <= SmoothMin: 110 sm.smoothMethod = SmoothMax - 1 111 case method >= SmoothMax: 112 sm.smoothMethod = SmoothMin + 1 113 default: 114 sm.smoothMethod = method 115 } 116 } 117 118 func (sm *smoother) switchSmoothing(ch, idx int, value, peak float64) float64 { 119 switch sm.smoothMethod { 120 default: 121 case SmoothMin, SmoothMax: 122 sm.smoothMethod = SmoothDefault 123 return sm.switchSmoothing(ch, idx, value, peak) 124 case SmoothSimple: 125 return sm.smoothBinSimple(ch, idx, value) 126 case SmoothAverage: 127 return sm.smoothBinAverage(ch, idx, value) 128 case SmoothSimpleAverage: 129 v := sm.smoothBinAverage(ch, idx, value) 130 return sm.smoothBinSimple(ch, idx, v) 131 case SmoothNew: 132 return sm.smoothBinNew(ch, idx, value, peak) 133 case SmoothNewAverage: 134 v := sm.smoothBinAverage(ch, idx, value) 135 return sm.smoothBinNew(ch, idx, v, peak) 136 case SmoothNone: 137 return value 138 } 139 140 return 0.0 141 } 142 143 func (sm *smoother) smoothBinSimple(ch, idx int, value float64) float64 { 144 if math.IsNaN(sm.values[ch][idx]) { 145 sm.values[ch][idx] = 0.0 146 } 147 value *= 1.0 - sm.smoothFactor 148 value += sm.values[ch][idx] * sm.smoothFactor 149 sm.values[ch][idx] = value 150 return value 151 } 152 153 func (sm *smoother) smoothBinAverage(ch, idx int, value float64) float64 { 154 if math.IsNaN(value) { 155 value = 0.0 156 } 157 avg, _ := sm.averages[ch][idx].Update(value) 158 return avg 159 } 160 161 func (sm *smoother) smoothBinNew(ch, idx int, value, peak float64) float64 { 162 if math.IsNaN(value) { 163 value = 0.0 164 } 165 166 existing := sm.values[ch][idx] 167 168 if math.IsNaN(existing) { 169 existing = 0.0 170 } 171 172 diff := math.Abs(value - existing) 173 max := math.Max(value, existing) 174 175 diffPct := diff / max 176 peakValuePct := value / math.Max(1.0, peak) 177 178 factor := sm.smoothFactor 179 partial := (1.0 - factor) * 0.45 180 181 factor += partial - ((partial + 0.1) * math.Pow(diffPct, 1.5)) 182 factor += (partial / 0.75) - ((partial / 0.5) * math.Pow(peakValuePct, 4.0)) 183 184 // Clamp the factor between 0+MinFloat64, and 1-MinFloat64 so that it does 185 // not zero out the value or not change at all. 186 factor = math.Max( 187 math.SmallestNonzeroFloat64, 188 math.Min( 189 1.0-math.SmallestNonzeroFloat64, 190 factor)) 191 192 value *= 1.0 - factor 193 value += existing * factor 194 195 sm.values[ch][idx] = value 196 197 return value 198 }