Compare commits

..

7 Commits

Author SHA1 Message Date
Renovate Bot
943906e125 Update golang.org/x/image commit hash to e162460
Generated by Renovate Bot
2020-10-01 19:17:59 -04:00
Tarrence van As
f3e3dc9589 use nolibopus in ci 2020-09-29 13:03:21 -04:00
Renovate Bot
a3d374f528 Update github.com/lherman-cs/opus commit hash to 26ea9d3
Generated by Renovate Bot
2020-09-29 13:03:21 -04:00
Lukas Herman
cba0042f5d Fix unalligned panic in 32 bits systems 2020-09-28 20:45:52 -04:00
Atsushi Watanabe
1732e2751d Drop source frames during pause
Source reader should drop frames to catch up the latest frame.
2020-09-28 20:45:52 -04:00
Atsushi Watanabe
5b1527d455 Add broadcast test conditions with pause
Add test case to pause provider feeding or consumer reading
during broadcasting.
2020-09-28 20:45:52 -04:00
Lukas Herman
00f0a44ab1 Add pull-based Broadcaster
* Add generic io.Reader
* Add generic broadcaster
* Add specialize video broadcaster
* Use ring buffer in broadcaster
* Use small delay to relax the schedule in polling
2020-09-28 20:45:52 -04:00
17 changed files with 699 additions and 351 deletions

View File

@@ -30,15 +30,15 @@ jobs:
libvpx-dev \
libx264-dev
- name: go vet
run: go vet ./...
run: go vet -tags nolibopusfile ./...
- name: go build
run: go build ./...
run: go build -tags nolibopusfile ./...
- name: go build without CGO
run: go build . pkg/...
env:
CGO_ENABLED: 0
- name: go test
run: go test ./... -v -race
run: go test -tags nolibopusfile ./... -v -race
- name: go test without CGO
run: go test . pkg/... -v
env:

4
go.mod
View File

@@ -5,9 +5,9 @@ go 1.13
require (
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa
github.com/lherman-cs/opus v0.0.0-20200925064139-8edf1852fd1f
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39
github.com/pion/webrtc/v2 v2.2.26
github.com/satori/go.uuid v1.2.0
golang.org/x/image v0.0.0-20200801110659-972c09e46d76
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a
)

8
go.sum
View File

@@ -22,8 +22,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lherman-cs/opus v0.0.0-20200925064139-8edf1852fd1f h1:xZKyjUoki95rRDQl3mDf20j2WZ+jZaFVzPZO72Jdi4A=
github.com/lherman-cs/opus v0.0.0-20200925064139-8edf1852fd1f/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39 h1:WEYmSwg/uoPVmfmpXWPYplb1UUx/Jr4TXGNrPaI8Cj4=
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
@@ -98,8 +98,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

View File

