diff --git a/effects/transition.go b/effects/transition.go new file mode 100644 index 0000000..a0af349 --- /dev/null +++ b/effects/transition.go @@ -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() +} diff --git a/effects/transition_test.go b/effects/transition_test.go new file mode 100644 index 0000000..018fd3b --- /dev/null +++ b/effects/transition_test.go @@ -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 +}