Skip to content

Commit

Permalink
Add loop start and stop points
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkKremer committed Aug 8, 2024
1 parent efa5fae commit 1ce5004
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 36 deletions.
104 changes: 86 additions & 18 deletions compositors.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package beep

import (
"fmt"
"math"
)

// 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 @@ -29,42 +34,105 @@ func (t *take) Err() error {
return t.s.Err()
}

// Loop takes a StreamSeeker and plays it count times. If count is negative, s is looped infinitely.
type LoopOption func(opts *loop)

// 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 *loop) {
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 *loop) {
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 *loop) {
LoopStart(start)(opts)
LoopEnd(end)(opts)
}
}

// Loop takes a StreamSeeker and plays it the specified number of times. If count is negative,
// s loops indefinitely. LoopStart, LoopEnd, or LoopBetween can be used to define a specific
// section of the stream to loop. The samples before the start and after the end positions are
// played once before and after the looping section, respectively.
//
// The returned Streamer propagates s's errors.
func Loop(count int, s StreamSeeker) Streamer {
return &loop{
// The returned Streamer propagates any errors from s.
func Loop(count int, s StreamSeeker, opts ...LoopOption) Streamer {
l := &loop{
s: s,
remains: count,
start: 0,
end: math.MaxInt,
}
for _, opt := range opts {
opt(l)
}

n := s.Len()
if l.start >= n {
panic(fmt.Sprintf("invalid argument to Loop; start position %d is bigger than the length %d of the source streamer", l.start, n))
}
if l.start > l.end {
panic(fmt.Sprintf("invalid argument to Loop; start position %d must be smaller than the end position %d", l.start, l.end))
}
l.end = min(l.end, n)

return l
}

type loop struct {
s StreamSeeker
remains int
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`.
}

func (l *loop) Stream(samples [][2]float64) (n int, ok bool) {
if l.remains == 0 || l.s.Err() != nil {
if l.s.Err() != nil {
return 0, false
}
for len(samples) > 0 {
sn, sok := l.s.Stream(samples)
if !sok {
if l.remains > 0 {
l.remains--
}
if l.remains == 0 {
break
}
err := l.s.Seek(0)
if err != nil {
return n, true
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 {
return n, true
}
continue
}
continue
// Stream only up to the end of the loop.
toStream = min(samplesUntilEnd, toStream)
}
samples = samples[sn:]

sn, sok := l.s.Stream(samples[:toStream])
n += sn
if sn < toStream || !sok {
return n, n > 0
}
samples = samples[sn:]
}
return n, true
}
Expand Down
56 changes: 41 additions & 15 deletions compositors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"reflect"
"testing"

"github.com/stretchr/testify/assert"

"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/internal/testtools"
)
Expand All @@ -25,21 +27,45 @@ func TestTake(t *testing.T) {
}

func TestLoop(t *testing.T) {
for i := 0; i < 7; i++ {
for n := 0; n < 5; n++ {
s, data := testtools.RandomDataStreamer(10)

var want [][2]float64
for j := 0; j < n; j++ {
want = append(want, data...)
}
got := testtools.Collect(beep.Loop(n, s))

if !reflect.DeepEqual(want, got) {
t.Error("Loop not working correctly")
}
}
}
// Test no loop.
s, data := testtools.NewSequentialDataStreamer(5)
got := testtools.Collect(beep.Loop(0, s))
assert.Equal(t, data, got)

// Test loop once.
s, _ = testtools.NewSequentialDataStreamer(5)
got = testtools.Collect(beep.Loop(1, s))
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)
got = testtools.Collect(beep.Loop(2, s))
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)

// Loop indefinitely.
s, _ = testtools.NewSequentialDataStreamer(5)
got = testtools.CollectNum(16, beep.Loop(-1, s))
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 loop from start position.
s, _ = testtools.NewSequentialDataStreamer(5)
got = testtools.Collect(beep.Loop(2, s, beep.LoopStart(2)))
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)
got = testtools.Collect(beep.Loop(2, s, beep.LoopEnd(4)))
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)
got = testtools.Collect(beep.Loop(2, s, beep.LoopBetween(2, 4)))
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)
got = testtools.CollectNum(10, beep.Loop(-1, s, beep.LoopBetween(2, 4)))
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)
}

func TestSeq(t *testing.T) {
Expand Down
24 changes: 21 additions & 3 deletions internal/testtools/streamers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,28 @@ import (
func RandomDataStreamer(numSamples int) (s beep.StreamSeeker, data [][2]float64) {
data = make([][2]float64, numSamples)
for i := range data {
data[i][0] = rand.Float64()*2 - 1
data[i][1] = rand.Float64()*2 - 1
data[i] = [2]float64{
rand.Float64()*2 - 1,
rand.Float64()*2 - 1,
}
}
return &dataStreamer{data, 0}, data
return NewDataStreamer(data), data
}

// NewSequentialDataStreamer creates a streamer which streams samples with values {0, 0}, {1, 1}, {2, 2}, etc.
// Note that this aren't valid sample values in the range of [-1, 1], but it can nonetheless
// be useful for testing.
func NewSequentialDataStreamer(numSamples int) (s beep.StreamSeeker, data [][2]float64) {
data = make([][2]float64, numSamples)
for i := range data {
data[i] = [2]float64{float64(i), float64(i)}
}
return NewDataStreamer(data), data
}

// NewDataStreamer creates a streamer which streams the given data.
func NewDataStreamer(data [][2]float64) (s beep.StreamSeeker) {
return &dataStreamer{data, 0}
}

type dataStreamer struct {
Expand Down

0 comments on commit 1ce5004

Please sign in to comment.