clipper/backend/media/audio_progress.go

120 lines
2.8 KiB
Go

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 int64
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 += int64(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
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)
}
}
}