Repository: pion/mediadevices
Pull request #139 (2020-04-18 10:27:23)

Define audio data interface
### Reference issue
#138 

### TODO
- [x] pkg/wave (new)
- [x] pkg/io/audio
- [x] pkg/driver/audiotest
- [x] pkg/codec/opus
- [x] pkg/driver/microphone
- [x] ./


Patch:
diff --git a/pkg/codec/opus/opus.go b/pkg/codec/opus/opus.go
index 2662fdc..c553210 100644
--- a/pkg/codec/opus/opus.go
+++ b/pkg/codec/opus/opus.go
@@ -3,18 +3,17 @@ package opus
 import (
 	"fmt"
 	"math"
-	"reflect"
-	"unsafe"
 
 	"github.com/lherman-cs/opus"
 	"github.com/pion/mediadevices/pkg/codec"
 	"github.com/pion/mediadevices/pkg/io/audio"
 	"github.com/pion/mediadevices/pkg/prop"
+	"github.com/pion/mediadevices/pkg/wave"
 )
 
 type encoder struct {
 	engine *opus.Encoder
-	inBuff [][2]float32
+	inBuff *wave.Float32Interleaved
 	reader audio.Reader
 }
 
@@ -59,39 +58,45 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
 		return nil, err
 	}
 
-	inBuffSize := targetLatency * float64(p.SampleRate) / 1000
-	inBuff := make([][2]float32, int(inBuffSize))
+	inBuffSize := int(targetLatency * float64(p.SampleRate) / 1000)
+	inBuff := wave.NewFloat32Interleaved(
+		wave.ChunkInfo{Channels: channels, Len: inBuffSize},
+	)
+	inBuff.Data = inBuff.Data[:0]
 	e := encoder{engine, inBuff, r}
 	return &e, nil
 }
 
