Skip to content

Commit

Permalink
Merge pull request #141 from gopxl/transition-effect
Browse files Browse the repository at this point in the history
Add effects.Transition and a cross-fade example
  • Loading branch information
MarkKremer authored Jan 24, 2024
2 parents 8c669d5 + 7b89ccb commit fcaaf86
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 0 deletions.
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
}

0 comments on commit fcaaf86

Please sign in to comment.