Repository: pion/mediadevices
Pull request #182 (2020-06-08 06:37:07)

Opus support int16
Usually int16 is sound system native format.


Patch:
diff --git a/pkg/codec/opus/opus.go b/pkg/codec/opus/opus.go
index c553210..c6e360d 100644
--- a/pkg/codec/opus/opus.go
+++ b/pkg/codec/opus/opus.go
@@ -1,6 +1,7 @@
 package opus
 
 import (
+	"errors"
 	"fmt"
 	"math"
 
@@ -9,11 +10,12 @@ import (
 	"github.com/pion/mediadevices/pkg/io/audio"
 	"github.com/pion/mediadevices/pkg/prop"
 	"github.com/pion/mediadevices/pkg/wave"
+	"github.com/pion/mediadevices/pkg/wave/mixer"
 )
 
 type encoder struct {
 	engine *opus.Encoder
-	inBuff *wave.Float32Interleaved
+	inBuff wave.Audio
 	reader audio.Reader
 }
 
@@ -32,6 +34,10 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
 		params.BitRate = 32000
 	}
 
+	if params.ChannelMixer == nil {
+		params.ChannelMixer = &mixer.MonoMixer{}
+	}
+
 	// Select the nearest supported latency
 	var targetLatency float64
 	// TODO: use p.Latency.Milliseconds() after Go 1.12 EOL
@@ -47,8 +53,7 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
 		targetLatency = latency
 	}
 
-	// Since audio.Reader only supports stereo mode, channels is always 2
-	channels := 2
+	channels := p.ChannelCount
 
 	engine, err := opus.NewEncoder(p.SampleRate, channels, opus.AppVoIP)
 	if err != nil {
@@ -58,47 +63,37 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
 		return nil, err
 	}
 
-	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}
+	rMix := audio.NewChannelMixer(channels, params.ChannelMixer)
+	rBuf := audio.NewBuffer(int(targetLatency * float64(p.SampleRate) / 1000))
+	e := encoder{
+		engine: engine,
+		reader: rMix(rBuf(r)),
+	}
 	return &e, nil
 }
 
