From 1ce5004aacc76a81bc5891d485eb49f5efc10b1d Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Thu, 8 Aug 2024 19:49:01 +0200 Subject: [PATCH] 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 {