@@ -80,13 +80,13 @@ func (e *encoder) Read(p []byte) (int, error) {
switch b := buff.(type) {
case *wave.Int16Interleaved:
n, err := e.engine.EncodeBytes(b.Data, p, false)
n, err := e.engine.Encode(b.Data, p)
if err != nil {
return n, err
}
return n, nil
case *wave.Float32Interleaved:
n, err := e.engine.EncodeBytes(b.Data, p, true)
n, err := e.engine.EncodeFloat32(b.Data, p)
if err != nil {
return n, err
}

View File

@@ -66,8 +66,8 @@ func NewBuffer(nSamples int) TransformFunc {
case *wave.Int16Interleaved:
ibCopy := *ib
ibCopy.Size.Len = nSamples
n := nSamples * ib.Size.Channels * 2
ibCopy.Data = make([]uint8, n)
n := nSamples * ib.Size.Channels
ibCopy.Data = make([]int16, n)
copy(ibCopy.Data, ib.Data)
ib.Data = ib.Data[n:]
ib.Size.Len -= nSamples
@@ -76,8 +76,8 @@ func NewBuffer(nSamples int) TransformFunc {
case *wave.Float32Interleaved:
ibCopy := *ib
ibCopy.Size.Len = nSamples
n := nSamples * ib.Size.Channels * 4
ibCopy.Data = make([]uint8, n)
n := nSamples * ib.Size.Channels
ibCopy.Data = make([]float32, n)
copy(ibCopy.Data, ib.Data)
ib.Data = ib.Data[n:]
ib.Size.Len -= nSamples

View File

@@ -9,83 +9,41 @@ import (
)
func TestBuffer(t *testing.T) {
input1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234})
input1.SetInt16(0, 0, 1)
input1.SetInt16(0, 1, 2)
input2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
input2.SetInt16(0, 0, 3)
input2.SetInt16(0, 1, 4)
input2.SetInt16(1, 0, 5)
input2.SetInt16(1, 1, 6)
input2.SetInt16(2, 0, 7)
input2.SetInt16(2, 1, 8)
input3 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 2, Channels: 2, SamplingRate: 1234})
input3.SetInt16(0, 0, 9)
input3.SetInt16(0, 1, 10)
input3.SetInt16(1, 0, 11)
input3.SetInt16(1, 1, 12)
input4 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 7, Channels: 2, SamplingRate: 1234})
input4.SetInt16(0, 0, 13)
input4.SetInt16(0, 1, 14)
input4.SetInt16(1, 0, 15)
input4.SetInt16(1, 1, 16)
input4.SetInt16(2, 0, 17)
input4.SetInt16(2, 1, 18)
input4.SetInt16(3, 0, 19)
input4.SetInt16(3, 1, 20)
input4.SetInt16(4, 0, 21)
input4.SetInt16(4, 1, 22)
input4.SetInt16(5, 0, 23)
input4.SetInt16(5, 1, 24)
input4.SetInt16(6, 0, 25)
input4.SetInt16(6, 1, 26)
expected1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected1.SetInt16(0, 0, 1)
expected1.SetInt16(0, 1, 2)
expected1.SetInt16(1, 0, 3)
expected1.SetInt16(1, 1, 4)
expected1.SetInt16(2, 0, 5)
expected1.SetInt16(2, 1, 6)
expected2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected2.SetInt16(0, 0, 7)
expected2.SetInt16(0, 1, 8)
expected2.SetInt16(1, 0, 9)
expected2.SetInt16(1, 1, 10)
expected2.SetInt16(2, 0, 11)
expected2.SetInt16(2, 1, 12)
expected3 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected3.SetInt16(0, 0, 13)
expected3.SetInt16(0, 1, 14)
expected3.SetInt16(1, 0, 15)
expected3.SetInt16(1, 1, 16)
expected3.SetInt16(2, 0, 17)
expected3.SetInt16(2, 1, 18)
expected4 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected4.SetInt16(0, 0, 19)
expected4.SetInt16(0, 1, 20)
expected4.SetInt16(1, 0, 21)
expected4.SetInt16(1, 1, 22)
expected4.SetInt16(2, 0, 23)
expected4.SetInt16(2, 1, 24)
input := []wave.Audio{
input1,
input2,
input3,
input4,
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234},
Data: []int16{1, 2},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{3, 4, 5, 6, 7, 8},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 2, Channels: 2, SamplingRate: 1234},
Data: []int16{9, 10, 11, 12},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 7, Channels: 2, SamplingRate: 1234},
Data: []int16{13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
},
}
expected := []wave.Audio{
expected1,
expected2,
expected3,
expected4,
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{1, 2, 3, 4, 5, 6},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{7, 8, 9, 10, 11, 12},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{13, 14, 15, 16, 17, 18},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{19, 20, 21, 22, 23, 24},
},
}
trans := NewBuffer(3)

View File

@@ -10,33 +10,25 @@ import (
)
func TestMixer(t *testing.T) {
input1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234})
input1.SetInt16(0, 0, 1)
input1.SetInt16(0, 1, 3)
input2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
input2.SetInt16(0, 0, 2)
input2.SetInt16(0, 1, 4)
input2.SetInt16(1, 0, 3)
input2.SetInt16(1, 1, 5)
input2.SetInt16(2, 0, 4)
input2.SetInt16(2, 1, 6)
expected1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 1, Channels: 1, SamplingRate: 1234})
expected1.SetInt16(0, 0, 2)
expected2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 1, SamplingRate: 1234})
expected2.SetInt16(0, 0, 3)
expected2.SetInt16(1, 0, 4)
expected2.SetInt16(2, 0, 5)
input := []wave.Audio{
input1,
input2,
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234},
Data: []int16{1, 3},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{2, 4, 3, 5, 4, 6},
},
}
expected := []wave.Audio{
expected1,
expected2,
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 1, Channels: 1, SamplingRate: 1234},
Data: []int16{2},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 1, SamplingRate: 1234},
Data: []int16{3, 4, 5},
},
}
trans := NewChannelMixer(1, &mixer.MonoMixer{})

162
pkg/io/broadcast.go Normal file
View File