-func (e *encoder) Read(p []byte) (n int, err error) {
-	// While the buffer is not full, keep reading so that we meet the latency requirement
-	nLatency := e.inBuff.ChunkInfo().Len * e.inBuff.ChunkInfo().Channels
-	for len(e.inBuff.Data) < nLatency {
-		buff, err := e.reader.Read()
+func (e *encoder) Read(p []byte) (int, error) {
+	buff, err := e.reader.Read()
+	if err != nil {
+		return 0, err
+	}
+
+	switch b := buff.(type) {
+	case *wave.Int16Interleaved:
+		n, err := e.engine.Encode(b.Data, p)
 		if err != nil {
-			return 0, err
+			return n, err
 		}
-		// 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...)
+		return n, nil
+	case *wave.Float32Interleaved:
+		n, err := e.engine.EncodeFloat32(b.Data, p)
+		if err != nil {
+			return n, err
 		}
+		return n, nil
+	default:
+		return 0, errors.New("unknown type of audio buffer")
 	}
-
-	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
 }
 
 func (e *encoder) SetBitRate(b int) error {
diff --git a/pkg/codec/opus/params.go b/pkg/codec/opus/params.go
index 0490362..4bf7b6f 100644
--- a/pkg/codec/opus/params.go
+++ b/pkg/codec/opus/params.go
@@ -4,12 +4,15 @@ import (
 	"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/mixer"
 	"github.com/pion/webrtc/v2"
 )
 
 // Params stores opus specific encoding parameters.
 type Params struct {
 	codec.BaseParams
+	// ChannelMixer is a mixer to be used if number of given and expected channels differ.
+	ChannelMixer mixer.ChannelMixer
 }
 
 // NewParams returns default opus codec specific parameters.
diff --git a/pkg/driver/microphone/microphone_linux.go b/pkg/driver/microphone/microphone_linux.go
index 0525e89..0af6cc2 100644
--- a/pkg/driver/microphone/microphone_linux.go
+++ b/pkg/driver/microphone/microphone_linux.go
@@ -14,7 +14,7 @@ import (
 type microphone struct {
 	c           *pulse.Client
 	id          string
-	samplesChan chan<- []float32
+	samplesChan chan<- []int16
 }
 
 func init() {
@@ -85,14 +85,14 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 		pulse.RecordSource(src),
 	)
 
-	samplesChan := make(chan []float32, 1)
+	samplesChan := make(chan []int16, 1)
 
-	handler := func(b []float32) (int, error) {
+	handler := func(b []int16) (int, error) {
 		samplesChan <- b
 		return len(b), nil
 	}
 
-	stream, err := m.c.NewRecord(pulse.Float32Writer(handler), options...)
+	stream, err := m.c.NewRecord(pulse.Int16Writer(handler), options...)
 	if err != nil {
 		return nil, err
 	}
@@ -104,7 +104,7 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 			return nil, io.EOF
 		}
 
-		a := wave.NewFloat32Interleaved(
+		a := wave.NewInt16Interleaved(
 			wave.ChunkInfo{
 				Channels: p.ChannelCount,
 				Len:      len(buff) / p.ChannelCount,
diff --git a/pkg/driver/microphone/microphone_windows.go b/pkg/driver/microphone/microphone_windows.go
index f6d420e..b9f1127 100644
--- a/pkg/driver/microphone/microphone_windows.go
+++ b/pkg/driver/microphone/microphone_windows.go
@@ -214,7 +214,7 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 			}
 		}
 
-		a := wave.NewFloat32Interleaved(
+		a := wave.NewInt16Interleaved(
 			wave.ChunkInfo{
 				Channels: p.ChannelCount,
 				Len:      (int(b.waveHdr.dwBytesRecorded) / 2) / p.ChannelCount,
@@ -224,7 +224,7 @@ func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
 		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))
+				a.SetInt16(i, ch, wave.Int16Sample(b.data[j]))
 				j++
 			}
 		}
diff --git a/pkg/io/audio/buffer.go b/pkg/io/audio/buffer.go
new file mode 100644
index 0000000..22b494a
--- /dev/null
+++ b/pkg/io/audio/buffer.go
@@ -0,0 +1,79 @@
+package audio
+
+import (
+	"errors"
+
+	"github.com/pion/mediadevices/pkg/wave"
+)
+
+var errUnsupported = errors.New("unsupported audio format")
+
+// NewBuffer creates audio transform to buffer signal to have exact nSample samples.
+func NewBuffer(nSamples int) TransformFunc {
+	var inBuff wave.Audio
+
+	return func(r Reader) Reader {
+		return ReaderFunc(func() (wave.Audio, error) {
+			for {
+				if inBuff != nil && inBuff.ChunkInfo().Len >= nSamples {
+					break
+				}
+
+				buff, err := r.Read()
+				if err != nil {
+					return nil, err
+				}
+				switch b := buff.(type) {
+				case *wave.Float32Interleaved:
+					ib, ok := inBuff.(*wave.Float32Interleaved)
+					if !ok || ib.Size.Channels != b.Size.Channels {
+						ib = wave.NewFloat32Interleaved(
+							wave.ChunkInfo{Channels: b.Size.Channels, Len: nSamples},
+						)
+						ib.Data = ib.Data[:0]
+						ib.Size.Len = 0
+						inBuff = ib
+					}
+					ib.Data = append(ib.Data, b.Data...)
+					ib.Size.Len += b.Size.Len
+
+				case *wave.Int16Interleaved:
+					ib, ok := inBuff.(*wave.Int16Interleaved)
+					if !ok || ib.Size.Channels != b.Size.Channels {
+						ib = wave.NewInt16Interleaved(
+							wave.ChunkInfo{Channels: b.Size.Channels, Len: nSamples},
+						)
+						ib.Data = ib.Data[:0]
+						ib.Size.Len = 0
+						inBuff = ib
+					}
+					ib.Data = append(ib.Data, b.Data...)
+					ib.Size.Len += b.Size.Len
+
+				default:
+					return nil, errUnsupported
+				}
+			}
+			switch ib := inBuff.(type) {
+			case *wave.Int16Interleaved:
+				ibCopy := *ib
+				n := nSamples * ib.Size.Channels
+				ibCopy.Data = make([]int16, n)
+				copy(ibCopy.Data, ib.Data)
+				ib.Data = ib.Data[n:]
+				ib.Size.Len -= nSamples
+				return &ibCopy, nil
+
+			case *wave.Float32Interleaved:
+				ibCopy := *ib
+				n := nSamples * ib.Size.Channels
+				ibCopy.Data = make([]float32, n)
+				copy(ibCopy.Data, ib.Data)
+				ib.Data = ib.Data[n:]
+				ib.Size.Len -= nSamples
+				return &ibCopy, nil
+			}
+			return nil, errUnsupported
+		})
+	}
+}
diff --git a/pkg/io/audio/mixer.go b/pkg/io/audio/mixer.go
new file mode 100644
index 0000000..6f44507
--- /dev/null
+++ b/pkg/io/audio/mixer.go
@@ -0,0 +1,45 @@
+package audio
+
+import (
+	"github.com/pion/mediadevices/pkg/wave"
+	"github.com/pion/mediadevices/pkg/wave/mixer"
+)
+
+// NewChannelMixer creates audio transform to mix audio channels.
+func NewChannelMixer(channels int, mixer mixer.ChannelMixer) TransformFunc {
+	return func(r Reader) Reader {
+		return ReaderFunc(func() (wave.Audio, error) {
+			buff, err := r.Read()
+			if err != nil {
+				return nil, err
+			}
+			ci := buff.ChunkInfo()
+			if ci.Channels == channels {
+				return buff, nil
+			}
+			var mixed wave.Audio
+			switch buff.(type) {
+			case *wave.Int16Interleaved:
+				mixed = wave.NewInt16Interleaved(
+					wave.ChunkInfo{Channels: channels, Len: ci.Len},
+				)
+			case *wave.Int16NonInterleaved:
+				mixed = wave.NewInt16NonInterleaved(
+					wave.ChunkInfo{Channels: channels, Len: ci.Len},
+				)
+			case *wave.Float32Interleaved:
+				mixed = wave.NewFloat32Interleaved(
+					wave.ChunkInfo{Channels: channels, Len: ci.Len},
+				)
+			case *wave.Float32NonInterleaved:
+				mixed = wave.NewFloat32NonInterleaved(
+					wave.ChunkInfo{Channels: channels, Len: ci.Len},
+				)
+			}
+			if err := mixer.Mix(mixed, buff); err != nil {
+				return nil, err
+			}
+			return mixed, nil
+		})
+	}
+}
diff --git a/pkg/prop/prop.go b/pkg/prop/prop.go
index f75de1a..ba3b8d0 100644
--- a/pkg/prop/prop.go
+++ b/pkg/prop/prop.go
@@ -105,6 +105,7 @@ func (p *MediaConstraints) FitnessDistance(o Media) (float64, bool) {
 	cmps.add(p.FrameFormat, o.FrameFormat)
 	cmps.add(p.SampleRate, o.SampleRate)
 	cmps.add(p.Latency, o.Latency)
+	cmps.add(p.ChannelCount, o.ChannelCount)
 
 	return cmps.fitnessDistance()
 }
diff --git a/pkg/wave/float32.go b/pkg/wave/float32.go
index 4d0ea95..54d06e2 100644
--- a/pkg/wave/float32.go
+++ b/pkg/wave/float32.go
@@ -34,6 +34,16 @@ func (a *Float32Interleaved) SetFloat32(i, ch int, s Float32Sample) {
 	a.Data[i*a.Size.Channels+ch] = float32(s)
 }
 
+// SubAudio returns part of the original audio sharing the buffer.
+func (a *Float32Interleaved) SubAudio(offsetSamples, nSamples int) *Float32Interleaved {
+	ret := *a
+	offset := offsetSamples * a.Size.Channels
+	n := nSamples * a.Size.Channels
+	ret.Data = ret.Data[offset : offset+n]
+	ret.Size.Len = nSamples
+	return &ret
+}
+
 func NewFloat32Interleaved(size ChunkInfo) *Float32Interleaved {
 	return &Float32Interleaved{
 		Data: make([]float32, size.Channels*size.Len),
@@ -68,6 +78,16 @@ func (a *Float32NonInterleaved) SetFloat32(i, ch int, s Float32Sample) {
 	a.Data[ch][i] = float32(s)
 }
 
+// SubAudio returns part of the original audio sharing the buffer.
+func (a *Float32NonInterleaved) SubAudio(offsetSamples, nSamples int) *Float32NonInterleaved {
+	ret := *a
+	for i := range a.Data {
+		ret.Data[i] = ret.Data[i][offsetSamples : offsetSamples+nSamples]
+	}
+	ret.Size.Len = nSamples
+	return &ret
+}
+
 func NewFloat32NonInterleaved(size ChunkInfo) *Float32NonInterleaved {
 	d := make([][]float32, size.Channels)
 	for i := 0; i < size.Channels; i++ {
diff --git a/pkg/wave/int16.go b/pkg/wave/int16.go
index 74604a2..3747d47 100644
--- a/pkg/wave/int16.go
+++ b/pkg/wave/int16.go
@@ -34,6 +34,16 @@ func (a *Int16Interleaved) SetInt16(i, ch int, s Int16Sample) {
 	a.Data[i*a.Size.Channels+ch] = int16(s)
 }
 
+// SubAudio returns part of the original audio sharing the buffer.
+func (a *Int16Interleaved) SubAudio(offsetSamples, nSamples int) *Int16Interleaved {
+	ret := *a
+	offset := offsetSamples * a.Size.Channels
+	n := nSamples * a.Size.Channels
+	ret.Data = ret.Data[offset : offset+n]
+	ret.Size.Len = nSamples
+	return &ret
+}
+
 func NewInt16Interleaved(size ChunkInfo) *Int16Interleaved {
 	return &Int16Interleaved{
 		Data: make([]int16, size.Channels*size.Len),
@@ -68,6 +78,16 @@ func (a *Int16NonInterleaved) SetInt16(i, ch int, s Int16Sample) {
 	a.Data[ch][i] = int16(s)
 }
 
+// SubAudio returns part of the original audio sharing the buffer.
+func (a *Int16NonInterleaved) SubAudio(offsetSamples, nSamples int) *Int16NonInterleaved {
+	ret := *a
+	for i := range a.Data {
+		ret.Data[i] = ret.Data[i][offsetSamples : offsetSamples+nSamples]
+	}
+	ret.Size.Len = nSamples
+	return &ret
+}
+
 func NewInt16NonInterleaved(size ChunkInfo) *Int16NonInterleaved {
 	d := make([][]int16, size.Channels)
 	for i := 0; i < size.Channels; i++ {
diff --git a/pkg/wave/int64.go b/pkg/wave/int64.go
new file mode 100644
index 0000000..ae4c990
--- /dev/null
+++ b/pkg/wave/int64.go
@@ -0,0 +1,8 @@
+package wave
+
+// Int64Sample is a 64-bits signed integer audio sample.
+type Int64Sample int64
+
+func (s Int64Sample) Int() int64 {
+	return int64(s)
+}
diff --git a/pkg/wave/mixer/mixer.go b/pkg/wave/mixer/mixer.go
new file mode 100644
index 0000000..d46b2de
--- /dev/null
+++ b/pkg/wave/mixer/mixer.go
@@ -0,0 +1,42 @@
+package mixer
+
+import (
+	"errors"
+
+	"github.com/pion/mediadevices/pkg/wave"
+)
+
+// ChannelMixer mixes audio into specifix channels.
+type ChannelMixer interface {
+	Mix(dst wave.Audio, src wave.Audio) error
+}
+
+// MonoMixer mixes channels into monaural audio.
+type MonoMixer struct {
+}
+
+func (m *MonoMixer) Mix(dst wave.Audio, src wave.Audio) error {
+	if dst.ChunkInfo().Len != src.ChunkInfo().Len {
+		return errors.New("buffer size mismatch")
+	}
+	dstSetter, ok := dst.(wave.EditableAudio)
+	if !ok {
+		return errors.New("destination buffer is not settable")
+	}
+
+	n := src.ChunkInfo().Len
+	channels := src.ChunkInfo().Channels
+	dstChannels := dst.ChunkInfo().Channels
+	for i := 0; i < n; i++ {
+		var mean int64
+		for ch := 0; ch < channels; ch++ {
+			mean += src.At(i, ch).Int()
+		}
+		mean /= int64(channels)
+
+		for ch := 0; ch < dstChannels; ch++ {
+			dstSetter.Set(i, ch, wave.Int64Sample(mean))
+		}
+	}
+	return nil
+}
diff --git a/pkg/wave/wave.go b/pkg/wave/wave.go
index 8a6f148..9723238 100644
--- a/pkg/wave/wave.go
+++ b/pkg/wave/wave.go
@@ -8,6 +8,12 @@ type Audio interface {
 	At(i, ch int) Sample
 }
 
+// EditableAudio is an editable finite series of audio Sample values.
+type EditableAudio interface {
+	Audio
+	Set(i, ch int, s Sample)
+}
+
 // ChunkInfo contains size of the audio chunk.
 type ChunkInfo struct {
 	Len          int


Test patch:
diff --git a/pkg/wave/float32_test.go b/pkg/wave/float32_test.go
index 7f6775f..4c1ba31 100644
--- a/pkg/wave/float32_test.go
+++ b/pkg/wave/float32_test.go
@@ -51,3 +51,44 @@ func TestFloat32(t *testing.T) {
 		})
 	}
 }
+
+func TestFloat32SubAudio(t *testing.T) {
+	t.Run("Interleaved", func(t *testing.T) {
+		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 := &Float32Interleaved{
+			Data: []float32{
+				0.3, -0.7, 0.4, -0.8, 0.5, -0.9,
+			},
+			Size: ChunkInfo{3, 2, 48000},
+		}
+		out := in.SubAudio(2, 3)
+		if !reflect.DeepEqual(expected, out) {
+			t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
+		}
+	})
+	t.Run("NonInterleaved", func(t *testing.T) {
+		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 := &Float32NonInterleaved{
+			Data: [][]float32{
+				{0.3, 0.4, 0.5},
+				{-0.7, -0.8, -0.9},
+			},
+			Size: ChunkInfo{3, 2, 48000},
+		}
+		out := in.SubAudio(2, 3)
+		if !reflect.DeepEqual(expected, out) {
+			t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
+		}
+	})
+}
diff --git a/pkg/wave/int16_test.go b/pkg/wave/int16_test.go
index 6032670..a016fb0 100644
--- a/pkg/wave/int16_test.go
+++ b/pkg/wave/int16_test.go
@@ -51,3 +51,44 @@ func TestInt16(t *testing.T) {
 		})
 	}
 }
+
+func TestInt32SubAudio(t *testing.T) {
+	t.Run("Interleaved", func(t *testing.T) {
+		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 := &Int16Interleaved{
+			Data: []int16{
+				3, -7, 4, -8, 5, -9,
+			},
+			Size: ChunkInfo{3, 2, 48000},
+		}
+		out := in.SubAudio(2, 3)
+		if !reflect.DeepEqual(expected, out) {
+			t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
+		}
+	})
+	t.Run("NonInterleaved", func(t *testing.T) {
+		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 := &Int16NonInterleaved{
+			Data: [][]int16{
+				{3, 4, 5},
+				{-7, -8, -9},
+			},
+			Size: ChunkInfo{3, 2, 48000},
+		}
+		out := in.SubAudio(2, 3)
+		if !reflect.DeepEqual(expected, out) {
+			t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
+		}
+	})
+}
diff --git a/pkg/wave/mixer/mixer_test.go b/pkg/wave/mixer/mixer_test.go
new file mode 100644
index 0000000..40ebdb9
--- /dev/null
+++ b/pkg/wave/mixer/mixer_test.go
@@ -0,0 +1,80 @@
+package mixer
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/pion/mediadevices/pkg/wave"
+)
+
+func TestMonoMixer(t *testing.T) {
+	testCases := map[string]struct {
+		src      wave.Audio
+		dst      wave.Audio
+		expected wave.Audio
+	}{
+		"MultiToMono": {
+			src: &wave.Int16Interleaved{
+				Size: wave.ChunkInfo{
+					Len:      3,
+					Channels: 3,
+				},
+				Data: []int16{
+					0, 2, 4,
+					1, -2, 1,
+					3, 3, 6,
+				},
+			},
+			dst: &wave.Int16Interleaved{
+				Size: wave.ChunkInfo{
+					Len:      3,
+					Channels: 1,
+				},
+				Data: make([]int16, 3),
+			},
+			expected: &wave.Int16Interleaved{
+				Size: wave.ChunkInfo{
+					Len:      3,
+					Channels: 1,
+				},
+				Data: []int16{2, 0, 4},
+			},
+		},
+		"MonoToStereo": {
+			src: &wave.Int16Interleaved{
+				Size: wave.ChunkInfo{
+					Len:      3,
+					Channels: 1,
+				},
+				Data: []int16{0, 2, 4},
+			},
+			dst: &wave.Int16Interleaved{
+				Size: wave.ChunkInfo{
+					Len:      3,
+					Channels: 2,
+				},
+				Data: make([]int16, 6),
+			},
+			expected: &wave.Int16Interleaved{
+				Size: wave.ChunkInfo{
+					Len:      3,
+					Channels: 2,
+				},
+				Data: []int16{0, 0, 2, 2, 4, 4},
+			},
+		},
+	}
+	for name, testCase := range testCases {
+		testCase := testCase
+		t.Run(name, func(t *testing.T) {
+			m := &MonoMixer{}
+			err := m.Mix(testCase.dst, testCase.src)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !reflect.DeepEqual(testCase.expected, testCase.dst) {
+				t.Errorf("Mix result is wrong\nexpected: %v\ngot: %v", testCase.expected, testCase.dst)
+			}
+		})
+	}
+}