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  }