Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add effects.Transition and a cross-fade example #141

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions effects/transition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package effects

import (
"math"

"github.com/gopxl/beep"
)

// TransitionFunc defines a function used in a transition to describe the progression curve
// from one value to the next. The input 'percent' always ranges from 0.0 to 1.0, where 0.0
// represents the starting point and 1.0 represents the end point of the transition.
//
// The returned value from TransitionFunc is expected to be in the normalized range of [0.0, 1.0].
// However, it may exceed this range, providing flexibility to generate curves with momentum.
// The Transition() function then maps this normalized output to the actual desired range.
type TransitionFunc func(percent float64) float64

// TransitionLinear transitions the gain linearly from the start to end value.
func TransitionLinear(percent float64) float64 {
return percent
}

// TransitionEqualPower transitions the gain of a streamer in such a way that the total perceived volume stays
// constant if mixed together with another streamer doing the inverse transition.
//
// See https://www.oreilly.com/library/view/web-audio-api/9781449332679/ch03.html#s03_2 for more information.
func TransitionEqualPower(percent float64) float64 {
return math.Cos((1.0 - percent) * 0.5 * math.Pi)
}

// Transition gradually adjusts the gain of the source streamer 's' from 'startGain' to 'endGain'
// over the entire duration of the stream, defined by the number of samples 'len'.
// The transition is defined by the provided 'transitionFunc' function, which determines the
// gain at each point during the transition.
func Transition(s beep.Streamer, len int, startGain, endGain float64, transitionfunc TransitionFunc) *TransitionStreamer {
return &TransitionStreamer{
s: s,
len: len,
startGain: startGain,
endGain: endGain,
transitionFunc: transitionfunc,
}
}

type TransitionStreamer struct {
s beep.Streamer
pos int
len int
startGain, endGain float64
transitionFunc TransitionFunc
}

// Stream fills samples with the gain-adjusted samples of the source streamer.
func (t *TransitionStreamer) Stream(samples [][2]float64) (n int, ok bool) {
n, ok = t.s.Stream(samples)

for i := 0; i < n; i++ {
pos := t.pos + i
progress := float64(pos) / float64(t.len)
if progress < 0 {
progress = 0
} else if progress > 1 {
progress = 1
}
value := t.transitionFunc(progress)
gain := t.startGain + (t.endGain-t.startGain)*value

samples[i][0] *= gain
samples[i][1] *= gain
}

t.pos += n

return
}

// Err propagates the original Streamer's errors.
func (t *TransitionStreamer) Err() error {
return t.s.Err()
}
64 changes: 64 additions & 0 deletions effects/transition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package effects_test

import (
"time"

"github.com/gopxl/beep"
"github.com/gopxl/beep/effects"
"github.com/gopxl/beep/generators"
"github.com/gopxl/beep/speaker"
)

// Cross-fade between two sine tones.
func ExampleTransition() {
sampleRate := beep.SampleRate(44100)

s1, err := generators.SineTone(sampleRate, 261.63)
if err != nil {
panic(err)
}
s2, err := generators.SineTone(sampleRate, 329.628)
if err != nil {
panic(err)
}

crossFades := beep.Seq(
// Play s1 normally for 3 seconds
beep.Take(sampleRate.N(time.Second*3), s1),
// Play s1 and s2 together. s1 transitions from a gain of 1.0 (normal volume)
// to 0.0 (silent) whereas s2 does the opposite. The equal power transition
// function helps keep the overall volume constant.
beep.Mix(
effects.Transition(
beep.Take(sampleRate.N(time.Second*2), s1),
sampleRate.N(time.Second*2),
1.0,
0.0,
effects.TransitionEqualPower,
),
effects.Transition(
beep.Take(sampleRate.N(time.Second*2), s2),
sampleRate.N(time.Second*2),
0.0,
1.0,
effects.TransitionEqualPower,
),
),
// Play the rest of s2 normally for 3 seconds
beep.Take(sampleRate.N(time.Second*3), s2),
)

err = speaker.Init(sampleRate, sampleRate.N(time.Second/30))
if err != nil {
panic(err)
}

done := make(chan struct{})
speaker.Play(beep.Seq(
crossFades,
beep.Callback(func() {
done <- struct{}{}
}),
))
<-done
}