@@ -0,0 +1,162 @@
package io
import (
"fmt"
"sync/atomic"
"time"
)
const (
maskReading = 1 << 63
defaultBroadcasterRingSize = 32
// TODO: If the data source has fps greater than 30, they'll see some
// fps fluctuation. But, 30 fps should be enough for general cases.
defaultBroadcasterRingPollDuration = time.Millisecond * 33
)
var errEmptySource = fmt.Errorf("Source can't be nil")
type broadcasterData struct {
data interface{}
count uint32
err error
}
type broadcasterRing struct {
// reading (1 bit) + reserved (31 bits) + data count (32 bits)
// IMPORTANT: state has to be the first element in struct, otherwise LoadUint64 will panic in 32 bits systems
// due to unallignment
state uint64
buffer []atomic.Value
pollDuration time.Duration
}
func newBroadcasterRing(size uint, pollDuration time.Duration) *broadcasterRing {
return &broadcasterRing{buffer: make([]atomic.Value, size), pollDuration: pollDuration}
}
func (ring *broadcasterRing) index(count uint32) int {
return int(count) % len(ring.buffer)
}
func (ring *broadcasterRing) acquire(count uint32) func(*broadcasterData) {
// Reader has reached the latest data, should read from the source.
// Only allow 1 reader to read from the source. When there are more than 1 readers,
// the other readers will need to share the same data that the first reader gets from
// the source.
state := uint64(count)
if atomic.CompareAndSwapUint64(&ring.state, state, state|maskReading) {
return func(data *broadcasterData) {
i := ring.index(count)
ring.buffer[i].Store(data)
atomic.StoreUint64(&ring.state, uint64(count+1))
}
}
return nil
}
func (ring *broadcasterRing) get(count uint32) *broadcasterData {
for {
reading := uint64(count) | maskReading
// TODO: since it's lockless, it spends a lot of resources in the scheduling.
for atomic.LoadUint64(&ring.state) == reading {
// Yield current goroutine to let other goroutines to run instead
time.Sleep(ring.pollDuration)
}
i := ring.index(count)
data := ring.buffer[i].Load().(*broadcasterData)
if data.count == count {
return data
}
count++
}
}
func (ring *broadcasterRing) lastCount() uint32 {
// ring.state always keeps track the next count, so we need to subtract it by 1 to get the
// last count
return uint32(atomic.LoadUint64(&ring.state)) - 1
}
// Broadcaster is a generic pull-based broadcaster. Broadcaster is unique in a sense that
// readers can come and go at anytime, and readers don't need to close or notify broadcaster.
type Broadcaster struct {
source atomic.Value
buffer *broadcasterRing
}
// BroadcasterConfig is a config to control broadcaster behaviour
type BroadcasterConfig struct {
// BufferSize configures the underlying ring buffer size that's being used
// to avoid data lost for late readers. The default value is 32.
BufferSize uint
// PollDuration configures the sleep duration in waiting for new data to come.
// The default value is 33 ms.
PollDuration time.Duration
}
// NewBroadcaster creates a new broadcaster. Source is expected to drop frames
// when any of the readers is slower than the source.
func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
pollDuration := defaultBroadcasterRingPollDuration
var bufferSize uint = defaultBroadcasterRingSize
if config != nil {
if config.PollDuration != 0 {
pollDuration = config.PollDuration
}
if config.BufferSize != 0 {
bufferSize = config.BufferSize
}
}
var broadcaster Broadcaster
broadcaster.buffer = newBroadcasterRing(bufferSize, pollDuration)
broadcaster.ReplaceSource(source)
return &broadcaster
}
// NewReader creates a new reader. Each reader will retrieve the same data from the source.
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
// in the ring buffer.
func (broadcaster *Broadcaster) NewReader(copyFn func(interface{}) interface{}) Reader {
currentCount := broadcaster.buffer.lastCount()
return ReaderFunc(func() (data interface{}, err error) {
currentCount++
if push := broadcaster.buffer.acquire(currentCount); push != nil {
data, err = broadcaster.source.Load().(Reader).Read()
push(&broadcasterData{
data: data,
err: err,
count: currentCount,
})
} else {
ringData := broadcaster.buffer.get(currentCount)
data, err, currentCount = ringData.data, ringData.err, ringData.count
}
data = copyFn(data)
return
})
}
// ReplaceSource replaces the underlying source. This operation is thread safe.
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
if source == nil {
return errEmptySource
}
broadcaster.source.Store(source)
return nil
}
// ReplaceSource retrieves the underlying source. This operation is thread safe.
func (broadcaster *Broadcaster) Source() Reader {
return broadcaster.source.Load().(Reader)
}

14
pkg/io/reader.go Normal file
View File

@@ -0,0 +1,14 @@
package io
// Reader is a generic data reader. In the future, interface{} should be replaced by a generic type
// to provide strong type.
type Reader interface {
Read() (interface{}, error)
}
// ReaderFunc is a proxy type for Reader
type ReaderFunc func() (interface{}, error)
func (f ReaderFunc) Read() (interface{}, error) {
return f()
}

76
pkg/io/video/broadcast.go Normal file
View File

@@ -0,0 +1,76 @@
package video
import (
"fmt"
"image"
"github.com/pion/mediadevices/pkg/io"
)
var errEmptySource = fmt.Errorf("Source can't be nil")
// Broadcaster is a specialized video broadcaster.
type Broadcaster struct {
ioBroadcaster *io.Broadcaster
}
type BroadcasterConfig struct {
Core *io.BroadcasterConfig
}
// NewBroadcaster creates a new broadcaster. Source is expected to drop frames
// when any of the readers is slower than the source.
func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
var coreConfig *io.BroadcasterConfig
if config != nil {
coreConfig = config.Core
}
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, error) {
return source.Read()
}), coreConfig)
return &Broadcaster{broadcaster}
}
// NewReader creates a new reader. Each reader will retrieve the same data from the source.
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
// in the ring buffer.
func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
copyFn := func(src interface{}) interface{} { return src }
if copyFrame {
buffer := NewFrameBuffer(0)
copyFn = func(src interface{}) interface{} {
realSrc, _ := src.(image.Image)
buffer.StoreCopy(realSrc)
return buffer.Load()
}
}
reader := broadcaster.ioBroadcaster.NewReader(copyFn)
return ReaderFunc(func() (image.Image, error) {
data, err := reader.Read()
img, _ := data.(image.Image)
return img, err
})
}
// ReplaceSource replaces the underlying source. This operation is thread safe.
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, error) {
return source.Read()
}))
}
// Source retrieves the underlying source. This operation is thread safe.
func (broadcaster *Broadcaster) Source() Reader {
source := broadcaster.ioBroadcaster.Source()
return ReaderFunc(func() (image.Image, error) {
data, err := source.Read()
img, _ := data.(image.Image)
return img, err
})
}

