diff --git a/compositors.go b/compositors.go index 8dabdb8..66ed7c0 100644 --- a/compositors.go +++ b/compositors.go @@ -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. @@ -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, @@ -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. diff --git a/compositors_test.go b/compositors_test.go index 79e0830..6397cb1 100644 --- a/compositors_test.go +++ b/compositors_test.go @@ -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" ) @@ -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 diff --git a/ctrl_test.go b/ctrl_test.go index ae0929c..c86b147 100644 --- a/ctrl_test.go +++ b/ctrl_test.go @@ -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()) } diff --git a/internal/testtools/streamers.go b/internal/testtools/streamers.go index f5d49fa..48e02be 100644 --- a/internal/testtools/streamers.go +++ b/internal/testtools/streamers.go @@ -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 { @@ -47,14 +65,101 @@ func (ds *dataStreamer) Seek(p int) error { return nil } +// NewErrorStreamer returns a streamer which errors immediately with the given err. +func NewErrorStreamer(err error) beep.StreamSeeker { + return &ErrorStreamer{ + s: beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { + panic("unreachable") + }), + samplesLeft: 0, + Error: err, + } +} + +// NewDelayedErrorStreamer wraps streamer s but returns an error after numSamples have been streamed. +func NewDelayedErrorStreamer(s beep.Streamer, numSamples int, err error) beep.StreamSeeker { + return &ErrorStreamer{ + s: s, + samplesLeft: numSamples, + Error: err, + } +} + type ErrorStreamer struct { - Error error + s beep.Streamer + samplesLeft int + Error error +} + +func (e *ErrorStreamer) Stream(samples [][2]float64) (n int, ok bool) { + if e.samplesLeft == 0 { + return 0, false + } + + toStream := min(e.samplesLeft, len(samples)) + n, ok = e.s.Stream(samples[:toStream]) + e.samplesLeft -= n + + return n, ok +} + +func (e *ErrorStreamer) Err() error { + if e.samplesLeft == 0 { + return e.Error + } else { + return e.s.Err() + } +} + +func (e *ErrorStreamer) Seek(p int) error { + if s, ok := e.s.(beep.StreamSeeker); ok { + return s.Seek(p) + } + panic("source streamer is not a beep.StreamSeeker") +} + +func (e *ErrorStreamer) Len() int { + if s, ok := e.s.(beep.StreamSeeker); ok { + return s.Len() + } + panic("source streamer is not a beep.StreamSeeker") +} + +func (e *ErrorStreamer) Position() int { + if s, ok := e.s.(beep.StreamSeeker); ok { + return s.Position() + } + panic("source streamer is not a beep.StreamSeeker") +} + +func NewSeekErrorStreamer(s beep.StreamSeeker, err error) *SeekErrorStreamer { + return &SeekErrorStreamer{ + s: s, + err: err, + } +} + +type SeekErrorStreamer struct { + s beep.StreamSeeker + err error +} + +func (s *SeekErrorStreamer) Stream(samples [][2]float64) (n int, ok bool) { + return s.s.Stream(samples) +} + +func (s *SeekErrorStreamer) Err() error { + return s.s.Err() +} + +func (s *SeekErrorStreamer) Len() int { + return s.s.Len() } -func (e ErrorStreamer) Stream(samples [][2]float64) (n int, ok bool) { - return 0, false +func (s *SeekErrorStreamer) Position() int { + return s.s.Position() } -func (e ErrorStreamer) Err() error { - return e.Error +func (s *SeekErrorStreamer) Seek(p int) error { + return s.err }