Skip to content

Commit

Permalink
Merge pull request #176 from gopxl/loop-start-and-stop-points
Browse files Browse the repository at this point in the history
Add `Loop2` that supports start and end positions
  • Loading branch information
MarkKremer authored Sep 5, 2024
2 parents 696dcd5 + 8f9a3f1 commit 01d1fa4
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 9 deletions.
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

0 comments on commit 01d1fa4

Please sign in to comment.