View File

@@ -0,0 +1,187 @@
package video
import (
"fmt"
"image"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
func BenchmarkBroadcast(b *testing.B) {
var src Reader
img := image.NewRGBA(image.Rect(0, 0, 1920, 1080))
interval := time.NewTicker(time.Millisecond * 33) // 30 fps
defer interval.Stop()
src = ReaderFunc(func() (image.Image, error) {
<-interval.C
return img, nil
})
for n := 1; n <= 4096; n *= 16 {
n := n
b.Run(fmt.Sprintf("Readers-%d", n), func(b *testing.B) {
b.SetParallelism(n)
broadcaster := NewBroadcaster(src, nil)
b.RunParallel(func(pb *testing.PB) {
reader := broadcaster.NewReader(false)
for pb.Next() {
reader.Read()
}
})
})
}
}
func TestBroadcast(t *testing.T) {
// https://github.com/pion/mediadevices/issues/198
if runtime.GOOS == "darwin" {
t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
}
frames := make([]image.Image, 5*30) // 5 seconds worth of frames
resolution := image.Rect(0, 0, 1920, 1080)
for i := range frames {
rgba := image.NewRGBA(resolution)
rgba.Pix[0] = uint8(i >> 24)
rgba.Pix[1] = uint8(i >> 16)
rgba.Pix[2] = uint8(i >> 8)
rgba.Pix[3] = uint8(i)
frames[i] = rgba
}
routinePauseConds := []struct {
src bool
dst bool
expectedFPS float64
expectedDrop float64
}{
{
src: false,
dst: false,
expectedFPS: 30,
},
{
src: true,
dst: false,
expectedFPS: 20,
expectedDrop: 10,
},
{
src: false,
dst: true,
expectedFPS: 20,
expectedDrop: 10,
},
}
for _, pauseCond := range routinePauseConds {
pauseCond := pauseCond
t.Run(fmt.Sprintf("SrcPause-%v/DstPause-%v", pauseCond.src, pauseCond.dst), func(t *testing.T) {
for n := 1; n <= 256; n *= 16 {
n := n
t.Run(fmt.Sprintf("Readers-%d", n), func(t *testing.T) {
var src Reader
interval := time.NewTicker(time.Millisecond * 33) // 30 fps
defer interval.Stop()
frameCount := 0
frameSent := 0
lastSend := time.Now()
src = ReaderFunc(func() (image.Image, error) {
if pauseCond.src && frameSent == 30 {
time.Sleep(time.Second)
}
<-interval.C
now := time.Now()
if interval := now.Sub(lastSend); interval > time.Millisecond*33*3/2 {
// Source reader should drop frames to catch up the latest frame.
drop := int(interval/(time.Millisecond*33)) - 1
frameCount += drop
t.Logf("Skipped %d frames", drop)
}
lastSend = now
frame := frames[frameCount]
frameCount++
frameSent++
return frame, nil
})
broadcaster := NewBroadcaster(src, nil)
var done uint32
duration := time.Second * 3
fpsChan := make(chan []float64)
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
reader := broadcaster.NewReader(false)
count := 0
lastFrameCount := -1
droppedFrames := 0
wg.Done()
wg.Wait()
for atomic.LoadUint32(&done) == 0 {
if pauseCond.dst && count == 30 {
time.Sleep(time.Second)
}
frame, err := reader.Read()
if err != nil {
t.Error(err)
}
rgba := frame.(*image.RGBA)
var frameCount int
frameCount |= int(rgba.Pix[0]) << 24
frameCount |= int(rgba.Pix[1]) << 16
frameCount |= int(rgba.Pix[2]) << 8
frameCount |= int(rgba.Pix[3])
droppedFrames += (frameCount - lastFrameCount - 1)
lastFrameCount = frameCount
count++
}
fps := float64(count) / duration.Seconds()
if fps < pauseCond.expectedFPS-2 || fps > pauseCond.expectedFPS+2 {
t.Fatal("Unexpected average FPS")
}
droppedFramesPerSecond := float64(droppedFrames) / duration.Seconds()
if droppedFramesPerSecond < pauseCond.expectedDrop-2 || droppedFramesPerSecond > pauseCond.expectedDrop+2 {
t.Fatal("Unexpected drop count")
}
fpsChan <- []float64{fps, droppedFramesPerSecond, float64(lastFrameCount)}
}()
}
time.Sleep(duration)
atomic.StoreUint32(&done, 1)
var fpsAvg float64
var droppedFramesPerSecondAvg float64
var lastFrameCountAvg float64
var count int
for metric := range fpsChan {
fps, droppedFramesPerSecond, lastFrameCount := metric[0], metric[1], metric[2]
fpsAvg += fps
droppedFramesPerSecondAvg += droppedFramesPerSecond
lastFrameCountAvg += lastFrameCount
count++
if count == n {
break
}
}
t.Log("Average FPS :", fpsAvg/float64(n))
t.Log("Average dropped frames per second:", droppedFramesPerSecondAvg/float64(n))
t.Log("Last frame count (src) :", frameCount)
t.Log("Average last frame count (dst) :", lastFrameCountAvg/float64(n))
})
}
})
}
}

