Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Loop2 that supports start and end positions #176

Merged
merged 5 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions compositors.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package beep

import (
"fmt"
"math"

"github.com/pkg/errors"
)

// Take returns a Streamer which streams at most num samples from s.
//
// The returned Streamer propagates s's errors through Err.
Expand Down Expand Up @@ -32,6 +39,12 @@ func (t *take) Err() error {
// Loop takes a StreamSeeker and plays it count times. If count is negative, s is looped infinitely.
//
// The returned Streamer propagates s's errors.
//
// Deprecated: use Loop2 instead. A call to Loop can be rewritten as follows:
// - beep.Loop(-1, s) -> beep.Loop2(s)
// - beep.Loop(0, s) -> no longer supported, use beep.Ctrl instead.
// - beep.Loop(3, s) -> beep.Loop2(s, beep.LoopTimes(2))
// Note that beep.LoopTimes takes the number of repeats instead of the number of total plays.
func Loop(count int, s StreamSeeker) Streamer {
return &loop{
s: s,
Expand Down Expand Up @@ -73,6 +86,128 @@ func (l *loop) Err() error {
return l.s.Err()
}

type LoopOption func(opts *loop2)

// LoopTimes sets how many times the source stream will repeat. If a section is defined
// by LoopStart, LoopEnd, or LoopBetween, only that section will repeat.
// A value of 0 plays the stream or section once (no repetition); 1 plays it twice, and so on.
func LoopTimes(times int) LoopOption {
if times < 0 {
panic("invalid argument to LoopTimes; times cannot be negative")
}
return func(loop *loop2) {
loop.remains = times
}
}

// LoopStart sets the position in the source stream to which it returns (using Seek())
// after reaching the end of the stream or the position set using LoopEnd. The samples
// before this position are played once before the loop begins.
func LoopStart(pos int) LoopOption {
if pos < 0 {
panic("invalid argument to LoopStart; pos cannot be negative")
}
return func(loop *loop2) {
loop.start = pos
}
}

// LoopEnd sets the position (exclusive) in the source stream up to which the stream plays
// before returning (seeking) back to the start of the stream or the position set by LoopStart.
// The samples after this position are played once after looping completes.
func LoopEnd(pos int) LoopOption {
if pos < 0 {
panic("invalid argument to LoopEnd; pos cannot be negative")
}
return func(loop *loop2) {
loop.end = pos
}
}

// LoopBetween sets both the LoopStart and LoopEnd positions simultaneously, specifying
// the section of the stream that will be looped.
func LoopBetween(start, end int) LoopOption {
return func(opts *loop2) {
LoopStart(start)(opts)
LoopEnd(end)(opts)
}
}

// Loop2 takes a StreamSeeker and repeats it according to the specified options. If no LoopTimes
// option is provided, the stream loops indefinitely. LoopStart, LoopEnd, or LoopBetween can define
// a specific section of the stream to loop. Samples before the start and after the end positions
// are played once before and after the looping section, respectively.
//
// The returned Streamer propagates any errors from s.
func Loop2(s StreamSeeker, opts ...LoopOption) (Streamer, error) {
l := &loop2{
s: s,
remains: -1, // indefinitely
start: 0,
end: math.MaxInt,
}
for _, opt := range opts {
opt(l)
}

n := s.Len()
if l.start >= n {
return nil, errors.New(fmt.Sprintf("invalid argument to Loop2; start position %d must be smaller than the source streamer length %d", l.start, n))
}
if l.start >= l.end {
return nil, errors.New(fmt.Sprintf("invalid argument to Loop2; start position %d must be smaller than the end position %d", l.start, l.end))
}
l.end = min(l.end, n)

return l, nil
}

type loop2 struct {
s StreamSeeker
remains int // number of seeks remaining.
start int // start position in the stream where looping begins. Samples before this position are played once before the first loop.
end int // end position in the stream where looping ends and restarts from `start`.
err error
}

func (l *loop2) Stream(samples [][2]float64) (n int, ok bool) {
if l.err != nil {
return 0, false
}
for len(samples) > 0 {
toStream := len(samples)
if l.remains != 0 {
samplesUntilEnd := l.end - l.s.Position()
if samplesUntilEnd <= 0 {
// End of loop, reset the position and decrease the loop count.
if l.remains > 0 {
l.remains--
}
if err := l.s.Seek(l.start); err != nil {
l.err = err
return n, true
}
continue
}
// Stream only up to the end of the loop.
toStream = min(samplesUntilEnd, toStream)
}

sn, sok := l.s.Stream(samples[:toStream])
n += sn
if sn < toStream || !sok {
l.err = l.s.Err()
return n, n > 0
}
samples = samples[sn:]
}
return n, true
}

func (l *loop2) Err() error {
return l.err
}

// Seq takes zero or more Streamers and returns a Streamer which streams them one by one without pauses.
//
// Seq does not propagate errors from the Streamers.
Expand Down
120 changes: 120 additions & 0 deletions compositors_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package beep_test

import (
"errors"
"math/rand"
"reflect"
"testing"

"github.com/stretchr/testify/assert"

"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/internal/testtools"
)
Expand Down Expand Up @@ -42,6 +45,123 @@ func TestLoop(t *testing.T) {
}
}

func TestLoop2(t *testing.T) {
// LoopStart is bigger than s.Len()
s, _ := testtools.NewSequentialDataStreamer(5)
l, err := beep.Loop2(s, beep.LoopStart(5))
assert.EqualError(t, err, "invalid argument to Loop2; start position 5 must be smaller than the source streamer length 5")

// LoopStart is bigger than LoopEnd
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopBetween(4, 4))
assert.EqualError(t, err, "invalid argument to Loop2; start position 4 must be smaller than the end position 4")

// Loop indefinitely (no options).
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s)
assert.NoError(t, err)
got := testtools.CollectNum(16, l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}}, got)

