From 1ce5004aacc76a81bc5891d485eb49f5efc10b1d Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Thu, 8 Aug 2024 19:49:01 +0200 Subject: [PATCH 1/5] Add loop start and stop points --- compositors.go | 104 ++++++++++++++++++++++++++------ compositors_test.go | 56 ++++++++++++----- internal/testtools/streamers.go | 24 +++++++- 3 files changed, 148 insertions(+), 36 deletions(-) diff --git a/compositors.go b/compositors.go index 8dabdb8..f023726 100644 --- a/compositors.go +++ b/compositors.go @@ -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. @@ -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 } diff --git a/compositors_test.go b/compositors_test.go index 79e0830..08b956a 100644 --- a/compositors_test.go +++ b/compositors_test.go @@ -5,6 +5,8 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/gopxl/beep/v2" "github.com/gopxl/beep/v2/internal/testtools" ) @@ -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) { diff --git a/internal/testtools/streamers.go b/internal/testtools/streamers.go index f5d49fa..4b0f3d1 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 { From 31f26b5a96487e54d2c64f5eed466e565994baea Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sun, 11 Aug 2024 14:59:36 +0200 Subject: [PATCH 2/5] Test & improve Loop edge cases - Propagate l.s.Seek() errors. - Make the loop count of 0 backwards compatible - Test error handling --- compositors.go | 28 ++++++---- compositors_test.go | 52 +++++++++++++++++- ctrl_test.go | 2 +- internal/testtools/streamers.go | 97 +++++++++++++++++++++++++++++++-- 4 files changed, 160 insertions(+), 19 deletions(-) diff --git a/compositors.go b/compositors.go index f023726..360ca9a 100644 --- a/compositors.go +++ b/compositors.go @@ -77,10 +77,11 @@ func LoopBetween(start, end int) LoopOption { // 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, + s: s, + remains: count, + finished: count == 0, + start: 0, + end: math.MaxInt, } for _, opt := range opts { opt(l) @@ -99,26 +100,29 @@ func Loop(count int, s StreamSeeker, opts ...LoopOption) Streamer { } 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`. + s StreamSeeker + remains int // number of seeks remaining. + finished bool + 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 *loop) Stream(samples [][2]float64) (n int, ok bool) { - if l.s.Err() != nil { + if l.finished || 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 { + 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 @@ -130,6 +134,8 @@ func (l *loop) Stream(samples [][2]float64) (n int, ok bool) { sn, sok := l.s.Stream(samples[:toStream]) n += sn if sn < toStream || !sok { + l.err = l.s.Err() + l.finished = true return n, n > 0 } samples = samples[sn:] @@ -138,7 +144,7 @@ func (l *loop) Stream(samples [][2]float64) (n int, ok bool) { } func (l *loop) Err() error { - return l.s.Err() + return l.err } // Seq takes zero or more Streamers and returns a Streamer which streams them one by one without pauses. diff --git a/compositors_test.go b/compositors_test.go index 08b956a..8e09caf 100644 --- a/compositors_test.go +++ b/compositors_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/gopxl/beep/v2" @@ -28,9 +29,10 @@ func TestTake(t *testing.T) { func TestLoop(t *testing.T) { // Test no loop. - s, data := testtools.NewSequentialDataStreamer(5) + // For backwards compatibility, a loop count of 0 means that nothing at all will be played. + s, _ := testtools.NewSequentialDataStreamer(5) got := testtools.Collect(beep.Loop(0, s)) - assert.Equal(t, data, got) + assert.Empty(t, got) // Test loop once. s, _ = testtools.NewSequentialDataStreamer(5) @@ -66,6 +68,52 @@ func TestLoop(t *testing.T) { 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) + + // 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 + // 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) + } + 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) + } + 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 = beep.Loop(3, s, 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) + } + 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 = beep.Loop(3, s, 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) + } + 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) { 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 4b0f3d1..48e02be 100644 --- a/internal/testtools/streamers.go +++ b/internal/testtools/streamers.go @@ -65,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 } From dc893272a451525e50c0cc73d3dd90a844760cf6 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Thu, 5 Sep 2024 15:27:17 +0200 Subject: [PATCH 3/5] Deprecate original Loop but keep it alongside the new Loop2 --- compositors.go | 92 +++++++++++++++++++++++++++++++++++++-------- compositors_test.go | 61 +++++++++++++++++++----------- 2 files changed, 116 insertions(+), 37 deletions(-) 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) From dc80e9a8cece0a089604b9d57919709e63497709 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Thu, 5 Sep 2024 15:32:45 +0200 Subject: [PATCH 4/5] Remove unnecessary finished property --- compositors.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/compositors.go b/compositors.go index ae167ac..d065707 100644 --- a/compositors.go +++ b/compositors.go @@ -139,11 +139,10 @@ func LoopBetween(start, end int) LoopOption { // The returned Streamer propagates any errors from s. func Loop2(s StreamSeeker, opts ...LoopOption) Streamer { l := &loop2{ - s: s, - remains: -1, // indefinitely - finished: false, - start: 0, - end: math.MaxInt, + s: s, + remains: -1, // indefinitely + start: 0, + end: math.MaxInt, } for _, opt := range opts { opt(l) @@ -162,16 +161,15 @@ func Loop2(s StreamSeeker, opts ...LoopOption) Streamer { } type loop2 struct { - s StreamSeeker - remains int // number of seeks remaining. - finished bool - 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 + 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.finished || l.err != nil { + if l.err != nil { return 0, false } for len(samples) > 0 { @@ -197,7 +195,6 @@ func (l *loop2) Stream(samples [][2]float64) (n int, ok bool) { n += sn if sn < toStream || !sok { l.err = l.s.Err() - l.finished = true return n, n > 0 } samples = samples[sn:] From 8f9a3f13a8e6b27edee62ccd6f588a9630f29b9b Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Thu, 5 Sep 2024 15:49:39 +0200 Subject: [PATCH 5/5] Replace panics with errors in Loop2 --- compositors.go | 12 +++++----- compositors_test.go | 53 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/compositors.go b/compositors.go index d065707..66ed7c0 100644 --- a/compositors.go +++ b/compositors.go @@ -3,6 +3,8 @@ package beep import ( "fmt" "math" + + "github.com/pkg/errors" ) // Take returns a Streamer which streams at most num samples from s. @@ -137,7 +139,7 @@ func LoopBetween(start, end int) LoopOption { // 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 { +func Loop2(s StreamSeeker, opts ...LoopOption) (Streamer, error) { l := &loop2{ s: s, remains: -1, // indefinitely @@ -150,14 +152,14 @@ func Loop2(s StreamSeeker, opts ...LoopOption) Streamer { 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)) + 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 { - panic(fmt.Sprintf("invalid argument to Loop; start position %d must be smaller than the end position %d", l.start, l.end)) + 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 + return l, nil } type loop2 struct { diff --git a/compositors_test.go b/compositors_test.go index de05412..6397cb1 100644 --- a/compositors_test.go +++ b/compositors_test.go @@ -46,49 +46,76 @@ func TestLoop(t *testing.T) { } func TestLoop2(t *testing.T) { - // Loop indefinitely (no options). + // LoopStart is bigger than s.Len() s, _ := testtools.NewSequentialDataStreamer(5) - got := testtools.CollectNum(16, beep.Loop2(s)) + 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) - got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(0))) + 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) - got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(1))) + 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) - got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2))) + 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) - got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2), beep.LoopStart(2))) + 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) - got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2), beep.LoopEnd(4))) + 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) - got = testtools.Collect(beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4))) + 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) - got = testtools.CollectNum(10, beep.Loop2(s, beep.LoopBetween(2, 4))) + 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 := beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 + 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 { @@ -105,7 +132,8 @@ func TestLoop2(t *testing.T) { expectedErr := errors.New("expected error") s, _ = testtools.NewSequentialDataStreamer(5) s = testtools.NewDelayedErrorStreamer(s, 5, expectedErr) - l = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 + 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) @@ -120,7 +148,8 @@ func TestLoop2(t *testing.T) { // Test error handling during call to Seek(). s, _ = testtools.NewSequentialDataStreamer(5) s = testtools.NewSeekErrorStreamer(s, expectedErr) - l = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, [error] + 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)