View File

@@ -134,15 +134,18 @@ func TestDecodeInt16Interleaved(t *testing.T) {
decoder, _ := newInt16InterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := NewInt16Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x01, 0x02})))
expected.SetInt16(0, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x03, 0x04})))
expected.SetInt16(1, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x05, 0x06})))
expected.SetInt16(1, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x07, 0x08})))
expected := &Int16Interleaved{
Data: []int16{
int16(binary.BigEndian.Uint16([]byte{0x01, 0x02})),
int16(binary.BigEndian.Uint16([]byte{0x03, 0x04})),
int16(binary.BigEndian.Uint16([]byte{0x05, 0x06})),
int16(binary.BigEndian.Uint16([]byte{0x07, 0x08})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -154,15 +157,18 @@ func TestDecodeInt16Interleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := NewInt16Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x02, 0x01})))
expected.SetInt16(0, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x04, 0x03})))
expected.SetInt16(1, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x06, 0x05})))
expected.SetInt16(1, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x08, 0x07})))
expected := &Int16Interleaved{
Data: []int16{
int16(binary.LittleEndian.Uint16([]byte{0x01, 0x02})),
int16(binary.LittleEndian.Uint16([]byte{0x03, 0x04})),
int16(binary.LittleEndian.Uint16([]byte{0x05, 0x06})),
int16(binary.LittleEndian.Uint16([]byte{0x07, 0x08})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -184,15 +190,16 @@ func TestDecodeInt16NonInterleaved(t *testing.T) {
decoder, _ := newInt16NonInterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := NewInt16NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x01, 0x02})))
expected.SetInt16(0, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x05, 0x06})))
expected.SetInt16(1, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x03, 0x04})))
expected.SetInt16(1, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x07, 0x08})))
expected := &Int16NonInterleaved{
Data: [][]int16{
{int16(binary.BigEndian.Uint16([]byte{0x01, 0x02})), int16(binary.BigEndian.Uint16([]byte{0x03, 0x04}))},
{int16(binary.BigEndian.Uint16([]byte{0x05, 0x06})), int16(binary.BigEndian.Uint16([]byte{0x07, 0x08}))},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -204,15 +211,16 @@ func TestDecodeInt16NonInterleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := NewInt16NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x02, 0x01})))
expected.SetInt16(0, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x06, 0x05})))
expected.SetInt16(1, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x04, 0x03})))
expected.SetInt16(1, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x08, 0x07})))
expected := &Int16NonInterleaved{
Data: [][]int16{
{int16(binary.LittleEndian.Uint16([]byte{0x01, 0x02})), int16(binary.LittleEndian.Uint16([]byte{0x03, 0x04}))},
{int16(binary.LittleEndian.Uint16([]byte{0x05, 0x06})), int16(binary.LittleEndian.Uint16([]byte{0x07, 0x08}))},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -234,15 +242,18 @@ func TestDecodeFloat32Interleaved(t *testing.T) {
decoder, _ := newFloat32InterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := NewFloat32Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10}))))
expected := &Float32Interleaved{
Data: []float32{
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -254,15 +265,18 @@ func TestDecodeFloat32Interleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := NewFloat32Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x04, 0x03, 0x02, 0x01}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x08, 0x07, 0x06, 0x05}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0c, 0x0b, 0x0a, 0x09}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x10, 0x0f, 0x0e, 0x0d}))))
expected := &Float32Interleaved{
Data: []float32{
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -284,15 +298,22 @@ func TestDecodeFloat32NonInterleaved(t *testing.T) {
decoder, _ := newFloat32NonInterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := NewFloat32NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10}))))
expected := &Float32NonInterleaved{
Data: [][]float32{
{
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
},
{
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -304,15 +325,22 @@ func TestDecodeFloat32NonInterleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := NewFloat32NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x04, 0x03, 0x02, 0x01}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0c, 0x0b, 0x0a, 0x09}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x08, 0x07, 0x06, 0x05}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x10, 0x0f, 0x0e, 0x0d}))))
expected := &Float32NonInterleaved{
Data: [][]float32{
{
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
},
{
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)

View File

@@ -1,7 +1,5 @@
package wave
import "math"
// Float32Sample is a 32-bits float audio sample.
type Float32Sample float32
@@ -11,7 +9,7 @@ func (s Float32Sample) Int() int64 {
// Float32Interleaved multi-channel interlaced Audio.
type Float32Interleaved struct {
Data []uint8
Data []float32
Size ChunkInfo
}
@@ -25,36 +23,22 @@ func (a *Float32Interleaved) SampleFormat() SampleFormat {
}
func (a *Float32Interleaved) At(i, ch int) Sample {
loc := 4 * (a.Size.Channels*i + ch)
var v uint32
v |= uint32(a.Data[loc]) << 24
v |= uint32(a.Data[loc+1]) << 16
v |= uint32(a.Data[loc+2]) << 8
v |= uint32(a.Data[loc+3])
return Float32Sample(math.Float32frombits(v))
return Float32Sample(a.Data[i*a.Size.Channels+ch])
}
func (a *Float32Interleaved) Set(i, ch int, s Sample) {
a.SetFloat32(i, ch, Float32SampleFormat.Convert(s).(Float32Sample))
a.Data[i*a.Size.Channels+ch] = float32(Float32SampleFormat.Convert(s).(Float32Sample))
}
func (a *Float32Interleaved) SetFloat32(i, ch int, s Float32Sample) {
loc := 4 * (a.Size.Channels*i + ch)
v := math.Float32bits(float32(s))
a.Data[loc] = uint8(v >> 24)
a.Data[loc+1] = uint8(v >> 16)
a.Data[loc+2] = uint8(v >> 8)
a.Data[loc+3] = uint8(v)
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 := 4 * offsetSamples * a.Size.Channels
n := 4 * nSamples * a.Size.Channels
offset := offsetSamples * a.Size.Channels
n := nSamples * a.Size.Channels
ret.Data = ret.Data[offset : offset+n]
ret.Size.Len = nSamples
return &ret
@@ -62,14 +46,14 @@ func (a *Float32Interleaved) SubAudio(offsetSamples, nSamples int) *Float32Inter
func NewFloat32Interleaved(size ChunkInfo) *Float32Interleaved {
return &Float32Interleaved{
Data: make([]uint8, size.Channels*size.Len*4),
Data: make([]float32, size.Channels*size.Len),
Size: size,
}
}
// Float32NonInterleaved multi-channel interlaced Audio.
type Float32NonInterleaved struct {
Data [][]uint8
Data [][]float32
Size ChunkInfo
}
@@ -83,48 +67,31 @@ func (a *Float32NonInterleaved) SampleFormat() SampleFormat {
}
func (a *Float32NonInterleaved) At(i, ch int) Sample {
loc := i * 4
var v uint32
v |= uint32(a.Data[ch][loc]) << 24
v |= uint32(a.Data[ch][loc+1]) << 16
v |= uint32(a.Data[ch][loc+2]) << 8
v |= uint32(a.Data[ch][loc+3])
return Float32Sample(math.Float32frombits(v))
return Float32Sample(a.Data[ch][i])
}
func (a *Float32NonInterleaved) Set(i, ch int, s Sample) {
a.SetFloat32(i, ch, Float32SampleFormat.Convert(s).(Float32Sample))
a.Data[ch][i] = float32(Float32SampleFormat.Convert(s).(Float32Sample))
}
func (a *Float32NonInterleaved) SetFloat32(i, ch int, s Float32Sample) {
loc := i * 4
v := math.Float32bits(float32(s))
a.Data[ch][loc] = uint8(v >> 24)
a.Data[ch][loc+1] = uint8(v >> 16)
a.Data[ch][loc+2] = uint8(v >> 8)
a.Data[ch][loc+3] = uint8(v)
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
ret.Size.Len = nSamples
offsetSamples *= 4
nSamples *= 4
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([][]uint8, size.Channels)
d := make([][]float32, size.Channels)
for i := 0; i < size.Channels; i++ {
d[i] = make([]uint8, size.Len*4)
d[i] = make([]float32, size.Len)
}
return &Float32NonInterleaved{
Data: d,

View File

@@ -1,51 +1,39 @@
package wave
import (
"math"
"reflect"
"testing"
)
func float32ToUint8(vs ...float32) []uint8 {
var b []uint8
for _, v := range vs {
s := math.Float32bits(v)
b = append(b, uint8(s>>24), uint8(s>>16), uint8(s>>8), uint8(s))
}
return b
}
func TestFloat32(t *testing.T) {
expected := [][]float32{
{0.0, 1.0, 2.0, 3.0},
{4.0, 5.0, 6.0, 7.0},
}
cases := map[string]struct {
in Audio
expected [][]float32
}{
"Interleaved": {
in: &Float32Interleaved{
Data: float32ToUint8(
0.0, 4.0, 1.0, 5.0,
2.0, 6.0, 3.0, 7.0,
),
Size: ChunkInfo{4, 2, 48000},
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},
},
expected: expected,
},
"NonInterleaved": {
in: &Float32NonInterleaved{
Data: [][]uint8{
float32ToUint8(expected[0]...),
float32ToUint8(expected[1]...),
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{4, 2, 48000},
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},
},
expected: expected,
},
}
for name, c := range cases {
@@ -67,43 +55,38 @@ func TestFloat32(t *testing.T) {
func TestFloat32SubAudio(t *testing.T) {
t.Run("Interleaved", func(t *testing.T) {
in := &Float32Interleaved{
// Data: []uint8{
// 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
// 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
// },
Data: float32ToUint8(
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
),
Size: ChunkInfo{4, 2, 48000},
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: float32ToUint8(
5.0, 6.0, 7.0, 8.0,
),
Size: ChunkInfo{2, 2, 48000},
Data: []float32{
0.3, -0.7, 0.4, -0.8, 0.5, -0.9,
},
Size: ChunkInfo{3, 2, 48000},
}
out := in.SubAudio(2, 2)
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: [][]uint8{
float32ToUint8(1.0, 2.0, 3.0, 4.0),
float32ToUint8(5.0, 6.0, 7.0, 8.0),
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{4, 2, 48000},
Size: ChunkInfo{8, 2, 48000},
}
expected := &Float32NonInterleaved{
Data: [][]uint8{
float32ToUint8(3.0, 4.0),
float32ToUint8(7.0, 8.0),
Data: [][]float32{
{0.3, 0.4, 0.5},
{-0.7, -0.8, -0.9},
},
Size: ChunkInfo{2, 2, 48000},
Size: ChunkInfo{3, 2, 48000},
}
out := in.SubAudio(2, 2)
out := in.SubAudio(2, 3)
if !reflect.DeepEqual(expected, out) {
t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
}

View File

@@ -9,7 +9,7 @@ func (s Int16Sample) Int() int64 {
// Int16Interleaved multi-channel interlaced Audio.
type Int16Interleaved struct {
Data []uint8
Data []int16
Size ChunkInfo
}
@@ -23,29 +23,22 @@ func (a *Int16Interleaved) SampleFormat() SampleFormat {
}
func (a *Int16Interleaved) At(i, ch int) Sample {
loc := 2 * (i*a.Size.Channels + ch)
var s Int16Sample
s |= Int16Sample(a.Data[loc]) << 8
s |= Int16Sample(a.Data[loc+1])
return s
return Int16Sample(a.Data[i*a.Size.Channels+ch])
}
func (a *Int16Interleaved) Set(i, ch int, s Sample) {
a.SetInt16(i, ch, Int16SampleFormat.Convert(s).(Int16Sample))
a.Data[i*a.Size.Channels+ch] = int16(Int16SampleFormat.Convert(s).(Int16Sample))
}
func (a *Int16Interleaved) SetInt16(i, ch int, s Int16Sample) {
loc := 2 * (i*a.Size.Channels + ch)
a.Data[loc] = uint8(s >> 8)
a.Data[loc+1] = uint8(s)
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 := 2 * offsetSamples * a.Size.Channels
n := 2 * nSamples * a.Size.Channels
offset := offsetSamples * a.Size.Channels
n := nSamples * a.Size.Channels
ret.Data = ret.Data[offset : offset+n]
ret.Size.Len = nSamples
return &ret
@@ -53,14 +46,14 @@ func (a *Int16Interleaved) SubAudio(offsetSamples, nSamples int) *Int16Interleav
func NewInt16Interleaved(size ChunkInfo) *Int16Interleaved {
return &Int16Interleaved{
Data: make([]uint8, size.Channels*size.Len*2),
Data: make([]int16, size.Channels*size.Len),
Size: size,
}
}
// Int16NonInterleaved multi-channel interlaced Audio.
type Int16NonInterleaved struct {
Data [][]uint8
Data [][]int16
Size ChunkInfo
}
@@ -74,41 +67,31 @@ func (a *Int16NonInterleaved) SampleFormat() SampleFormat {
}
func (a *Int16NonInterleaved) At(i, ch int) Sample {
loc := i * 2
var s Int16Sample
s |= Int16Sample(a.Data[ch][loc]) << 8
s |= Int16Sample(a.Data[ch][loc+1])
return s
return Int16Sample(a.Data[ch][i])
}
func (a *Int16NonInterleaved) Set(i, ch int, s Sample) {
a.SetInt16(i, ch, Int16SampleFormat.Convert(s).(Int16Sample))
a.Data[ch][i] = int16(Int16SampleFormat.Convert(s).(Int16Sample))
}
func (a *Int16NonInterleaved) SetInt16(i, ch int, s Int16Sample) {
loc := i * 2
a.Data[ch][loc] = uint8(s >> 8)
a.Data[ch][loc+1] = uint8(s)
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
ret.Size.Len = nSamples
nSamples *= 2
offsetSamples *= 2
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([][]uint8, size.Channels)
d := make([][]int16, size.Channels)
for i := 0; i < size.Channels; i++ {
d[i] = make([]uint8, size.Len*2)
d[i] = make([]int16, size.Len)
}
return &Int16NonInterleaved{
Data: d,

View File

@@ -12,28 +12,27 @@ func TestInt16(t *testing.T) {
}{
"Interleaved": {
in: &Int16Interleaved{
Data: []uint8{
0, 1, 1, 2, 2, 3, 3, 4,
4, 5, 5, 6, 6, 7, 7, 8,
Data: []int16{
1, -5, 2, -6, 3, -7, 4, -8, 5, -9, 6, -10, 7, -11, 8, -12,
},
Size: ChunkInfo{4, 2, 48000},
Size: ChunkInfo{8, 2, 48000},
},
expected: [][]int16{
{(0 << 8) | 1, (2 << 8) | 3, (4 << 8) | 5, (6 << 8) | 7},
{(1 << 8) | 2, (3 << 8) | 4, (5 << 8) | 6, (7 << 8) | 8},
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
},
},
"NonInterleaved": {
in: &Int16NonInterleaved{
Data: [][]uint8{
{0, 1, 2, 3, 4, 5, 6, 7},
Data: [][]int16{
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
},
Size: ChunkInfo{4, 2, 48000},
Size: ChunkInfo{8, 2, 48000},
},
expected: [][]int16{
{(0 << 8) | 1, (2 << 8) | 3, (4 << 8) | 5, (6 << 8) | 7},
{(1 << 8) | 2, (3 << 8) | 4, (5 << 8) | 6, (7 << 8) | 8},
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
},
},
}
@@ -56,39 +55,38 @@ func TestInt16(t *testing.T) {
func TestInt32SubAudio(t *testing.T) {
t.Run("Interleaved", func(t *testing.T) {
in := &Int16Interleaved{
Data: []uint8{
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
Data: []int16{
1, -5, 2, -6, 3, -7, 4, -8, 5, -9, 6, -10, 7, -11, 8, -12,
},
Size: ChunkInfo{4, 2, 48000},
Size: ChunkInfo{8, 2, 48000},
}
expected := &Int16Interleaved{
Data: []uint8{
9, 10, 11, 12, 13, 14, 15, 16,
Data: []int16{
3, -7, 4, -8, 5, -9,
},
Size: ChunkInfo{2, 2, 48000},
Size: ChunkInfo{3, 2, 48000},
}
out := in.SubAudio(2, 2)
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: [][]uint8{
{1, 2, 5, 6, 9, 10, 13, 14},
{3, 4, 7, 8, 11, 12, 15, 16},
Data: [][]int16{
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
},
Size: ChunkInfo{4, 2, 48000},
Size: ChunkInfo{8, 2, 48000},
}
expected := &Int16NonInterleaved{
Data: [][]uint8{
{9, 10, 13, 14},
{11, 12, 15, 16},
Data: [][]int16{
{3, 4, 5},
{-7, -8, -9},
},
Size: ChunkInfo{2, 2, 48000},
Size: ChunkInfo{3, 2, 48000},
}
out := in.SubAudio(2, 2)
out := in.SubAudio(2, 3)
if !reflect.DeepEqual(expected, out) {
t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
}

View File

@@ -19,10 +19,10 @@ func TestMonoMixer(t *testing.T) {
Len: 3,
Channels: 3,
},
Data: []uint8{
0x00, 0x01, 0x00, 0x02, 0x00, 0x04,
0x00, 0x01, 0x00, 0x02, 0x00, 0x01,
0x00, 0x03, 0x00, 0x03, 0x00, 0x06,
Data: []int16{
0, 2, 4,
1, -2, 1,
3, 3, 6,
},
},
dst: &wave.Int16Interleaved{
@@ -30,14 +30,14 @@ func TestMonoMixer(t *testing.T) {
Len: 3,
Channels: 1,
},
Data: make([]uint8, 3*2),
Data: make([]int16, 3),
},
expected: &wave.Int16Interleaved{
Size: wave.ChunkInfo{
Len: 3,
Channels: 1,
},
Data: []uint8{0x00, 0x02, 0x00, 0x01, 0x00, 0x04},
Data: []int16{2, 0, 4},
},
},
"MonoToStereo": {
@@ -46,21 +46,21 @@ func TestMonoMixer(t *testing.T) {
Len: 3,
Channels: 1,
},
Data: []uint8{0x00, 0x00, 0x00, 0x02, 0x00, 0x04},
Data: []int16{0, 2, 4},
},
dst: &wave.Int16Interleaved{
Size: wave.ChunkInfo{
Len: 3,
Channels: 2,
},
Data: make([]uint8, 6*2),
Data: make([]int16, 6),
},
expected: &wave.Int16Interleaved{
Size: wave.ChunkInfo{
Len: 3,
Channels: 2,
},
Data: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x04, 0x00, 0x04},
Data: []int16{0, 0, 2, 2, 4, 4},
},
},
}