// Test no loop.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(0))
assert.NoError(t, err)
got = testtools.Collect(l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got)

// Test loop once.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(1))
assert.NoError(t, err)
got = testtools.Collect(l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got)

// Test loop twice.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(2))
assert.NoError(t, err)
got = testtools.Collect(l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got)

// Test loop from start position.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopStart(2))
assert.NoError(t, err)
got = testtools.Collect(l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {2, 2}, {3, 3}, {4, 4}, {2, 2}, {3, 3}, {4, 4}}, got)

// Test loop with end position.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopEnd(4))
assert.NoError(t, err)
got = testtools.Collect(l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got)

// Test loop with start and end position.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4))
assert.NoError(t, err)
got = testtools.Collect(l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {4, 4}}, got)

// Loop indefinitely with both start and end position.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopBetween(2, 4))
assert.NoError(t, err)
got = testtools.CollectNum(10, l)
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {2, 2}, {3, 3}}, got)

//// Test streaming from the middle of the loops.
s, _ = testtools.NewSequentialDataStreamer(5)
l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3
assert.NoError(t, err)
// First stream to the middle of a loop.
buf := make([][2]float64, 3)
if n, ok := l.Stream(buf); n != 3 || !ok {
t.Fatalf("want n %d got %d, want ok %t got %t", 3, n, true, ok)
}
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}}, buf)
// Then stream starting at the middle of the loop.
if n, ok := l.Stream(buf); n != 3 || !ok {
t.Fatalf("want n %d got %d, want ok %t got %t", 3, n, true, ok)
}
assert.Equal(t, [][2]float64{{3, 3}, {2, 2}, {3, 3}}, buf)

// Test error handling in middle of loop.
expectedErr := errors.New("expected error")
s, _ = testtools.NewSequentialDataStreamer(5)
s = testtools.NewDelayedErrorStreamer(s, 5, expectedErr)
l, err = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3
assert.NoError(t, err)
buf = make([][2]float64, 10)
if n, ok := l.Stream(buf); n != 5 || !ok {
t.Fatalf("want n %d got %d, want ok %t got %t", 5, n, true, ok)
}
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {2, 2}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}, buf)
assert.Equal(t, expectedErr, l.Err())
if n, ok := l.Stream(buf); n != 0 || ok {
t.Fatalf("want n %d got %d, want ok %t got %t", 0, n, false, ok)
}
assert.Equal(t, expectedErr, l.Err())

// Test error handling during call to Seek().
s, _ = testtools.NewSequentialDataStreamer(5)
s = testtools.NewSeekErrorStreamer(s, expectedErr)
l, err = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, [error]
assert.NoError(t, err)
buf = make([][2]float64, 10)
if n, ok := l.Stream(buf); n != 4 || !ok {
t.Fatalf("want n %d got %d, want ok %t got %t", 4, n, true, ok)
}
assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}, buf)
assert.Equal(t, expectedErr, l.Err())
if n, ok := l.Stream(buf); n != 0 || ok {
t.Fatalf("want n %d got %d, want ok %t got %t", 0, n, false, ok)
}
assert.Equal(t, expectedErr, l.Err())
}

func TestSeq(t *testing.T) {
var (
n = 7
Expand Down
2 changes: 1 addition & 1 deletion ctrl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ func TestCtrl_PropagatesErrors(t *testing.T) {
assert.NoError(t, ctrl.Err())

err := errors.New("oh no")
ctrl.Streamer = testtools.ErrorStreamer{Error: err}
ctrl.Streamer = testtools.NewErrorStreamer(err)
assert.Equal(t, err, ctrl.Err())
}
Loading
Loading