diff --git a/audio/wav/decoder.go b/audio/wav/decoder.go new file mode 100644 index 0000000..6b8f504 --- /dev/null +++ b/audio/wav/decoder.go @@ -0,0 +1,123 @@ +package wav + +import ( + "io" + "time" + + "github.com/pkg/errors" +) + +type Streamer struct { + rc io.ReadCloser + h header + pos int32 + err error +} + +func NewStreamer(rc io.ReadCloser) (*Streamer, error) { + var ( + d Streamer + err error + ) + d.rc = rc + d.h, err = readHeader(rc) + if err != nil { + rc.Close() + return nil, errors.Wrap(err, "wav") + } + if d.h.formatType != 1 { + rc.Close() + return nil, errors.New("wav: unsupported format type") + } + if d.h.numChans <= 0 { + rc.Close() + return nil, errors.New("wav: invalid number of channels (less than 1)") + } + if d.h.bitsPerSample != 8 && d.h.bitsPerSample != 16 { + rc.Close() + return nil, errors.New("wav: unsupported number of bits per sample, 8 or 16 are supported") + } + return &d, nil +} + +func (d *Streamer) Err() error { + return d.err +} + +func (d *Streamer) Duration() time.Duration { + numBytes := time.Duration(d.h.dataSize) + perFrame := time.Duration(d.h.bytesPerFrame) + sampRate := time.Duration(d.h.sampleRate) + return numBytes / perFrame * time.Second / sampRate +} + +func (d *Streamer) Stream(samples [][2]float64) (n int, ok bool) { + if d.pos >= d.h.dataSize { + return 0, false + } + switch { + case d.h.bitsPerSample == 8 && d.h.numChans == 1: + width := 1 + p := make([]byte, len(samples)*width) + n, err := d.rc.Read(p) + for i, j := 0, 0; i < n-width; i, j = i+width, j+1 { + val := float64(p[i])/255*2 - 1 + samples[j][0] = val + samples[j][1] = val + } + if err != nil { + d.err = err + } + d.pos += int32(n) + return n / width, true + case d.h.bitsPerSample == 8 && d.h.numChans >= 2: + width := int(d.h.numChans) + p := make([]byte, len(samples)*width) + n, err := d.rc.Read(p) + for i, j := 0, 0; i < n-width; i, j = i+width, j+1 { + samples[j][0] = float64(p[i+0])/255*2 - 1 + samples[j][1] = float64(p[i+1])/255*2 - 1 + } + if err != nil { + d.err = err + } + d.pos += int32(n) + return n / width, true + case d.h.bitsPerSample == 16 && d.h.numChans == 1: + width := 2 + p := make([]byte, len(samples)*width) + n, err := d.rc.Read(p) + for i, j := 0, 0; i < n-width; i, j = i+width, j+1 { + val := float64(int16(p[i+0])+int16(p[i+1])*(1<<8)) / (1<<15 - 1) + samples[j][0] = val + samples[j][1] = val + } + if err != nil { + d.err = err + } + d.pos += int32(n) + return n / width, true + case d.h.bitsPerSample == 16 && d.h.numChans >= 2: + width := int(d.h.numChans) * 2 + p := make([]byte, len(samples)*width) + n, err := d.rc.Read(p) + for i, j := 0, 0; i <= n-width; i, j = i+width, j+1 { + samples[j][0] = float64(int16(p[i+0])+int16(p[i+1])*(1<<8)) / (1<<15 - 1) + samples[j][1] = float64(int16(p[i+2])+int16(p[i+3])*(1<<8)) / (1<<15 - 1) + } + if err != nil { + d.err = err + } + d.pos += int32(n) + return n / width, true + } + panic("unreachable") +} + +func (d *Streamer) Close() error { + err := d.rc.Close() + if err != nil { + return errors.Wrap(err, "wav") + } + return nil +} diff --git a/audio/wav/header.go b/audio/wav/header.go new file mode 100644 index 0000000..dfd9240 --- /dev/null +++ b/audio/wav/header.go @@ -0,0 +1,78 @@ +package wav + +import ( + "encoding/binary" + "io" + + "github.com/pkg/errors" +) + +type header struct { + fileSize int32 + formatSize int32 + formatType int16 + numChans int16 + sampleRate int32 + byteRate int32 + bytesPerFrame int16 + bitsPerSample int16 + dataSize int32 +} + +func readHeader(r io.Reader) (header, error) { + var ( + h header + er errReader + ) + err := er. + ReadString(r, "RIFF", errors.New("missing RIFF at the beginning")). + ReadBinary(r, binary.LittleEndian, &h.fileSize). + ReadString(r, "WAVE", errors.New("unsupported file type")). + ReadString(r, "fmt\x00", errors.New("missing format chunk marker")). + ReadBinary(r, binary.LittleEndian, &h.formatSize). + ReadBinary(r, binary.LittleEndian, &h.formatType). + ReadBinary(r, binary.LittleEndian, &h.numChans). + ReadBinary(r, binary.LittleEndian, &h.sampleRate). + ReadBinary(r, binary.LittleEndian, &h.byteRate). + ReadBinary(r, binary.LittleEndian, &h.bytesPerFrame). + ReadBinary(r, binary.LittleEndian, &h.bitsPerSample). + ReadString(r, "data", errors.New("missing data chunk marker")). + ReadBinary(r, binary.LittleEndian, &h.dataSize). + Err() + return h, err +} + +type errReader struct { + err error +} + +func (e *errReader) ReadString(r io.Reader, s string, notThereErr error) *errReader { + if e.err != nil { + return e + } + buf := make([]byte, len(s)) + _, err := r.Read(buf) + if err != nil { + e.err = errors.Wrap(err, "error while reading header") + return e + } + if string(buf) != s { + e.err = errors.Wrap(err, "invalid header") + } + return e +} + +func (e *errReader) ReadBinary(r io.Reader, order binary.ByteOrder, data interface{}) *errReader { + if e.err != nil { + return e + } + err := binary.Read(r, order, data) + if err != nil { + e.err = errors.Wrap(err, "invalid header") + } + return e +} + +func (e *errReader) Err() error { + return e.err +}