diff --git a/compositors.go b/compositors.go index 360ca9a..ae167ac 100644 --- a/compositors.go +++ b/compositors.go @@ -34,7 +34,69 @@ func (t *take) Err() error { return t.s.Err() } -type LoopOption func(opts *loop) +// 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, + remains: count, + } +} + +type loop struct { + s StreamSeeker + remains int +} + +func (l *loop) Stream(samples [][2]float64) (n int, ok bool) { + if l.remains == 0 || 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 + } + continue + } + samples = samples[sn:] + n += sn + } + return n, true +} + +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 @@ -43,7 +105,7 @@ func LoopStart(pos int) LoopOption { if pos < 0 { panic("invalid argument to LoopStart; pos cannot be negative") } - return func(loop *loop) { + return func(loop *loop2) { loop.start = pos } } @@ -55,7 +117,7 @@ func LoopEnd(pos int) LoopOption { if pos < 0 { panic("invalid argument to LoopEnd; pos cannot be negative") } - return func(loop *loop) { + return func(loop *loop2) { loop.end = pos } } @@ -63,23 +125,23 @@ func LoopEnd(pos int) LoopOption { // 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) { + return func(opts *loop2) { 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. +// 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 Loop(count int, s StreamSeeker, opts ...LoopOption) Streamer { - l := &loop{ +func Loop2(s StreamSeeker, opts ...LoopOption) Streamer { + l := &loop2{ s: s, - remains: count, - finished: count == 0, + remains: -1, // indefinitely + finished: false, start: 0, end: math.MaxInt, } @@ -99,7 +161,7 @@ func Loop(count int, s StreamSeeker, opts ...LoopOption) Streamer { return l } -type loop struct { +type loop2 struct { s StreamSeeker remains int // number of seeks remaining. finished bool @@ -108,7 +170,7 @@ type loop struct { err error } -func (l *loop) Stream(samples [][2]float64) (n int, ok bool) { +func (l *loop2) Stream(samples [][2]float64) (n int, ok bool) { if l.finished || l.err != nil { return 0, false } @@ -143,7 +205,7 @@ func (l *loop) Stream(samples [][2]float64) (n int, ok bool) { return n, true } -func (l *loop) Err() error { +func (l *loop2) Err() error { return l.err } diff --git a/compositors_test.go b/compositors_test.go index 8e09caf..de05412 100644 --- a/compositors_test.go +++ b/compositors_test.go @@ -1,11 +1,11 @@ package beep_test import ( + "errors" "math/rand" "reflect" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/gopxl/beep/v2" @@ -28,59 +28,76 @@ func TestTake(t *testing.T) { } func TestLoop(t *testing.T) { - // Test no loop. - // For backwards compatibility, a loop count of 0 means that nothing at all will be played. + 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") + } + } + } +} + +func TestLoop2(t *testing.T) { + // Loop indefinitely (no options). s, _ := testtools.NewSequentialDataStreamer(5) - got := testtools.Collect(beep.Loop(0, s)) - assert.Empty(t, got) + got := testtools.CollectNum(16, beep.Loop2(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 no loop. + s, _ = testtools.NewSequentialDataStreamer(5) + got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(0))) + assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got) // Test loop once. s, _ = testtools.NewSequentialDataStreamer(5) - got = testtools.Collect(beep.Loop(1, s)) + got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(1))) 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)) + got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2))) 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))) + got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2), 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))) + got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2), 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))) + got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2), 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))) + got = testtools.CollectNum(10, beep.Loop2(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) - // Test streaming from the middle of the loops. + //// Test streaming from the middle of the loops. s, _ = testtools.NewSequentialDataStreamer(5) - l := beep.Loop(2, s, beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 + l := beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 // 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", 5, n, true, 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", 5, n, true, 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) @@ -88,7 +105,7 @@ func TestLoop(t *testing.T) { expectedErr := errors.New("expected error") s, _ = testtools.NewSequentialDataStreamer(5) s = testtools.NewDelayedErrorStreamer(s, 5, expectedErr) - l = beep.Loop(3, s, beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 + l = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 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) @@ -103,7 +120,7 @@ func TestLoop(t *testing.T) { // Test error handling during call to Seek(). s, _ = testtools.NewSequentialDataStreamer(5) s = testtools.NewSeekErrorStreamer(s, expectedErr) - l = beep.Loop(3, s, beep.LoopBetween(2, 4)) // 0, 1, 2, 3, [error] + l = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, [error] 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)