From 4abadd4aa81853d26f10ba49fcfd7f6622f63c13 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 3 Jan 2024 19:27:21 +0100 Subject: [PATCH 1/5] Add KeepAlive method to Mixer --- mixer.go | 42 ++++++++++++++++++++++++++++++++++-------- mixer_test.go | 21 +++++++-------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/mixer.go b/mixer.go index 9f0dd19..3764904 100644 --- a/mixer.go +++ b/mixer.go @@ -1,9 +1,17 @@ package beep // Mixer allows for dynamic mixing of arbitrary number of Streamers. Mixer automatically removes -// drained Streamers. Mixer's stream never drains, when empty, Mixer streams silence. +// drained Streamers. Depending on the KeepAlive() setting, Stream will either play silence or +// drain when all Streamers have been drained. By default, Mixer keeps playing silence. type Mixer struct { - streamers []Streamer + streamers []Streamer + stopWhenEmpty bool +} + +// KeepAlive configures the Mixer to either keep playing silence when all its Streamers have +// drained (keepAlive == true) or stop playing (keepAlive == false). +func (m *Mixer) KeepAlive(keepAlive bool) { + m.stopWhenEmpty = !keepAlive } // Len returns the number of Streamers currently playing in the Mixer. @@ -18,12 +26,20 @@ func (m *Mixer) Add(s ...Streamer) { // Clear removes all Streamers from the mixer. func (m *Mixer) Clear() { + for i := range m.streamers { + m.streamers[i] = nil + } m.streamers = m.streamers[:0] } -// Stream streams all Streamers currently in the Mixer mixed together. This method always returns -// len(samples), true. If there are no Streamers available, this methods streams silence. +// Stream the samples of all Streamers currently in the Mixer mixed together. Depending on the +// KeepAlive() setting, Stream will either play silence or drain when all Streamers have been +// drained. func (m *Mixer) Stream(samples [][2]float64) (n int, ok bool) { + if m.stopWhenEmpty && len(m.streamers) == 0 { + return 0, false + } + var tmp [512][2]float64 for len(samples) > 0 { @@ -37,6 +53,7 @@ func (m *Mixer) Stream(samples [][2]float64) (n int, ok bool) { samples[i] = [2]float64{} } + snMax := 0 for si := 0; si < len(m.streamers); si++ { // mix the stream sn, sok := m.streamers[si].Stream(tmp[:toStream]) @@ -44,12 +61,21 @@ func (m *Mixer) Stream(samples [][2]float64) (n int, ok bool) { samples[i][0] += tmp[i][0] samples[i][1] += tmp[i][1] } - if !sok { + if sn > snMax { + snMax = sn + } + + if sn < toStream || !sok { // remove drained streamer - sj := len(m.streamers) - 1 - m.streamers[si], m.streamers[sj] = m.streamers[sj], m.streamers[si] - m.streamers = m.streamers[:sj] + last := len(m.streamers) - 1 + m.streamers[si] = m.streamers[last] + m.streamers[last] = nil + m.streamers = m.streamers[:last] si-- + + if m.stopWhenEmpty && len(m.streamers) == 0 { + return n + snMax, true + } } } diff --git a/mixer_test.go b/mixer_test.go index e19218e..f2cae82 100644 --- a/mixer_test.go +++ b/mixer_test.go @@ -51,24 +51,23 @@ func TestMixer_MixesSamples(t *testing.T) { func TestMixer_DrainedStreamersAreRemoved(t *testing.T) { s1, _ := testtools.RandomDataStreamer(50) - s2, _ := testtools.RandomDataStreamer(60) + s2, _ := testtools.RandomDataStreamer(65) m := beep.Mixer{} m.Add(s1) m.Add(s2) - // Drain s1 but not so far it returns false. + // Almost drain s1 samples := testtools.CollectNum(50, &m) assert.Len(t, samples, 50) assert.Equal(t, 2, m.Len()) - // Fully drain s1. - // Drain s2 but not so far it returns false. + // Drain s1 (s1 returns !ok, n == 0) samples = testtools.CollectNum(10, &m) assert.Len(t, samples, 10) assert.Equal(t, 1, m.Len()) - // Fully drain s2. + // Drain s2 (s2 returns ok, n < len(samples)) samples = testtools.CollectNum(10, &m) assert.Len(t, samples, 10) assert.Equal(t, 0, m.Len()) @@ -82,22 +81,16 @@ func TestMixer_PlaysSilenceWhenNoStreamersProduceSamples(t *testing.T) { assert.Len(t, samples, 10) assert.Equal(t, make([][2]float64, 10), samples) - // Test silence after streamer is partly drained. + // Test silence after streamer has only streamed part of the requested samples. s, _ := testtools.RandomDataStreamer(50) m.Add(s) samples = testtools.CollectNum(100, &m) assert.Len(t, samples, 100) - assert.Equal(t, 1, m.Len()) - assert.Equal(t, make([][2]float64, 50), samples[50:]) - - // Test silence when streamer is fully drained. - samples = testtools.CollectNum(10, &m) - assert.Len(t, samples, 10) assert.Equal(t, 0, m.Len()) - assert.Equal(t, make([][2]float64, 10), samples) + assert.Equal(t, make([][2]float64, 50), samples[50:]) - // Test silence after streamer was fully drained. + // Test silence after streamers have been drained & removed. samples = testtools.CollectNum(10, &m) assert.Len(t, samples, 10) assert.Equal(t, make([][2]float64, 10), samples) From 746bf706b84010f7dcb2d3775a9cefac37baf3b3 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 3 Jan 2024 19:28:27 +0100 Subject: [PATCH 2/5] Allow using samples as scratch space when Stream returns n < len(samples) --- interface.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface.go b/interface.go index 93ab4f1..30d7db0 100644 --- a/interface.go +++ b/interface.go @@ -11,8 +11,8 @@ type Streamer interface { // Similarly, samples[i][1] is the value of the right channel of the i-th sample. // // Stream returns the number of streamed samples. If the Streamer is drained and no more - // samples will be produced, it returns 0 and false. Stream must not touch any samples - // outside samples[:n]. + // samples will be produced, it returns 0 and false. Even if Stream returns n < len(samples), + // it may use all of samples as scratch space during the call. // // There are 3 valid return patterns of the Stream method: // From 2858cbe05d595dde77fb24b11645ff0f422d4126 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 3 Jan 2024 19:29:08 +0100 Subject: [PATCH 3/5] Replace the implementation of Mix with Mixer --- compositors.go | 42 ++++------------------------------- compositors_test.go | 4 +--- internal/testtools/asserts.go | 22 ++++++++++++++++++ 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/compositors.go b/compositors.go index d874d9f..755cab2 100644 --- a/compositors.go +++ b/compositors.go @@ -98,44 +98,10 @@ func Seq(s ...Streamer) Streamer { // // Mix does not propagate errors from the Streamers. func Mix(s ...Streamer) Streamer { - return StreamerFunc(func(samples [][2]float64) (n int, ok bool) { - var tmp [512][2]float64 - - for len(samples) > 0 { - toStream := len(tmp) - if toStream > len(samples) { - toStream = len(samples) - } - - // clear the samples - for i := range samples[:toStream] { - samples[i] = [2]float64{} - } - - snMax := 0 // max number of streamed samples in this iteration - for _, st := range s { - // mix the stream - sn, sok := st.Stream(tmp[:toStream]) - if sn > snMax { - snMax = sn - } - ok = ok || sok - - for i := range tmp[:sn] { - samples[i][0] += tmp[i][0] - samples[i][1] += tmp[i][1] - } - } - - n += snMax - if snMax < len(tmp) { - break - } - samples = samples[snMax:] - } - - return n, ok - }) + return &Mixer{ + streamers: s, + stopWhenEmpty: true, + } } // Dup returns two Streamers which both stream the same data as the original s. The two Streamers diff --git a/compositors_test.go b/compositors_test.go index 53b9dd5..24a1ca5 100644 --- a/compositors_test.go +++ b/compositors_test.go @@ -91,9 +91,7 @@ func TestMix(t *testing.T) { got := testtools.Collect(beep.Mix(s...)) - if !reflect.DeepEqual(want, got) { - t.Error("Mix not working correctly") - } + testtools.AssertSamplesEqual(t, want, got) } func TestDup(t *testing.T) { diff --git a/internal/testtools/asserts.go b/internal/testtools/asserts.go index 3f48dca..f9afe95 100644 --- a/internal/testtools/asserts.go +++ b/internal/testtools/asserts.go @@ -57,3 +57,25 @@ func AssertStreamerHasCorrectReturnBehaviour(t *testing.T, s beep.Streamer, expe assert.Equal(t, 0, n) assert.NoError(t, s.Err()) } + +func AssertSamplesEqual(t *testing.T, expected, actual [][2]float64) { + t.Helper() + + if len(expected) != len(actual) { + t.Errorf("expected sample data length to be %d, got %d", len(expected), len(actual)) + return + } + + const epsilon = 1e-9 + equals := true + for i := range expected { + if actual[i][0] < expected[i][0]-epsilon || actual[i][0] > expected[i][0]+epsilon || + actual[i][1] < expected[i][1]-epsilon || actual[i][1] > expected[i][1]+epsilon { + equals = false + break + } + } + if !equals { + t.Errorf("the sample data isn't equal to the expected data") + } +} From 745b86e1f77bfb6a03600fae60de50336557e990 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 11 Sep 2024 16:04:52 +0200 Subject: [PATCH 4/5] Revert change to interface This change must wait until we're releasing Beep v3. --- interface.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface.go b/interface.go index 30d7db0..93ab4f1 100644 --- a/interface.go +++ b/interface.go @@ -11,8 +11,8 @@ type Streamer interface { // Similarly, samples[i][1] is the value of the right channel of the i-th sample. // // Stream returns the number of streamed samples. If the Streamer is drained and no more - // samples will be produced, it returns 0 and false. Even if Stream returns n < len(samples), - // it may use all of samples as scratch space during the call. + // samples will be produced, it returns 0 and false. Stream must not touch any samples + // outside samples[:n]. // // There are 3 valid return patterns of the Stream method: // From 8326e3abf14209e849545e2552ab35af1401e8d9 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 11 Sep 2024 16:05:07 +0200 Subject: [PATCH 5/5] Small improvements to Mixer tests --- mixer_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mixer_test.go b/mixer_test.go index 68dc4fd..843ee20 100644 --- a/mixer_test.go +++ b/mixer_test.go @@ -57,7 +57,7 @@ func TestMixer_DrainedStreamersAreRemoved(t *testing.T) { m.Add(s1) m.Add(s2) - // Almost drain s1 + // Drain s1 but not so far it returns false. samples := testtools.CollectNum(50, &m) assert.Len(t, samples, 50) assert.Equal(t, 2, m.Len()) @@ -82,12 +82,12 @@ func TestMixer_PlaysSilenceWhenNoStreamersProduceSamples(t *testing.T) { assert.Equal(t, make([][2]float64, 10), samples) // Test silence after streamer has only streamed part of the requested samples. - s, _ := testtools.RandomDataStreamer(50) + s, data := testtools.RandomDataStreamer(50) m.Add(s) - samples = testtools.CollectNum(100, &m) assert.Len(t, samples, 100) assert.Equal(t, 0, m.Len()) + assert.Equal(t, data, samples[:50]) assert.Equal(t, make([][2]float64, 50), samples[50:]) // Test silence after streamers have been drained & removed.