package media import ( "bytes" "encoding/binary" "fmt" "io" ) type GetAudioProgress struct { PercentComplete float32 Peaks []int16 } type GetAudioProgressReader interface { Read() (GetAudioProgress, error) Close() error } // fetchAudioProgressReader accepts a byte stream containing little endian // signed int16s and, given a target number of bins, emits a stream of peaks // corresponding to each channel of the audio data. type fetchAudioProgressReader struct { framesExpected int64 channels int framesPerBin int samples []int16 currPeaks []int16 currCount int framesProcessed int progress chan GetAudioProgress errorChan chan error } // TODO: validate inputs, debugging is confusing otherwise func newGetAudioProgressReader(framesExpected int64, channels, numBins int) *fetchAudioProgressReader { return &fetchAudioProgressReader{ channels: channels, framesExpected: framesExpected, framesPerBin: int(framesExpected / int64(numBins)), samples: make([]int16, 8_192), currPeaks: make([]int16, channels), progress: make(chan GetAudioProgress), errorChan: make(chan error, 1), } } func (w *fetchAudioProgressReader) Abort(err error) { w.errorChan <- err } func (w *fetchAudioProgressReader) Close() error { close(w.progress) return nil } func (w *fetchAudioProgressReader) Write(p []byte) (int, error) { // expand our target slice if it is of insufficient size: numSamples := len(p) / SizeOfInt16 if len(w.samples) < numSamples { w.samples = append(w.samples, make([]int16, numSamples-len(w.samples))...) } samples := w.samples[:numSamples] if err := binary.Read(bytes.NewReader(p), binary.LittleEndian, samples); err != nil { return 0, fmt.Errorf("error parsing samples: %v", err) } for i := 0; i < len(samples); i += w.channels { for j := 0; j < w.channels; j++ { samp := samples[i+j] if samp < 0 { samp = -samp } if samp > w.currPeaks[j] { w.currPeaks[j] = samp } } w.currCount++ if w.currCount == w.framesPerBin { w.nextBin() } } w.framesProcessed += len(samples) / w.channels return len(p), nil } func (w *fetchAudioProgressReader) nextBin() { var progress GetAudioProgress // TODO: avoid an allocation? progress.Peaks = append(progress.Peaks, w.currPeaks...) progress.PercentComplete = (float32(w.framesProcessed) / float32(w.framesExpected)) * 100.0 w.progress <- progress w.currCount = 0 // log.Printf("got peak for %d frames, which is equal to target of %d frames per bin, %d total bins processed, peaks: %+v", w.currCount, w.framesPerBin, w.total+1, w.currPeaks) for i := 0; i < len(w.currPeaks); i++ { w.currPeaks[i] = 0 } w.framesProcessed++ } func (w *fetchAudioProgressReader) Read() (GetAudioProgress, error) { for { select { case progress, ok := <-w.progress: if !ok { return GetAudioProgress{}, io.EOF } return progress, nil case err := <-w.errorChan: return GetAudioProgress{}, fmt.Errorf("error waiting for progress: %v", err) } } }