-func flatten(samples [][2]float32) []float32 {
-	if len(samples) == 0 {
-		return nil
-	}
-
-	data := uintptr(unsafe.Pointer(&samples[0]))
-	l := len(samples) * 2
-	return *(*[]float32)(unsafe.Pointer(&reflect.SliceHeader{Data: data, Len: l, Cap: l}))
-}
-
 func (e *encoder) Read(p []byte) (n int, err error) {
-	var curN int
-
 	// While the buffer is not full, keep reading so that we meet the latency requirement
-	for curN < len(e.inBuff) {
-		n, err = e.reader.Read(e.inBuff[curN:])
+	nLatency := e.inBuff.ChunkInfo().Len * e.inBuff.ChunkInfo().Channels
+	for len(e.inBuff.Data) < nLatency {
+		buff, err := e.reader.Read()
 		if err != nil {
 			return 0, err
 		}
-
-		curN += n
+		// TODO: convert audio format
+		b, ok := buff.(*wave.Float32Interleaved)
+		if !ok {
+			panic("unsupported audio format")
+		}
+		switch {
+		case b.Size.Channels == 1 && e.inBuff.ChunkInfo().Channels != 1:
+			for _, d := range b.Data {
+				for ch := 0; ch < e.inBuff.ChunkInfo().Channels; ch++ {
+					e.inBuff.Data = append(e.inBuff.Data, d)
+				}
+			}
+		case b.Size.Channels == e.inBuff.ChunkInfo().Channels:
+			e.inBuff.Data = append(e.inBuff.Data, b.Data...)
+		}
 	}
 
-	n, err = e.engine.EncodeFloat32(flatten(e.inBuff), p)
+	n, err = e.engine.EncodeFloat32(e.inBuff.Data[:nLatency], p)
 	if err != nil {
 		return n, err
 	}
+	e.inBuff.Data = e.inBuff.Data[nLatency:]
 
 	return n, nil
 }
diff --git a/pkg/driver/microphone/microphone_linux.go b/pkg/driver/microphone/microphone_linux.go
index f70c878..6ccc3dc 100644
--- a/pkg/driver/microphone/microphone_linux.go
+++ b/pkg/driver/microphone/microphone_linux.go
@@ -8,6 +8,7 @@ import (
 	"github.com/pion/mediadevices/pkg/driver"
 	"github.com/pion/mediadevices/pkg/io/audio"
 	"github.com/pion/mediadevices/pkg/prop"
+	"github.com/pion/mediadevices/pkg/wave"
 )
 
 type microphone struct {
@@ -85,9 +86,6 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 	)
 
 	samplesChan := make(chan []float32, 1)
-	var buff []float32
-	var bi int
-	var more bool
 
 	handler := func(b []float32) {
 		samplesChan <- b
@@ -98,28 +96,22 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 		return nil, err
 	}
 
-	reader := audio.ReaderFunc(func(samples [][2]float32) (n int, err error) {
-		for i := range samples {
-			// if we don't have anything left in buff, we'll wait until we receive
-			// more samples
-			if bi == len(buff) {
-				buff, more = <-samplesChan
-				if !more {
-					stream.Close()
-					return i, io.EOF
-				}
-				bi = 0
-			}
-
-			samples[i][0] = buff[bi]
-			if p.ChannelCount == 2 {
-				samples[i][1] = buff[bi+1]
-				bi++
-			}
-			bi++
+	reader := audio.ReaderFunc(func() (wave.Audio, error) {
+		buff, ok := <-samplesChan
+		if !ok {
+			stream.Close()
+			return nil, io.EOF
 		}
 
-		return len(samples), nil
+		a := wave.NewFloat32Interleaved(
+			wave.ChunkInfo{
+				Channels: p.ChannelCount,
+				Len:      len(buff) / p.ChannelCount,
+			},
+		)
+		copy(a.Data, buff)
+
+		return a, nil
 	})
 
 	stream.Start()
diff --git a/pkg/driver/microphone/microphone_windows.go b/pkg/driver/microphone/microphone_windows.go
index cb0bb06..f6d420e 100644
--- a/pkg/driver/microphone/microphone_windows.go
+++ b/pkg/driver/microphone/microphone_windows.go
@@ -10,6 +10,7 @@ import (
 	"github.com/pion/mediadevices/pkg/driver"
 	"github.com/pion/mediadevices/pkg/io/audio"
 	"github.com/pion/mediadevices/pkg/prop"
+	"github.com/pion/mediadevices/pkg/wave"
 )
 
 const (
@@ -193,44 +194,42 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 
 	// TODO: detect microphone device disconnection and return EOF
 
-	var bi int
-	reader := audio.ReaderFunc(func(samples [][2]float32) (n int, err error) {
-		var b *buffer
-		for i := range samples {
-			// if we don't have anything left in buff, we'll wait until we receive
-			// more samples
-			if b == nil || bi == int(b.waveHdr.dwBytesRecorded/2) {
-				var more bool
-				b, more = <-m.chBuf
-				if !more {
-					return i, io.EOF
-				}
-
-				select {
-				case <-m.closed:
-				default:
-					// Re-enqueue used buffer.
-					ret, _, _ := waveInAddBuffer.Call(
-						uintptr(unsafe.Pointer(m.hWaveIn)),
-						uintptr(unsafe.Pointer(&b.waveHdr)),
-						uintptr(unsafe.Sizeof(b.waveHdr)),
-					)
-					if err := errWinmm[ret]; err != nil {
-						return 0, err
-					}
-				}
-
-				bi = 0
+	reader := audio.ReaderFunc(func() (wave.Audio, error) {
+		b, ok := <-m.chBuf
+		if !ok {
+			return nil, io.EOF
+		}
+
+		select {
+		case <-m.closed:
+		default:
+			// Re-enqueue used buffer.
+			ret, _, _ := waveInAddBuffer.Call(
+				uintptr(unsafe.Pointer(m.hWaveIn)),
+				uintptr(unsafe.Pointer(&b.waveHdr)),
+				uintptr(unsafe.Sizeof(b.waveHdr)),
+			)
+			if err := errWinmm[ret]; err != nil {
+				return nil, err
 			}
+		}
 
-			samples[i][0] = float32(b.data[bi]) / 0x8000
-			if p.ChannelCount == 2 {
-				samples[i][1] = float32(b.data[bi+1]) / 0x8000
-				bi++
+		a := wave.NewFloat32Interleaved(
+			wave.ChunkInfo{
+				Channels: p.ChannelCount,
+				Len:      (int(b.waveHdr.dwBytesRecorded) / 2) / p.ChannelCount,
+			},
+		)
+
+		j := 0
+		for i := 0; i < a.Size.Len; i++ {
+			for ch := 0; ch < a.Size.Channels; ch++ {
+				a.SetFloat32(i, ch, wave.Float32Sample(float32(b.data[j])/0x8000))
+				j++
 			}
-			bi++
 		}
-		return len(samples), nil
+
+		return a, nil
 	})
 	return reader, nil
 }
diff --git a/pkg/io/audio/audio.go b/pkg/io/audio/audio.go
index 5f27fcd..d1eee76 100644
--- a/pkg/io/audio/audio.go
+++ b/pkg/io/audio/audio.go
@@ -1,13 +1,17 @@
 package audio
 
+import (
+	"github.com/pion/mediadevices/pkg/wave"
+)
+
 type Reader interface {
-	Read(samples [][2]float32) (n int, err error)
+	Read() (wave.Audio, error)
 }
 
-type ReaderFunc func(samples [][2]float32) (n int, err error)
+type ReaderFunc func() (wave.Audio, error)
 
-func (rf ReaderFunc) Read(samples [][2]float32) (n int, err error) {
-	return rf(samples)
+func (rf ReaderFunc) Read() (wave.Audio, error) {
+	return rf()
 }
 
 // TransformFunc produces a new Reader that will produces a transformed audio
diff --git a/pkg/io/audio/beep.go b/pkg/io/audio/beep.go
index 2d610d1..10fa525 100644
--- a/pkg/io/audio/beep.go
+++ b/pkg/io/audio/beep.go
@@ -4,12 +4,13 @@ import (
 	"io"
 
 	"github.com/faiface/beep"
+
+	"github.com/pion/mediadevices/pkg/wave"
 )
 
 type beepStreamer struct {
-	err  error
-	r    Reader
-	buff [][2]float32
+	err error
+	r   Reader
 }
 
 func ToBeep(r Reader) beep.Streamer {
@@ -20,17 +21,13 @@ func ToBeep(r Reader) beep.Streamer {
 	return &beepStreamer{r: r}
 }
 
-func (b *beepStreamer) Stream(samples [][2]float64) (n int, ok bool) {
+func (b *beepStreamer) Stream(samples [][2]float64) (int, bool) {
 	// Since there was an error, the stream has to be drained
 	if b.err != nil {
 		return 0, false
 	}
 
-	if len(b.buff) < len(samples) {
-		b.buff = append(b.buff, make([][2]float32, len(samples)-len(b.buff))...)
-	}
-
-	n, err := b.r.Read(b.buff[:len(samples)])
+	d, err := b.r.Read()
 	if err != nil {
 		b.err = err
 		if err != io.EOF {
@@ -38,9 +35,10 @@ func (b *beepStreamer) Stream(samples [][2]float64) (n int, ok bool) {
 		}
 	}
 
+	n := d.ChunkInfo().Len
 	for i := 0; i < n; i++ {
-		samples[i][0] = float64(b.buff[i][0])
-		samples[i][1] = float64(b.buff[i][1])
+		samples[i][0] = float64(wave.Float32SampleFormat.Convert(d.At(i, 0)).(wave.Float32Sample))
+		samples[i][1] = float64(wave.Float32SampleFormat.Convert(d.At(i, 1)).(wave.Float32Sample))
 	}
 
 	return n, true
@@ -53,6 +51,7 @@ func (b *beepStreamer) Err() error {
 type beepReader struct {
 	s    beep.Streamer
 	buff [][2]float64
+	size int
 }
 
 func FromBeep(s beep.Streamer) Reader {
@@ -60,28 +59,31 @@ func FromBeep(s beep.Streamer) Reader {
 		panic("FromStreamer requires a non-nil beep.Streamer")
 	}
 
-	return &beepReader{s: s}
+	return &beepReader{
+		s:    s,
+		buff: make([][2]float64, 1024), // TODO: configure chunk size
+	}
 }
 
-func (r *beepReader) Read(samples [][2]float32) (n int, err error) {
-	if len(r.buff) < len(samples) {
-		r.buff = append(r.buff, make([][2]float64, len(samples)-len(r.buff))...)
-	}
+func (r *beepReader) Read() (wave.Audio, error) {
+	out := wave.NewFloat32Interleaved(
+		wave.ChunkInfo{Len: len(r.buff), Channels: 2, SamplingRate: 48000},
+	)
 
-	n, ok := r.s.Stream(r.buff[:len(samples)])
+	n, ok := r.s.Stream(r.buff)
 	if !ok {
 		err := r.s.Err()
 		if err == nil {
 			err = io.EOF
 		}
 
-		return n, err
+		return nil, err
 	}
 
 	for i := 0; i < n; i++ {
-		samples[i][0] = float32(r.buff[i][0])
-		samples[i][1] = float32(r.buff[i][1])
+		out.SetFloat32(i, 0, wave.Float32Sample(r.buff[i][0]))
+		out.SetFloat32(i, 1, wave.Float32Sample(r.buff[i][1]))
 	}
 
-	return n, nil
+	return out, nil
 }
diff --git a/pkg/wave/float32.go b/pkg/wave/float32.go
new file mode 100644
index 0000000..4d0ea95
--- /dev/null
+++ b/pkg/wave/float32.go
@@ -0,0 +1,80 @@
+package wave
+
+// Float32Sample is a 32-bits float audio sample.
+type Float32Sample float32
+
+func (s Float32Sample) Int() int64 {
+	return int64(s * 0x100000000)
+}
+
+// Float32Interleaved multi-channel interlaced Audio.
+type Float32Interleaved struct {
+	Data []float32
+	Size ChunkInfo
+}
+
+// ChunkInfo returns audio chunk size.
+func (a *Float32Interleaved) ChunkInfo() ChunkInfo {
+	return a.Size
+}
+
+func (a *Float32Interleaved) SampleFormat() SampleFormat {
+	return Float32SampleFormat
+}
+
+func (a *Float32Interleaved) At(i, ch int) Sample {
+	return Float32Sample(a.Data[i*a.Size.Channels+ch])
+}
+
+func (a *Float32Interleaved) Set(i, ch int, s Sample) {
+	a.Data[i*a.Size.Channels+ch] = float32(Float32SampleFormat.Convert(s).(Float32Sample))
+}
+
+func (a *Float32Interleaved) SetFloat32(i, ch int, s Float32Sample) {
+	a.Data[i*a.Size.Channels+ch] = float32(s)
+}
+
+func NewFloat32Interleaved(size ChunkInfo) *Float32Interleaved {
+	return &Float32Interleaved{
+		Data: make([]float32, size.Channels*size.Len),
+		Size: size,
+	}
+}
+
+// Float32NonInterleaved multi-channel interlaced Audio.
+type Float32NonInterleaved struct {
+	Data [][]float32
+	Size ChunkInfo
+}
+
+// ChunkInfo returns audio chunk size.
+func (a *Float32NonInterleaved) ChunkInfo() ChunkInfo {
+	return a.Size
+}
+
+func (a *Float32NonInterleaved) SampleFormat() SampleFormat {
+	return Float32SampleFormat
+}
+
+func (a *Float32NonInterleaved) At(i, ch int) Sample {
+	return Float32Sample(a.Data[ch][i])
+}
+
+func (a *Float32NonInterleaved) Set(i, ch int, s Sample) {
+	a.Data[ch][i] = float32(Float32SampleFormat.Convert(s).(Float32Sample))
+}
+
+func (a *Float32NonInterleaved) SetFloat32(i, ch int, s Float32Sample) {
+	a.Data[ch][i] = float32(s)
+}
+
+func NewFloat32NonInterleaved(size ChunkInfo) *Float32NonInterleaved {
+	d := make([][]float32, size.Channels)
+	for i := 0; i < size.Channels; i++ {
+		d[i] = make([]float32, size.Len)
+	}
+	return &Float32NonInterleaved{
+		Data: d,
+		Size: size,
+	}
+}
diff --git a/pkg/wave/int16.go b/pkg/wave/int16.go
new file mode 100644
index 0000000..74604a2
--- /dev/null
+++ b/pkg/wave/int16.go
@@ -0,0 +1,80 @@
+package wave
+
+// Int16Sample is a 16-bits signed integer audio sample.
+type Int16Sample int16
+
+func (s Int16Sample) Int() int64 {
+	return int64(s) << 16
+}
+
+// Int16Interleaved multi-channel interlaced Audio.
+type Int16Interleaved struct {
+	Data []int16
+	Size ChunkInfo
+}
+
+// ChunkInfo returns audio chunk size.
+func (a *Int16Interleaved) ChunkInfo() ChunkInfo {
+	return a.Size
+}
+
+func (a *Int16Interleaved) SampleFormat() SampleFormat {
+	return Int16SampleFormat
+}
+
+func (a *Int16Interleaved) At(i, ch int) Sample {
+	return Int16Sample(a.Data[i*a.Size.Channels+ch])
+}
+
+func (a *Int16Interleaved) Set(i, ch int, s Sample) {
+	a.Data[i*a.Size.Channels+ch] = int16(Int16SampleFormat.Convert(s).(Int16Sample))
+}
+
+func (a *Int16Interleaved) SetInt16(i, ch int, s Int16Sample) {
+	a.Data[i*a.Size.Channels+ch] = int16(s)
+}
+
+func NewInt16Interleaved(size ChunkInfo) *Int16Interleaved {
+	return &Int16Interleaved{
+		Data: make([]int16, size.Channels*size.Len),
+		Size: size,
+	}
+}
+
+// Int16NonInterleaved multi-channel interlaced Audio.
+type Int16NonInterleaved struct {
+	Data [][]int16
+	Size ChunkInfo
+}
+
+// ChunkInfo returns audio chunk size.
+func (a *Int16NonInterleaved) ChunkInfo() ChunkInfo {
+	return a.Size
+}
+
+func (a *Int16NonInterleaved) SampleFormat() SampleFormat {
+	return Int16SampleFormat
+}
+
+func (a *Int16NonInterleaved) At(i, ch int) Sample {
+	return Int16Sample(a.Data[ch][i])
+}
+
+func (a *Int16NonInterleaved) Set(i, ch int, s Sample) {
+	a.Data[ch][i] = int16(Int16SampleFormat.Convert(s).(Int16Sample))
+}
+
+func (a *Int16NonInterleaved) SetInt16(i, ch int, s Int16Sample) {
+	a.Data[ch][i] = int16(s)
+}
+
+func NewInt16NonInterleaved(size ChunkInfo) *Int16NonInterleaved {
+	d := make([][]int16, size.Channels)
+	for i := 0; i < size.Channels; i++ {
+		d[i] = make([]int16, size.Len)
+	}
+	return &Int16NonInterleaved{
+		Data: d,
+		Size: size,
+	}
+}
diff --git a/pkg/wave/wave.go b/pkg/wave/wave.go
new file mode 100644
index 0000000..8a6f148
--- /dev/null
+++ b/pkg/wave/wave.go
@@ -0,0 +1,57 @@
+// Package wave implements a basic audio data library.
+package wave
+
+// Audio is a finite series of audio Sample values.
+type Audio interface {
+	SampleFormat() SampleFormat
+	ChunkInfo() ChunkInfo
+	At(i, ch int) Sample
+}
+
+// ChunkInfo contains size of the audio chunk.
+type ChunkInfo struct {
+	Len          int
+	Channels     int
+	SamplingRate int
+}
+
+// SampleFormat can convert any Sample to one from its own sample format.
+type SampleFormat interface {
+	Convert(c Sample) Sample
+}
+
+// SampleFormatFunc returns a SampleFormat that invokes f to implement the conversion.
+func SampleFormatFunc(f func(Sample) Sample) SampleFormat {
+	return &sampleFormatFunc{f}
+}
+
+type sampleFormatFunc struct {
+	f func(Sample) Sample
+}
+
+func (f *sampleFormatFunc) Convert(s Sample) Sample {
+	return f.f(s)
+}
+
+// SampleFormats for the standard formats.
+var (
+	Int16SampleFormat = SampleFormatFunc(func(s Sample) Sample {
+		if _, ok := s.(Int16Sample); ok {
+			return s
+		}
+		return Int16Sample(s.Int() >> 16)
+	})
+	Float32SampleFormat = SampleFormatFunc(func(s Sample) Sample {
+		if _, ok := s.(Float32Sample); ok {
+			return s
+		}
+		return Float32Sample(float32(s.Int()) / 0x100000000)
+	})
+)
+
+// Sample can convert itself to 64-bits signed value.
+type Sample interface {
+	// Int returns the audio level value for the sample.
+	// A value ranges within [0, 0xffffffff], but is represented by a int64.
+	Int() int64
+}


Test patch:
diff --git a/mediadevices_test.go b/mediadevices_test.go
index 7730638..825b0c8 100644
--- a/mediadevices_test.go
+++ b/mediadevices_test.go
@@ -211,8 +211,7 @@ type mockAudioCodec struct {
 }
 
 func (m *mockAudioCodec) Read(b []byte) (int, error) {
-	buf := make([][2]float32, 100)
-	if _, err := m.r.Read(buf); err != nil {
+	if _, err := m.r.Read(); err != nil {
 		return 0, err
 	}
 	return len(b), nil
diff --git a/pkg/driver/audiotest/dummy.go b/pkg/driver/audiotest/dummy.go
index 31e5f2f..fc9ddaa 100644
--- a/pkg/driver/audiotest/dummy.go
+++ b/pkg/driver/audiotest/dummy.go
@@ -10,6 +10,7 @@ import (
 	"github.com/pion/mediadevices/pkg/driver"
 	"github.com/pion/mediadevices/pkg/io/audio"
 	"github.com/pion/mediadevices/pkg/prop"
+	"github.com/pion/mediadevices/pkg/wave"
 )
 
 func init() {
@@ -41,33 +42,43 @@ func (d *dummy) AudioRecord(p prop.Media) (audio.Reader, error) {
 		sin[i] = float32(math.Sin(2*math.Pi*float64(i)/100) * 0.25) // 480 Hz
 	}
 
+	if p.Latency == 0 {
+		p.Latency = 20 * time.Millisecond
+	}
+	nSample := int(uint64(p.SampleRate) * uint64(p.Latency) / uint64(time.Second))
+
 	nextReadTime := time.Now()
 	var phase int
 
 	closed := d.closed
 
-	reader := audio.ReaderFunc(func(samples [][2]float32) (int, error) {
+	reader := audio.ReaderFunc(func() (wave.Audio, error) {
 		select {
 		case <-closed:
-			return 0, io.EOF
+			return nil, io.EOF
 		default:
 		}
 
 		time.Sleep(nextReadTime.Sub(time.Now()))
-		dur := time.Second * time.Duration(len(samples)) / 48000
-		nextReadTime = nextReadTime.Add(dur)
+		nextReadTime = nextReadTime.Add(p.Latency)
+
+		a := wave.NewFloat32Interleaved(
+			wave.ChunkInfo{
+				Channels: p.ChannelCount,
+				Len:      nSample,
+			},
+		)
 
-		for i := range samples {
+		for i := 0; i < nSample; i++ {
 			phase++
 			if phase >= 100 {
 				phase = 0
 			}
-			samples[i][0] = sin[phase]
-			if p.ChannelCount == 2 {
-				samples[i][1] = sin[phase]
+			for ch := 0; ch < p.ChannelCount; ch++ {
+				a.SetFloat32(i, ch, wave.Float32Sample(sin[phase]))
 			}
 		}
-		return len(samples), nil
+		return a, nil
 	})
 	return reader, nil
 }
diff --git a/pkg/wave/float32_test.go b/pkg/wave/float32_test.go
new file mode 100644
index 0000000..7f6775f
--- /dev/null
+++ b/pkg/wave/float32_test.go
@@ -0,0 +1,53 @@
+package wave
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestFloat32(t *testing.T) {
+	cases := map[string]struct {
+		in       Audio
+		expected [][]float32
+	}{
+		"Interleaved": {
+			in: &Float32Interleaved{
+				Data: []float32{
+					0.1, -0.5, 0.2, -0.6, 0.3, -0.7, 0.4, -0.8, 0.5, -0.9, 0.6, -1.0, 0.7, -1.1, 0.8, -1.2,
+				},
+				Size: ChunkInfo{8, 2, 48000},
+			},
+			expected: [][]float32{
+				{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
+				{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
+			},
+		},
+		"NonInterleaved": {
+			in: &Float32NonInterleaved{
+				Data: [][]float32{
+					{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
+					{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
+				},
+				Size: ChunkInfo{8, 2, 48000},
+			},
+			expected: [][]float32{
+				{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
+				{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
+			},
+		},
+	}
+	for name, c := range cases {
+		c := c
+		t.Run(name, func(t *testing.T) {
+			out := make([][]float32, c.in.ChunkInfo().Channels)
+			for i := 0; i < c.in.ChunkInfo().Channels; i++ {
+				for j := 0; j < c.in.ChunkInfo().Len; j++ {
+					out[i] = append(out[i], float32(c.in.At(j, i).(Float32Sample)))
+				}
+			}
+			if !reflect.DeepEqual(c.expected, out) {
+				t.Errorf("Sample level differs, expected: %v, got: %v", c.expected, out)
+			}
+		})
+	}
+}
diff --git a/pkg/wave/int16_test.go b/pkg/wave/int16_test.go
new file mode 100644
index 0000000..6032670
--- /dev/null
+++ b/pkg/wave/int16_test.go
@@ -0,0 +1,53 @@
+package wave
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestInt16(t *testing.T) {
+	cases := map[string]struct {
+		in       Audio
+		expected [][]int16
+	}{
+		"Interleaved": {
+			in: &Int16Interleaved{
+				Data: []int16{
+					1, -5, 2, -6, 3, -7, 4, -8, 5, -9, 6, -10, 7, -11, 8, -12,
+				},
+				Size: ChunkInfo{8, 2, 48000},
+			},
+			expected: [][]int16{
+				{1, 2, 3, 4, 5, 6, 7, 8},
+				{-5, -6, -7, -8, -9, -10, -11, -12},
+			},
+		},
+		"NonInterleaved": {
+			in: &Int16NonInterleaved{
+				Data: [][]int16{
+					{1, 2, 3, 4, 5, 6, 7, 8},
+					{-5, -6, -7, -8, -9, -10, -11, -12},
+				},
+				Size: ChunkInfo{8, 2, 48000},
+			},
+			expected: [][]int16{
+				{1, 2, 3, 4, 5, 6, 7, 8},
+				{-5, -6, -7, -8, -9, -10, -11, -12},
+			},
+		},
+	}
+	for name, c := range cases {
+		c := c
+		t.Run(name, func(t *testing.T) {
+			out := make([][]int16, c.in.ChunkInfo().Channels)
+			for i := 0; i < c.in.ChunkInfo().Channels; i++ {
+				for j := 0; j < c.in.ChunkInfo().Len; j++ {
+					out[i] = append(out[i], int16(c.in.At(j, i).(Int16Sample)))
+				}
+			}
+			if !reflect.DeepEqual(c.expected, out) {
+				t.Errorf("Sample level differs, expected: %v, got: %v", c.expected, out)
+			}
+		})
+	}
+}
diff --git a/pkg/wave/wave_test.go b/pkg/wave/wave_test.go
new file mode 100644
index 0000000..de4c58a
--- /dev/null
+++ b/pkg/wave/wave_test.go
@@ -0,0 +1,61 @@
+package wave
+
+import (
+	"math"
+	"reflect"
+	"testing"
+)
+
+func TestConvert(t *testing.T) {
+	cases := map[string]struct {
+		in       []Sample
+		typ      SampleFormat
+		expected []Sample
+	}{
+		"Int16ToFloat32": {
+			in: []Sample{
+				Int16Sample(-0x1000),
+				Int16Sample(-0x100),
+				Int16Sample(0x0),
+				Int16Sample(0x100),
+				Int16Sample(0x1000),
+			},
+			typ: Float32SampleFormat,
+			expected: []Sample{
+				Float32Sample(-math.Pow(2, -4)),
+				Float32Sample(-math.Pow(2, -8)),
+				Float32Sample(0.0),
+				Float32Sample(math.Pow(2, -8)),
+				Float32Sample(math.Pow(2, -4)),
+			},
+		},
+		"Float32ToInt16": {
+			in: []Sample{
+				Float32Sample(-math.Pow(2, -4)),
+				Float32Sample(-math.Pow(2, -8)),
+				Float32Sample(0.0),
+				Float32Sample(math.Pow(2, -8)),
+				Float32Sample(math.Pow(2, -4)),
+			},
+			typ: Int16SampleFormat,
+			expected: []Sample{
+				Int16Sample(-0x1000),
+				Int16Sample(-0x100),
+				Int16Sample(0x0),
+				Int16Sample(0x100),
+				Int16Sample(0x1000),
+			},
+		},
+	}
+	for name, c := range cases {
+		c := c
+		t.Run(name, func(t *testing.T) {
+			for i := range c.in {
+				s := c.typ.Convert(c.in[i])
+				if !reflect.DeepEqual(c.expected[i], s) {
+					t.Errorf("Convert result differs, expected: %v, got: %v", c.expected[i], s)
+				}
+			}
+		})
+	}
+}