github.com/noriah/catnip@v1.8.5/processor/processor.go (about) 1 package processor 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/noriah/catnip/dsp" 9 "github.com/noriah/catnip/dsp/window" 10 "github.com/noriah/catnip/fft" 11 "github.com/noriah/catnip/input" 12 ) 13 14 type Output interface { 15 Bins(int) int 16 Write([][]float64, int) error 17 } 18 19 type Processor interface { 20 Start(ctx context.Context, kickChan chan bool, mu *sync.Mutex) context.Context 21 Stop() 22 Process() 23 } 24 25 type Config struct { 26 SampleRate float64 // rate at which samples are read 27 SampleSize int // number of samples per buffer 28 ChannelCount int // number of channels 29 ProcessRate int // target framerate 30 Buffers [][]input.Sample // sample buffers 31 Analyzer dsp.Analyzer // audio analyzer 32 Output Output // data output 33 Smoother dsp.Smoother // time smoother 34 Windower window.Function // data windower 35 } 36 37 type processor struct { 38 channelCount int 39 processRate int 40 41 bars int 42 43 fftBufs [][]complex128 44 barBufs [][]float64 45 46 // Double-buffer the audio samples so we can read on it again while the code 47 // is processing it. 48 inputBufs [][]input.Sample 49 50 plans []*fft.Plan 51 52 mu *sync.Mutex 53 ctxCancel context.CancelFunc 54 55 anlz dsp.Analyzer 56 out Output 57 smth dsp.Smoother 58 wndwr window.Function 59 } 60 61 func New(cfg Config) *processor { 62 63 vis := &processor{ 64 channelCount: cfg.ChannelCount, 65 processRate: cfg.ProcessRate, 66 fftBufs: make([][]complex128, cfg.ChannelCount), 67 barBufs: make([][]float64, cfg.ChannelCount), 68 inputBufs: cfg.Buffers, 69 plans: make([]*fft.Plan, cfg.ChannelCount), 70 anlz: cfg.Analyzer, 71 out: cfg.Output, 72 smth: cfg.Smoother, 73 wndwr: cfg.Windower, 74 } 75 76 for idx := range vis.barBufs { 77 vis.barBufs[idx] = make([]float64, cfg.SampleSize) 78 vis.fftBufs[idx] = make([]complex128, cfg.SampleSize/2+1) 79 80 fft.InitPlan(&vis.plans[idx], vis.inputBufs[idx], vis.fftBufs[idx]) 81 } 82 83 return vis 84 } 85 86 func (vis *processor) Start(ctx context.Context, kickChan chan bool, mu *sync.Mutex) context.Context { 87 newCtx, cancel := context.WithCancel(ctx) 88 vis.ctxCancel = cancel 89 vis.mu = mu 90 go vis.run(newCtx, kickChan) 91 92 return newCtx 93 } 94 95 func (vis *processor) Stop() { 96 vis.ctxCancel() 97 } 98 99 func (vis *processor) run(ctx context.Context, kickChan chan bool) { 100 if vis.processRate <= 0 { 101 // if we do not have a framerate set, allow at most 1 second per sampling 102 vis.processRate = 1 103 } 104 105 dur := time.Second / time.Duration(vis.processRate) 106 ticker := time.NewTicker(dur) 107 defer ticker.Stop() 108 109 for { 110 vis.Process() 111 select { 112 case <-ctx.Done(): 113 return 114 case <-kickChan: 115 case <-ticker.C: 116 // default: 117 } 118 ticker.Reset(dur) 119 } 120 } 121 122 // Process runs processing on sample sets and calls Write on the output once per sample set. 123 func (vis *processor) Process() { 124 vis.mu.Lock() 125 for idx := range vis.barBufs { 126 if vis.wndwr != nil { 127 vis.wndwr(vis.inputBufs[idx]) 128 } 129 vis.plans[idx].Execute() 130 } 131 vis.mu.Unlock() 132 133 if n := vis.out.Bins(vis.channelCount); n != vis.bars { 134 vis.bars = vis.anlz.Recalculate(n) 135 } 136 137 for idx, fftBuf := range vis.fftBufs { 138 buf := vis.barBufs[idx] 139 140 for bIdx := range buf[:vis.bars] { 141 buf[bIdx] = vis.anlz.ProcessBin(bIdx, fftBuf) 142 } 143 } 144 145 if vis.smth != nil { 146 vis.smth.SmoothBuffers(vis.barBufs) 147 } 148 149 vis.out.Write(vis.barBufs, vis.channelCount) 150 }